mysql_genius 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d2b3f6350a4c464b3ec546a5248a9b1db4dc933e80e2da26472012367740828
4
- data.tar.gz: 13ffee6262a381bb970cf6f9e473c95e40161d191249228225b6960aec7c9313
3
+ metadata.gz: 15b1fc129dbade000c7a7467601b927804f11f73892a0f450f92f6ffc1d4b313
4
+ data.tar.gz: d01c43dd9e9e06212c6fa85cad829fe940c982b1a1cf412ae02336de69bde376
5
5
  SHA512:
6
- metadata.gz: 350005c5824825d20f187099dab6f181eb476d1b99441fdf2101f09430b4bb23abff72bbbae487f9a58bca43de02a20213e508980dbb407577678c45bbdf3f08
7
- data.tar.gz: c372b88067e33f91b89840e994733ffb5a810ac0bdf38a582e32e240126d026871babe689911a4655bf7a1ba1d0e7664e4f3c7185ba1a96472a6f65d349ecfec
6
+ metadata.gz: 3b9e69fee6f8b6c79e36bdccca6c838876f10fb79b333464496f37aa14db50d42b2507a4dfd18e0162c2b612af4279bf4ea731cd765da83068a23f3e303c340e
7
+ data.tar.gz: cf0730102bb3ce854f669dcf77343c3f5f0704996e1fa5bbe0c08c6a02f34110c59dfa53d9a303044f5c8ac6417f9e6f2863d3cc1d2535e84f108922b3379d0e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Added
6
+ - **6 new AI analysis features:**
7
+ - **Variable Config Reviewer** — reviews my.cnf settings against observed workload
8
+ - **Connection Pressure Advisor** — diagnoses connection pool health
9
+ - **Workload Digest** — executive summary of the entire query workload
10
+ - **InnoDB Health Interpreter** — plain English translation of `SHOW ENGINE INNODB STATUS`
11
+ - **Index Consolidation Planner** — holistic drop/merge/add index plan across tables
12
+ - **Slow Query Pattern Grouper** — groups slow queries by shared root cause
13
+ - AI buttons added to Server tab, Query Stats tab, and Indexes tabs
14
+ - `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.8.0`
15
+
3
16
  ## 0.7.2
4
17
 
5
18
  ### Added
@@ -232,6 +232,61 @@ module MysqlGenius
232
232
  render(json: { error: "Migration risk assessment failed: #{e.message}" }, status: :unprocessable_entity)
233
233
  end
234
234
 
235
+ def variable_review
236
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
237
+
238
+ result = MysqlGenius::Core::Ai::VariableReviewer.new(ai_client, ai_config_for_core, rails_connection).call
239
+ render(json: result)
240
+ rescue StandardError => e
241
+ render(json: { error: "Variable review failed: #{e.message}" }, status: :unprocessable_entity)
242
+ end
243
+
244
+ def connection_advisor
245
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
246
+
247
+ result = MysqlGenius::Core::Ai::ConnectionAdvisor.new(ai_client, ai_config_for_core, rails_connection).call
248
+ render(json: result)
249
+ rescue StandardError => e
250
+ render(json: { error: "Connection advisor failed: #{e.message}" }, status: :unprocessable_entity)
251
+ end
252
+
253
+ def workload_digest
254
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
255
+
256
+ result = MysqlGenius::Core::Ai::WorkloadDigest.new(rails_connection, ai_client, ai_config_for_core).call
257
+ render(json: result)
258
+ rescue StandardError => e
259
+ render(json: { error: "Workload digest failed: #{e.message}" }, status: :unprocessable_entity)
260
+ end
261
+
262
+ def innodb_health
263
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
264
+
265
+ result = MysqlGenius::Core::Ai::InnodbInterpreter.new(ai_client, ai_config_for_core, rails_connection).call
266
+ render(json: result)
267
+ rescue StandardError => e
268
+ render(json: { error: "InnoDB health analysis failed: #{e.message}" }, status: :unprocessable_entity)
269
+ end
270
+
271
+ def index_planner
272
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
273
+
274
+ tables = params[:tables].present? ? Array(params[:tables]) : nil
275
+ result = MysqlGenius::Core::Ai::IndexPlanner.new(ai_client, ai_config_for_core, rails_connection).call(tables)
276
+ render(json: result)
277
+ rescue StandardError => e
278
+ render(json: { error: "Index planner failed: #{e.message}" }, status: :unprocessable_entity)
279
+ end
280
+
281
+ def pattern_grouper
282
+ return ai_not_configured unless mysql_genius_config.ai_enabled?
283
+
284
+ result = MysqlGenius::Core::Ai::PatternGrouper.new(rails_connection, ai_client, ai_config_for_core).call
285
+ render(json: result)
286
+ rescue StandardError => e
287
+ render(json: { error: "Pattern grouper failed: #{e.message}" }, status: :unprocessable_entity)
288
+ end
289
+
235
290
  private
236
291
 
237
292
  RAILS_DOMAIN_CONTEXT = <<~CTX
data/config/routes.rb CHANGED
@@ -25,4 +25,10 @@ MysqlGenius::Engine.routes.draw do
25
25
  post "anomaly_detection", to: "queries#anomaly_detection"
26
26
  post "root_cause", to: "queries#root_cause"
27
27
  post "migration_risk", to: "queries#migration_risk"
28
+ post "variable_review", to: "queries#variable_review"
29
+ post "connection_advisor", to: "queries#connection_advisor"
30
+ post "workload_digest", to: "queries#workload_digest"
31
+ post "innodb_health", to: "queries#innodb_health"
32
+ post "index_planner", to: "queries#index_planner"
33
+ post "pattern_grouper", to: "queries#pattern_grouper"
28
34
  end
@@ -0,0 +1,115 @@
1
+ # AI Features Guide
2
+
3
+ MysqlGenius integrates with OpenAI-compatible LLM APIs to provide AI-powered database analysis. All AI features are optional — the dashboard works fully without them.
4
+
5
+ ## Supported providers
6
+
7
+ | Provider | Endpoint | Auth Style |
8
+ |---|---|---|
9
+ | OpenAI | `https://api.openai.com/v1/chat/completions` | Bearer |
10
+ | Anthropic | `https://api.anthropic.com/v1/messages` | x-api-key |
11
+ | Google Gemini | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | Bearer |
12
+ | Azure OpenAI | `https://YOUR-RESOURCE.openai.azure.com/...` | api-key |
13
+ | DeepSeek | `https://api.deepseek.com/chat/completions` | Bearer |
14
+ | Groq | `https://api.groq.com/openai/v1/chat/completions` | Bearer |
15
+ | Ollama (local) | `http://localhost:11434/v1/chat/completions` | Bearer |
16
+ | OpenRouter | `https://openrouter.ai/api/v1/chat/completions` | Bearer |
17
+ | Perplexity | `https://api.perplexity.ai/chat/completions` | Bearer |
18
+ | Any OpenAI-compatible | Your endpoint URL | Bearer or api-key |
19
+
20
+ ## Configuration
21
+
22
+ In your Rails initializer (`config/initializers/mysql_genius.rb`):
23
+
24
+ ```ruby
25
+ MysqlGenius.configure do |config|
26
+ config.ai_endpoint = "https://api.openai.com/v1/chat/completions"
27
+ config.ai_api_key = ENV["OPENAI_API_KEY"]
28
+ config.ai_model = "gpt-4o-mini"
29
+ config.ai_auth_style = :bearer # or :api_key for Azure, :x_api_key for Anthropic
30
+ end
31
+ ```
32
+
33
+ ## Available features
34
+
35
+ ### Query Explorer AI
36
+
37
+ | Feature | What it does | Where |
38
+ |---|---|---|
39
+ | **AI Suggest** | Generate SQL from natural language | Query Explorer tab |
40
+ | **AI Optimization** | Analyze EXPLAIN output, suggest improvements | After running EXPLAIN |
41
+ | **Index Advisor** | Recommend indexes for a specific query | After running EXPLAIN |
42
+ | **Describe Query** | Explain what a SQL query does in plain English | Query Explorer tab |
43
+ | **Rewrite Query** | Suggest an optimized version of the SQL | Query Explorer tab |
44
+
45
+ ### Schema & Migration
46
+
47
+ | Feature | What it does | Where |
48
+ |---|---|---|
49
+ | **Schema Review** | Find anti-patterns across your schema | AI Tools tab |
50
+ | **Migration Risk** | Assess safety of a DDL/migration before deploying | AI Tools tab |
51
+ | **AI Optimize** | Review a specific table's schema (appears on fragmented tables) | Tables tab |
52
+
53
+ ### Server Analysis
54
+
55
+ | Feature | What it does | Where |
56
+ |---|---|---|
57
+ | **Variable Config Review** | Review my.cnf settings against your workload | Server tab |
58
+ | **Connection Advisor** | Diagnose connection pool issues | Server tab |
59
+ | **InnoDB Health** | Interpret SHOW ENGINE INNODB STATUS | Server tab |
60
+
61
+ ### Workload Analysis
62
+
63
+ | Feature | What it does | Where |
64
+ |---|---|---|
65
+ | **Workload Digest** | Executive summary of your query workload | Query Stats tab |
66
+ | **Pattern Grouper** | Group slow queries by shared root cause | Query Stats tab |
67
+ | **Index Planner** | Holistic index optimization plan | Indexes tabs |
68
+
69
+ ## Settings
70
+
71
+ ### Max Tokens
72
+
73
+ Controls the maximum response length from the LLM. Default: 4096. Adjust with the slider in AI Configuration.
74
+
75
+ - **Lower (256-1024)** — faster responses, may truncate complex analyses
76
+ - **Higher (4096-16384)** — complete analyses, slower and more expensive
77
+
78
+ ### System Prompt
79
+
80
+ Optional context injected into every AI request. Use it to describe your application:
81
+
82
+ > "This is an e-commerce platform with 50M orders. The database handles 5000 QPS during peak hours."
83
+
84
+ ### Domain Prompt
85
+
86
+ Optional instructions for the AI's recommendations:
87
+
88
+ > "Prefer window functions over correlated subqueries. Don't recommend foreign keys — we handle referential integrity in the application layer."
89
+
90
+ ## Using with Ollama (free, local)
91
+
92
+ 1. Install Ollama: `brew install ollama`
93
+ 2. Pull a model: `ollama pull llama3.2`
94
+ 3. Start Ollama: `ollama serve`
95
+ 4. In MysqlGenius, select **Ollama (local)** as the provider
96
+ 5. The endpoint and model auto-fill — just click **Save**
97
+
98
+ No API key needed. All data stays on your machine.
99
+
100
+ ## Copying AI responses
101
+
102
+ Every AI response has a **Copy response** button at the bottom right. Click it to copy the plain text to your clipboard — useful for sharing with team members or pasting into tickets.
103
+
104
+ ## Cost considerations
105
+
106
+ Each AI feature makes one API call per invocation. Typical costs with OpenAI gpt-4o-mini:
107
+
108
+ | Feature | ~Input tokens | ~Output tokens | ~Cost |
109
+ |---|---|---|---|
110
+ | Schema Review (all tables) | 2000-5000 | 500-2000 | $0.001-0.005 |
111
+ | Migration Risk | 500-1000 | 500-1000 | $0.001 |
112
+ | Query Optimization | 1000-2000 | 500-1000 | $0.001 |
113
+ | Workload Digest | 3000-5000 | 1000-2000 | $0.003 |
114
+
115
+ Using Ollama or other local models eliminates API costs entirely.
@@ -0,0 +1,118 @@
1
+ # Getting Started with MysqlGenius (Rails)
2
+
3
+ ## Installation
4
+
5
+ Add to your Gemfile:
6
+
7
+ ```ruby
8
+ gem "mysql_genius"
9
+ ```
10
+
11
+ ```bash
12
+ bundle install
13
+ ```
14
+
15
+ ## Mount the engine
16
+
17
+ In `config/routes.rb`:
18
+
19
+ ```ruby
20
+ Rails.application.routes.draw do
21
+ mount MysqlGenius::Engine, at: "/mysql_genius"
22
+ # ... your other routes
23
+ end
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ Create an initializer at `config/initializers/mysql_genius.rb`:
29
+
30
+ ```ruby
31
+ MysqlGenius.configure do |config|
32
+ # Authentication (required in production)
33
+ config.authenticate = ->(controller) {
34
+ # Example: restrict to admin users
35
+ controller.current_user&.admin?
36
+ }
37
+
38
+ # Tables to hide from the dashboard
39
+ config.blocked_tables = %w[
40
+ schema_migrations
41
+ ar_internal_metadata
42
+ ]
43
+
44
+ # Columns to mask in query results
45
+ config.masked_column_patterns = %w[
46
+ password
47
+ token
48
+ secret
49
+ ssn
50
+ ]
51
+
52
+ # Query limits
53
+ config.default_row_limit = 100
54
+ config.max_row_limit = 10_000
55
+ config.query_timeout_ms = 10_000
56
+
57
+ # AI features (optional)
58
+ config.ai_endpoint = ENV["MYSQL_GENIUS_AI_ENDPOINT"]
59
+ config.ai_api_key = ENV["MYSQL_GENIUS_AI_KEY"]
60
+ config.ai_model = "gpt-4o-mini"
61
+ config.ai_auth_style = :bearer
62
+
63
+ # Stats collection (background thread, default: true)
64
+ config.stats_collection = true
65
+
66
+ # Slow query monitoring via Redis (optional)
67
+ # config.redis_url = ENV["REDIS_URL"]
68
+ end
69
+ ```
70
+
71
+ ## Visit the dashboard
72
+
73
+ Start your Rails server and navigate to:
74
+
75
+ ```
76
+ http://localhost:3000/mysql_genius
77
+ ```
78
+
79
+ ## Features
80
+
81
+ ### Dashboard
82
+ Overview of server health, top slow queries, expensive queries, duplicate/unused index counts.
83
+
84
+ ### Query Explorer
85
+ Run SELECT queries against your database with syntax highlighting, EXPLAIN output, and AI-powered suggestions.
86
+
87
+ ### Query Stats
88
+ Top queries from `performance_schema.events_statements_summary_by_digest`. Click any query to see a detail page with time-series performance charts.
89
+
90
+ ### Server
91
+ Server status, connections, InnoDB buffer pool, query activity.
92
+
93
+ ### Tables
94
+ Table sizes with fragmentation detection. Tables needing optimization get an "AI Optimize" button.
95
+
96
+ ### Indexes
97
+ Duplicate and unused index detection.
98
+
99
+ ### AI Tools (when configured)
100
+ - **Schema Review** — find anti-patterns in your schema
101
+ - **Migration Risk** — assess DDL safety before deploying
102
+ - **Query Description** — explain what a query does in plain English
103
+ - **Query Rewrite** — suggest optimized versions of slow queries
104
+ - **Index Advisor** — recommend indexes for specific queries
105
+
106
+ ## Security
107
+
108
+ **Always configure authentication in production.** Without it, anyone who can reach the mounted path can view your database schema and run SELECT queries.
109
+
110
+ The `blocked_tables` and `masked_column_patterns` settings provide defense-in-depth but are not a substitute for authentication.
111
+
112
+ ## Supported databases
113
+
114
+ - MySQL 5.7+
115
+ - MySQL 8.0+
116
+ - MariaDB 10.3+
117
+
118
+ Some features (query stats, unused indexes) require `performance_schema` to be enabled.
@@ -0,0 +1,151 @@
1
+ # Connecting to MySQL Through an SSH Tunnel
2
+
3
+ If your MySQL server is behind a firewall, on a private network, or only accessible via a bastion/jump host, you can use an SSH tunnel to connect MysqlGenius to it.
4
+
5
+ ## How it works
6
+
7
+ An SSH tunnel forwards a local port on your machine to the MySQL port on the remote server. MysqlGenius connects to `localhost:<local_port>` and the tunnel transparently routes traffic to the actual database server.
8
+
9
+ ```
10
+ Your Machine (MysqlGenius) → SSH Tunnel → Bastion Host → MySQL Server
11
+ localhost:3307 db.internal:3306
12
+ ```
13
+
14
+ ## Step 1: Open the SSH tunnel
15
+
16
+ In a terminal, run:
17
+
18
+ ```bash
19
+ ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N
20
+ ```
21
+
22
+ **Flags explained:**
23
+ - `-L 3307:db.internal:3306` — forward local port `3307` to `db.internal:3306` through the tunnel
24
+ - `user@bastion-host.example.com` — your SSH login on the bastion/jump host
25
+ - `-N` — don't open a shell, just forward the port
26
+
27
+ **With an SSH key:**
28
+
29
+ ```bash
30
+ ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N -i ~/.ssh/my_key
31
+ ```
32
+
33
+ **Keep it running in the background:**
34
+
35
+ ```bash
36
+ ssh -L 3307:db.internal:3306 user@bastion-host.example.com -N -f
37
+ ```
38
+
39
+ The `-f` flag sends SSH to the background after connecting.
40
+
41
+ ## Step 2: Configure your Rails app
42
+
43
+ MysqlGenius uses your app's `ActiveRecord::Base.connection`, which reads from `database.yml`. Point it at the tunnel:
44
+
45
+ ```yaml
46
+ # config/database.yml
47
+ production:
48
+ adapter: mysql2
49
+ host: 127.0.0.1
50
+ port: 3307
51
+ username: readonly
52
+ password: <%= ENV["DB_PASSWORD"] %>
53
+ database: app_production
54
+ ```
55
+
56
+ No special MysqlGenius configuration needed — it automatically uses the same connection as your Rails app.
57
+
58
+ ## Step 3: Verify the connection
59
+
60
+ Start your Rails server and visit `/mysql_genius`. If the tunnel is running, the dashboard loads normally. If not, you'll see a connection error.
61
+
62
+ ## Common issues
63
+
64
+ ### "Connection refused"
65
+
66
+ The SSH tunnel is not running. Start it first:
67
+
68
+ ```bash
69
+ ssh -L 3307:db.internal:3306 user@bastion -N
70
+ ```
71
+
72
+ ### "Access denied"
73
+
74
+ The tunnel is working but the MySQL credentials are wrong. Verify your username/password can connect to the database directly from the bastion host.
75
+
76
+ ### "Lost connection to MySQL server during query"
77
+
78
+ The SSH tunnel dropped. This happens if the tunnel is idle for too long. Add keep-alive settings:
79
+
80
+ ```bash
81
+ ssh -L 3307:db.internal:3306 user@bastion -N -o ServerAliveInterval=60 -o ServerAliveCountMax=3
82
+ ```
83
+
84
+ ### Port already in use
85
+
86
+ Another process is using port 3307. Pick a different local port:
87
+
88
+ ```bash
89
+ ssh -L 3308:db.internal:3306 user@bastion -N
90
+ ```
91
+
92
+ Then update your MysqlGenius config to use port `3308`.
93
+
94
+ ## Multiple databases through one bastion
95
+
96
+ You can tunnel to multiple MySQL servers through the same bastion:
97
+
98
+ ```bash
99
+ # Production on local port 3307
100
+ ssh -L 3307:db-prod.internal:3306 user@bastion -N -f
101
+
102
+ # Staging on local port 3308
103
+ ssh -L 3308:db-staging.internal:3306 user@bastion -N -f
104
+ ```
105
+
106
+ Then create separate MysqlGenius profiles for each:
107
+ - **Production**: `127.0.0.1:3307`
108
+ - **Staging**: `127.0.0.1:3308`
109
+
110
+ ## Automating the tunnel
111
+
112
+ ### macOS: Launch Agent
113
+
114
+ Create `~/Library/LaunchAgents/com.mysqlgenius.tunnel.plist`:
115
+
116
+ ```xml
117
+ <?xml version="1.0" encoding="UTF-8"?>
118
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
119
+ <plist version="1.0">
120
+ <dict>
121
+ <key>Label</key>
122
+ <string>com.mysqlgenius.tunnel</string>
123
+ <key>ProgramArguments</key>
124
+ <array>
125
+ <string>ssh</string>
126
+ <string>-L</string>
127
+ <string>3307:db.internal:3306</string>
128
+ <string>user@bastion</string>
129
+ <string>-N</string>
130
+ <string>-o</string>
131
+ <string>ServerAliveInterval=60</string>
132
+ </array>
133
+ <key>KeepAlive</key>
134
+ <true/>
135
+ <key>RunAtLoad</key>
136
+ <true/>
137
+ </dict>
138
+ </plist>
139
+ ```
140
+
141
+ Load it:
142
+
143
+ ```bash
144
+ launchctl load ~/Library/LaunchAgents/com.mysqlgenius.tunnel.plist
145
+ ```
146
+
147
+ The tunnel will start automatically on login and restart if it drops.
148
+
149
+ ## Future: Built-in SSH tunnel support
150
+
151
+ Built-in SSH tunnel support is planned for a future release. When available, you'll be able to configure the SSH connection directly in the MysqlGenius profile form without needing a separate terminal.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlGenius
4
- VERSION = "0.7.2"
4
+ VERSION = "0.8.0"
5
5
  end
data/mysql_genius.gemspec CHANGED
@@ -30,6 +30,6 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  spec.add_dependency("activerecord", ">= 6.0", "< 9")
33
- spec.add_dependency("mysql_genius-core", "~> 0.7.2")
33
+ spec.add_dependency("mysql_genius-core", "~> 0.8.0")
34
34
  spec.add_dependency("railties", ">= 6.0", "< 9")
35
35
  end
data/ralph/prd.json CHANGED
@@ -1,149 +1,117 @@
1
1
  {
2
2
  "project": "MysqlGenius",
3
- "branchName": "ralph/sqlite-migration",
4
- "description": "Replace in-memory StatsHistory and YAML ProfileManager with SQLite for persistent stats and clean profile CRUD in the desktop sidecar",
3
+ "branchName": "ralph/ssh-tunnel-support",
4
+ "description": "Add built-in SSH tunnel support to the desktop sidecar so users can connect to MySQL databases behind firewalls without manually running ssh -L",
5
5
  "userStories": [
6
6
  {
7
7
  "id": "US-001",
8
- "title": "Add sqlite3 dependency and create Database class with schema",
9
- "description": "As a developer, I need a Database class that manages a SQLite file with profiles, settings, and stats_snapshots tables.",
8
+ "title": "Add net-ssh dependency and SshTunnel class",
9
+ "description": "As a developer, I need a class that opens an SSH tunnel forwarding a local port to a remote MySQL server.",
10
10
  "acceptanceCriteria": [
11
- "Add sqlite3 ~> 2.0 to gems/mysql_genius-desktop/mysql_genius-desktop.gemspec as runtime dependency",
11
+ "Add net-ssh ~> 7.0 to gems/mysql_genius-desktop/mysql_genius-desktop.gemspec as runtime dependency",
12
12
  "Run bundle install in gems/mysql_genius-desktop",
13
- "Create gems/mysql_genius-desktop/lib/mysql_genius/desktop/database.rb",
14
- "Database.new(path) creates the SQLite file if missing and runs CREATE TABLE IF NOT EXISTS for all 3 tables",
15
- "profiles table: name TEXT PRIMARY KEY, host TEXT NOT NULL, port INTEGER DEFAULT 3306, username TEXT NOT NULL, password TEXT DEFAULT '', database_name TEXT NOT NULL, tls_mode TEXT DEFAULT 'preferred', created_at TEXT, updated_at TEXT",
16
- "settings table: key TEXT PRIMARY KEY, value TEXT",
17
- "stats_snapshots table: id INTEGER PRIMARY KEY AUTOINCREMENT, digest_text TEXT NOT NULL, timestamp TEXT NOT NULL, delta_calls INTEGER DEFAULT 0, delta_total_time_ms REAL DEFAULT 0, delta_avg_time_ms REAL DEFAULT 0",
18
- "Index on stats_snapshots(digest_text, timestamp)",
19
- "Profile CRUD: list_profiles returns array of hashes, find_profile(name) returns hash or nil, add_profile(attrs) inserts (raises on duplicate), update_profile(name, attrs) updates (raises if not found), delete_profile(name) deletes (raises if not found)",
20
- "Settings: get_setting(key) returns string or nil, set_setting(key, value) upserts, get_ai_config returns hash of ai.* keys with prefix stripped, set_ai_config(hash) writes each key with ai. prefix",
21
- "Stats: record_snapshot(digest_text, snapshot_hash) inserts + prunes rows older than 24 hours, series_for(digest_text) returns ordered array last 24hr, digests returns distinct digest_texts, clear deletes all",
22
- "Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb with tests for all CRUD operations using tmpdir",
13
+ "Create gems/mysql_genius-desktop/lib/mysql_genius/desktop/ssh_tunnel.rb",
14
+ "SshTunnel.new(ssh_host:, ssh_port: 22, ssh_user:, ssh_key_path: nil, ssh_password: nil, remote_host:, remote_port: 3306, local_port: 0)",
15
+ "start method: opens SSH connection, sets up port forwarding, returns the allocated local port",
16
+ "stop method: closes the SSH connection and port forwarding",
17
+ "running? method: returns boolean",
18
+ "The tunnel runs in a background thread so it doesn't block the main thread",
19
+ "If ssh_key_path is provided, use key-based auth. If ssh_password, use password auth. If neither, use ssh-agent",
20
+ "Raises SshTunnel::ConnectionError with a clear message on failure",
21
+ "Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/ssh_tunnel_spec.rb",
23
22
  "Desktop gem suite passes",
24
23
  "Typecheck passes"
25
24
  ],
26
25
  "priority": 1,
27
26
  "passes": true,
28
- "notes": "Use raw sqlite3 gem calls, no ORM. Thread safety: SQLite WAL mode handles concurrent reads. Error classes: Database::DuplicateProfileError, Database::ProfileNotFoundError. The spec should use Dir.mktmpdir for isolated DB files per test."
27
+ "notes": "Use Net::SSH.start for the connection and Net::SSH::Gateway or manual forwarding via Net::SSH::Service::Forward. For local_port: 0, bind to an ephemeral port and read the assigned port from the socket. The background thread should keep the SSH connection alive with keepalive packets. Spec can mock Net::SSH.start since we can't open real SSH connections in CI."
29
28
  },
30
29
  {
31
30
  "id": "US-002",
32
- "title": "Create SqliteStatsHistory as drop-in replacement for StatsHistory",
33
- "description": "As a developer, I need a SQLite-backed stats history with the same API as the in-memory StatsHistory so the StatsCollector works unchanged.",
31
+ "title": "Add SSH fields to the Database profiles schema",
32
+ "description": "As a developer, I need the SQLite profiles table to store SSH tunnel configuration.",
34
33
  "acceptanceCriteria": [
35
- "Create gems/mysql_genius-desktop/lib/mysql_genius/desktop/sqlite_stats_history.rb",
36
- "SqliteStatsHistory.new(database) takes a Database instance",
37
- "record(digest_text, snapshot_hash) delegates to database.record_snapshot",
38
- "series_for(digest_text) delegates to database.series_for, returns array of hashes with symbol keys (timestamp:, calls:, total_time_ms:, avg_time_ms:)",
39
- "digests delegates to database.digests",
40
- "clear delegates to database.clear",
41
- "Same public API as MysqlGenius::Core::Analysis::StatsHistory (record, series_for, digests, clear)",
42
- "Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/sqlite_stats_history_spec.rb",
34
+ "Add columns to the profiles table in Database class: ssh_enabled BOOLEAN DEFAULT 0, ssh_host TEXT, ssh_port INTEGER DEFAULT 22, ssh_user TEXT, ssh_key_path TEXT, ssh_password TEXT, remote_host TEXT, remote_port INTEGER DEFAULT 3306",
35
+ "The schema migration must be backward-compatible: use ALTER TABLE ADD COLUMN IF NOT EXISTS (or check column existence before adding)",
36
+ "Database#add_profile and #update_profile accept the new SSH fields",
37
+ "Database#find_profile and #list_profiles return the new fields",
38
+ "Update database_spec.rb to test SSH fields round-trip",
43
39
  "Desktop gem suite passes",
44
40
  "Typecheck passes"
45
41
  ],
46
42
  "priority": 2,
47
43
  "passes": true,
48
- "notes": "The StatsCollector passes snapshot hashes with string keys from its tick method. SqliteStatsHistory should accept both string and symbol keys. The series_for return format must match what the query_detail template JS expects: [{timestamp:, calls:, total_time_ms:, avg_time_ms:}]."
44
+ "notes": "SQLite doesn't support ADD COLUMN IF NOT EXISTS directly. Use a rescue on the ALTER TABLE or check pragma_table_info for column existence before adding. The ssh_password field stores the SSH password (not the MySQL password). For Tauri app, the SSH password could later move to Keychain — for now store in SQLite alongside the MySQL password."
49
45
  },
50
46
  {
51
47
  "id": "US-003",
52
- "title": "Add first-boot YAML import logic to Launcher",
53
- "description": "As a user, I want my existing YAML profiles and AI config automatically imported into SQLite on first boot.",
48
+ "title": "Wire SSH tunnel into ActiveSession connection flow",
49
+ "description": "As a user, I want the sidecar to automatically open an SSH tunnel before connecting to MySQL when SSH is enabled on a profile.",
54
50
  "acceptanceCriteria": [
55
- "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb",
56
- "After Config.load, create Database.new at ~/.config/mysql_genius/mysql_genius.db (create dir if needed)",
57
- "If database.list_profiles is empty AND config.profiles is not empty, import each profile into SQLite",
58
- "If database.get_setting('ai.endpoint') is nil AND config.ai.endpoint is present, import AI config into SQLite",
59
- "Set App.set(:database, db)",
60
- "Create SqliteStatsHistory.new(db) and pass to StatsCollector instead of StatsHistory.new",
61
- "Add require statements for database and sqlite_stats_history to desktop.rb",
62
- "Existing desktop specs still pass (rack_helper needs updating to inject a test Database)",
51
+ "Modify ActiveSession to check if the profile has SSH enabled",
52
+ "If ssh_enabled: open SshTunnel first, get the local forwarded port, then connect Trilogy to 127.0.0.1:local_port instead of the profile's host:port",
53
+ "ActiveSession stores a reference to the tunnel and closes it on session.close",
54
+ "On connection retry (Trilogy::ConnectionResetError), also restart the SSH tunnel",
55
+ "The Launcher reads SSH fields from the Database and passes them through the Config/profile to ActiveSession",
56
+ "If SSH connection fails, raise ConnectError with a clear message including the SSH host",
63
57
  "Desktop gem suite passes",
64
58
  "Typecheck passes"
65
59
  ],
66
60
  "priority": 3,
67
61
  "passes": true,
68
- "notes": "The rack_helper.rb needs to create a tmpdir-based Database and set it on the App via App.set(:database, db). Ensure mkdir_p for ~/.config/mysql_genius/ before creating the DB. The import only runs when the DB is empty deleting mysql_genius.db forces re-import on next boot."
62
+ "notes": "The flow is: Launcher reads profile from DB -> if ssh_enabled, ActiveSession opens SshTunnel(ssh_host, ..., remote_host=profile.host, remote_port=profile.port) -> tunnel returns local_port -> Trilogy connects to 127.0.0.1:local_port. The profile's host/port become the REMOTE host/port (what the tunnel forwards to), not the direct Trilogy target. This is the key architectural insight."
69
63
  },
70
64
  {
71
65
  "id": "US-004",
72
- "title": "Rewire profile API routes to use Database",
73
- "description": "As a user, I want the /connections page to read and write profiles from SQLite instead of YAML.",
66
+ "title": "Add SSH fields to the connections page UI",
67
+ "description": "As a user, I want to configure SSH tunnel settings when adding or editing a profile.",
74
68
  "acceptanceCriteria": [
75
- "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb",
76
- "GET /api/profiles reads from settings.database.list_profiles + settings.current_profile_name",
77
- "POST /api/profiles calls settings.database.add_profile, returns updated list",
78
- "PUT /api/profiles/:name calls settings.database.update_profile, returns updated list",
79
- "DELETE /api/profiles/:name calls settings.database.delete_profile (rejects active profile), returns updated list",
80
- "POST /api/test_connection unchanged (no storage involved)",
81
- "POST /api/profiles/:name/connect reads profile from settings.database.find_profile",
82
- "GET /api/ai_config reads from settings.database.get_ai_config",
83
- "PUT /api/ai_config writes to settings.database.set_ai_config + reloads into running app",
84
- "Remove require for profile_manager from app.rb",
85
- "Update profiles_api_spec.rb to use Database instead of YAML tmpdir",
69
+ "Add an 'Enable SSH Tunnel' checkbox to the profile form on /connections",
70
+ "When checked, show additional fields: SSH Host, SSH Port (default 22), SSH User, SSH Key Path, SSH Password",
71
+ "SSH Key Path field has a placeholder: ~/.ssh/id_rsa",
72
+ "SSH Password field uses type=password with the existing eye toggle",
73
+ "When unchecked, hide the SSH fields",
74
+ "The form sends ssh_enabled, ssh_host, ssh_port, ssh_user, ssh_key_path, ssh_password in the profile POST/PUT request",
75
+ "The existing Test Connection button tests through the tunnel when SSH is enabled",
76
+ "Edit mode populates the SSH fields from the saved profile",
77
+ "Dark mode works correctly on all new fields",
86
78
  "Desktop gem suite passes",
87
79
  "Typecheck passes"
88
80
  ],
89
81
  "priority": 4,
90
82
  "passes": true,
91
- "notes": "Error mapping: Database::DuplicateProfileError -> 409, Database::ProfileNotFoundError -> 404. The active-profile-delete check stays in the route handler (check settings.current_profile_name before calling delete). The connect route builds a MysqlConfig from the DB hash: Config::MysqlConfig.from_hash(profile_hash)."
83
+ "notes": "The checkbox should use a simple <input type='checkbox' id='mg-f-ssh-enabled'>. When it changes, toggle visibility of a div containing the SSH fields. The Test Connection flow: if SSH enabled, first open tunnel (POST /api/test_ssh_tunnel), get local port, then test MySQL connection against 127.0.0.1:local_port. Or simpler: the existing /api/test_connection route handles it server-side (check ssh_enabled in the payload, open tunnel internally, test MySQL, close tunnel)."
92
84
  },
93
85
  {
94
86
  "id": "US-005",
95
- "title": "Rewire SessionSwapper to use Database",
96
- "description": "As a developer, I need SessionSwapper to look up profiles from SQLite instead of Config.",
87
+ "title": "Update profile switching to handle SSH tunnels",
88
+ "description": "As a user, I want SSH tunnels to be properly managed when switching between profiles.",
97
89
  "acceptanceCriteria": [
98
- "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb",
99
- "Change initialize to accept (app_class, config, database) instead of (app_class, config)",
100
- "switch_to looks up profile via @database.find_profile(name) instead of @config.profile_by_name(name)",
101
- "Build MysqlConfig from the DB hash via Config::MysqlConfig.from_hash(profile)",
102
- "Update all callers of SessionSwapper.new to pass the database",
103
- "Update session_swapper_spec.rb",
90
+ "SessionSwapper closes the old SSH tunnel (if any) when switching profiles",
91
+ "SessionSwapper opens a new SSH tunnel if the new profile has SSH enabled",
92
+ "The Launcher opens the initial SSH tunnel on boot if the default profile has SSH enabled",
93
+ "On sidecar shutdown (at_exit), both the MySQL session and SSH tunnel are closed",
104
94
  "Desktop gem suite passes",
105
95
  "Typecheck passes"
106
96
  ],
107
97
  "priority": 5,
108
98
  "passes": true,
109
- "notes": "The profile hash from Database has string keys (host, port, etc). MysqlConfig.from_hash handles both string and symbol keys via transform_keys. The build_switch_config method still creates a Config.allocate with profiles array for ActiveSession compatibility."
99
+ "notes": "ActiveSession already has a close method that the SessionSwapper calls. If ActiveSession holds a reference to the tunnel, session.close should also close the tunnel. The new profile's tunnel opens as part of ActiveSession.new in the swapper."
110
100
  },
111
101
  {
112
102
  "id": "US-006",
113
- "title": "Delete ProfileManager and clean up",
114
- "description": "As a developer, I want to remove the dead ProfileManager code now that Database handles everything.",
103
+ "title": "Full green sweep",
104
+ "description": "As a developer, I need all test suites passing.",
115
105
  "acceptanceCriteria": [
116
- "Delete gems/mysql_genius-desktop/lib/mysql_genius/desktop/profile_manager.rb",
117
- "Delete gems/mysql_genius-desktop/spec/mysql_genius/desktop/profile_manager_spec.rb",
118
- "Remove require 'mysql_genius/desktop/profile_manager' from desktop.rb",
119
- "Remove require 'mysql_genius/desktop/profile_manager' from app.rb if present",
120
- "Grep the entire desktop gem for 'ProfileManager' references and remove any remaining",
121
- "Desktop gem suite passes",
122
- "All 3 rubocops pass",
106
+ "Rails adapter rspec passes (94+ examples)",
107
+ "Core gem rspec passes (248+ examples)",
108
+ "Desktop gem rspec passes (202+ examples)",
109
+ "All rubocops clean",
123
110
  "Typecheck passes"
124
111
  ],
125
112
  "priority": 6,
126
113
  "passes": true,
127
- "notes": "Make sure no spec files reference ProfileManager. The Database class error classes (DuplicateProfileError, ProfileNotFoundError) replace ProfileManager's error classes — verify the route handlers rescue the correct class names."
128
- },
129
- {
130
- "id": "US-007",
131
- "title": "Full green sweep across all suites",
132
- "description": "As a developer, I need all test suites and linters passing before the PR.",
133
- "acceptanceCriteria": [
134
- "Rails adapter rspec passes (82+ examples)",
135
- "Core gem rspec passes (215+ examples)",
136
- "Desktop gem rspec passes (150+ examples)",
137
- "Rails adapter rubocop clean",
138
- "Core gem rubocop clean",
139
- "Desktop gem rubocop clean",
140
- "No changes to mysql_genius-core or mysql_genius Rails adapter source files",
141
- "publish.yml untouched",
142
- "Typecheck passes"
143
- ],
144
- "priority": 7,
145
- "passes": true,
146
- "notes": "Run all six commands. If any fail, fix before marking complete."
114
+ "notes": ""
147
115
  }
148
116
  ]
149
117
  }
data/ralph/progress.txt CHANGED
@@ -1,138 +1,119 @@
1
1
  # Ralph Progress Log
2
- Started: Sun Apr 12 23:47:10 CDT 2026
2
+ Started: Mon Apr 13 06:43:11 CDT 2026
3
3
  ---
4
4
 
5
5
  ## Codebase Patterns
6
- - Desktop gem specs use `Dir.mktmpdir` for isolated file-based test fixtures (DB files, YAML configs)
7
- - Desktop gem rubocop config inherits from root `.rubocop.yml` via `inherit_from`
8
- - Desktop gem Gemfile.lock is not tracked in git (gem convention)
9
- - Use `results_as_hash = true` on SQLite3::Database to get hash-style row access
10
- - Desktop app settings are injected via `App.set(:key, value)` (Sinatra pattern)
11
- - Launcher private methods can be stubbed via `allow(launcher).to(receive(:method_name))` in RSpec
12
- - First-boot import logic: only runs when DB is empty (list_profiles.empty? / get_setting returns nil)
13
- - rack_helper.rb must set `App.set(:database, ...)` for request specs that depend on DB settings
14
- - Database returns `database_name` key; `MysqlConfig.from_hash` expects `database` remap via `mysql_hash_from_profile`
15
- - Profile API response must be `{name:, mysql: {host:, port:, database:, ...}}` — use `format_profile` to reshape DB rows
6
+ - Desktop gem specs use RSpec mocking (allow/receive), NOT Mocha - Mocha is only for the Rails adapter
7
+ - Desktop gem uses `instance_double` for test doubles (VerifiedDoubles cop is disabled)
8
+ - Config objects are built in tests via `Config.allocate` + `instance_variable_set` pattern
9
+ - RSpec/MultipleMemoizedHelpers max is 5 - inline simple constants to stay under the limit
10
+ - Net::SSH is mocked at the class level: `allow(Net::SSH).to(receive(:start).and_return(ssh_session))`
11
+ - SshTunnel event loop runs in a background Thread - test thread existence, not `session.loop` (race condition)
12
+ - SQLite migration: use `PRAGMA table_info(table)` to check column existence before `ALTER TABLE ADD COLUMN`
13
+ - Database profiles use string keys throughout (from `results_as_hash = true`); `ssh_enabled` is INTEGER 0/1
14
+ - ActiveSession.open_adapter_for accepts `tunnel_port:` kwarg pass it from `session.tunnel_port` in conn_proc lambdas
15
+ - MysqlConfig#ssh_enabled? coerces INTEGER/boolean/string to boolean (handles DB and YAML sources)
16
+ - When SSH enabled, profile.host/port become SshTunnel remote_host/remote_port; Trilogy connects to 127.0.0.1:tunnel_port
17
+ - Instance doubles of ActiveSession must include `tunnel_port: nil` in specs that trigger conn_proc creation
18
+ - Launcher `import_profiles` must include all profile fields — keep in sync when adding new columns to Database schema
19
+ - Use `stub_boot(session:, db_name:)` helper in launcher_spec.rb for full boot mock setup
20
+ - Eye toggle: `.mg-eye-toggle` button with `data-target="input-id"` toggles password/text input type
21
+ - input[type="password"] must be included in both light-mode and dark-mode CSS selectors alongside text/number
22
+ - Layout/MultilineHashKeyLineBreaks cop: each key on its own line in multiline hashes (use rubocop -A to auto-fix)
23
+ ---
16
24
 
25
+ ## 2026-04-13 - US-001
26
+ - What was implemented: SshTunnel class with start/stop/running? methods, net-ssh dependency, 22 specs
27
+ - Files changed:
28
+ - gems/mysql_genius-desktop/mysql_genius-desktop.gemspec (added net-ssh ~> 7.0)
29
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb (added require for ssh_tunnel)
30
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/ssh_tunnel.rb (new - SshTunnel class)
31
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/ssh_tunnel_spec.rb (new - 22 examples)
32
+ - **Learnings for future iterations:**
33
+ - SshTunnel uses Net::SSH::Service::Forward for port forwarding, NOT Net::SSH::Gateway
34
+ - Ephemeral port allocation: bind TCPServer to port 0, read assigned port, close immediately
35
+ - Auth priority: ssh_key_path > ssh_password > ssh-agent (no auth options = agent fallback)
36
+ - The event loop thread runs `@session.loop(0.5) { @running }` - the 0.5 is the poll interval in seconds
37
+ - ConnectionError wraps all SSH failures with descriptive messages including host:port
17
38
  ---
18
39
 
19
- ## 2026-04-12 - US-001
20
- - What was implemented:
21
- - Added `sqlite3 ~> 2.0` runtime dependency to `mysql_genius-desktop.gemspec`
22
- - Created `Database` class at `lib/mysql_genius/desktop/database.rb` with:
23
- - SQLite WAL mode for concurrent reads
24
- - 3 tables: profiles, settings, stats_snapshots
25
- - Composite index on stats_snapshots(digest_text, timestamp)
26
- - Full profile CRUD: list_profiles, find_profile, add_profile, update_profile, delete_profile
27
- - Settings: get_setting, set_setting, get_ai_config, set_ai_config
28
- - Stats: record_snapshot (with 24hr pruning), series_for, digests, clear
29
- - Error classes: DuplicateProfileError, ProfileNotFoundError
30
- - Created comprehensive spec at `spec/mysql_genius/desktop/database_spec.rb` (29 examples)
40
+ ## 2026-04-13 - US-002
41
+ - What was implemented: Added SSH tunnel columns to the profiles SQLite table with backward-compatible migration
31
42
  - Files changed:
32
- - `gems/mysql_genius-desktop/mysql_genius-desktop.gemspec` (added sqlite3 dep)
33
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/database.rb` (new)
34
- - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb` (new)
43
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/database.rb (schema + migration + add_profile + update_profile)
44
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb (6 new SSH field specs + legacy migration test)
35
45
  - **Learnings for future iterations:**
36
- - ISO 8601 timestamps have second precision — `sleep(0.01)` is not enough to test timestamp changes, need `sleep(1.1)`
37
- - RSpec `let` is lazy must reference the variable in the test body before asserting on side effects like file creation
38
- - `Naming/AccessorMethodName` cop flags `get_`/`set_` prefixes disable inline when API names are mandated by the PRD
39
- - `add_profile` accepts both `database_name` and `database` keys for compatibility with existing config hash shapes
46
+ - SQLite doesn't support `ADD COLUMN IF NOT EXISTS` use `PRAGMA table_info(table)` to check column existence before ALTER TABLE
47
+ - The `migrate_ssh_columns` method runs after `create_schema` so it handles both fresh and legacy databases
48
+ - `ssh_enabled` is stored as INTEGER (0/1) since SQLite has no native boolean type use `.to_i` when writing
49
+ - `find_profile` and `list_profiles` use `SELECT *` so they automatically return new columns without code changes
50
+ - RSpec/ExampleLength max is 25 lines — extract DB setup into helper methods for longer integration tests
51
+ - SSH fields use string keys throughout (matching SQLite3 `results_as_hash = true` output)
40
52
  ---
41
53
 
42
54
  ## 2026-04-13 - US-003
43
- - What was implemented:
44
- - Modified `launcher.rb` to create Database at `~/.config/mysql_genius/mysql_genius.db`
45
- - Added first-boot YAML import: profiles imported when DB empty, AI config imported when endpoint missing
46
- - Replaced `StatsHistory.new` with `SqliteStatsHistory.new(db)` for persistent stats
47
- - Added `App.set(:database, db)` to wire Database into Sinatra settings
48
- - Added `database` and `sqlite_stats_history` requires to `desktop.rb`
49
- - Added `set :database, nil` to `app.rb` settings
50
- - Updated `rack_helper.rb` to create tmpdir-based test Database for request specs
51
- - Updated `launcher_spec.rb` to stub `open_database` and verify Database wiring
55
+ - What was implemented: Wired SSH tunnel into ActiveSession connection flow with tunnel lifecycle management
52
56
  - Files changed:
53
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb` (modified)
54
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (added database setting)
55
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb` (added requires)
56
- - `gems/mysql_genius-desktop/spec/rack_helper.rb` (inject test Database)
57
- - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb` (updated)
57
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/config/mysql_config.rb (added SSH fields + ssh_enabled? method)
58
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/active_session.rb (tunnel start/stop/restart, open_adapter_for tunnel_port kwarg)
59
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb (SSH fields in mysql_hash_from_profile, tunnel_port in conn_proc)
60
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb (tunnel_port in conn_proc)
61
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/active_session_spec.rb (7 new SSH tunnel specs)
62
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb (added tunnel_port to instance_double)
63
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb (added tunnel_port to instance_double)
64
+ - gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb (added tunnel_port to instance_double)
58
65
  - **Learnings for future iterations:**
59
- - `open_database` extracted as private method so specs can stub it without touching filesystem
60
- - Import logic checks DB emptiness, not config emptiness deleting mysql_genius.db forces re-import
61
- - Profile import maps `mysql.database` `database_name` for Database schema compatibility
62
- - rack_helper cleanup: must `FileUtils.remove_entry` tmpdir in after block to avoid leftover test DBs
66
+ - ActiveSession manages SSH tunnel lifecycle: start in initialize, stop in close, restart on retry
67
+ - open_adapter_for(config, tunnel_port: nil) when tunnel_port is set, connect to 127.0.0.1:tunnel_port instead of config host/port
68
+ - Profile host/port become SshTunnel remote_host/remote_port this is the key architectural pattern
69
+ - SshTunnel::ConnectionError is caught and re-raised as ConnectError with SSH host context
70
+ - Tunnel restart on Trilogy::ConnectionResetError creates a fresh SshTunnel object (stop old, start new)
71
+ - StatsCollector conn_proc captures tunnel_port via closure so it routes through the existing tunnel
72
+ - Any instance_double(ActiveSession) in specs that trigger conn_proc must stub tunnel_port
63
73
  ---
64
74
 
65
75
  ## 2026-04-13 - US-004
66
- - What was implemented:
67
- - Rewired all profile API routes (GET/POST/PUT/DELETE) in `app.rb` to use `Database` instead of `ProfileManager`
68
- - Added `GET /api/ai_config` and `PUT /api/ai_config` routes backed by Database settings
69
- - Removed `require 'mysql_genius/desktop/profile_manager'` from `app.rb`
70
- - Added `switch_to_config(name, mysql_config)` to `SessionSwapper` for pre-built MysqlConfig objects
71
- - Inlined `test_connection` logic in the route handler (was delegated to ProfileManager)
72
- - Added private helpers: `format_profile`, `profile_attrs_from_request`, `update_attrs_from_request`, `mysql_hash_from_profile`, `build_minimal_config`, `reload_ai_config_from_database`
73
- - Rewrote `profiles_api_spec.rb` to seed test data via `@test_database.add_profile` instead of YAML tmpdir
74
- - Added specs for `GET /api/ai_config`, `PUT /api/ai_config`, and `POST /api/profiles/:name/connect` with unknown profile
76
+ - What was implemented: Added SSH tunnel UI fields to the connections page profile form
75
77
  - Files changed:
76
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (rewired routes, removed ProfileManager require)
77
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb` (added switch_to_config method)
78
- - `gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb` (rewrote for Database)
78
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/connections.html.erb (SSH checkbox, conditional fields, eye toggle, JS updates)
79
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb (format_profile, profile_attrs_from_request, update_attrs_from_request, mysql_hash_from_profile, test_connection route with SSH tunnel support)
80
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/layout.html.erb (input[type="password"] in base + dark mode CSS)
81
+ - gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb (3 new specs: SSH create, SSH update, SSH test_connection)
79
82
  - **Learnings for future iterations:**
80
- - Database returns `database_name` key but `MysqlConfig.from_hash` expects `database` need `mysql_hash_from_profile` helper to remap
81
- - Profile API response format must be `{name:, mysql: {host:, port:, database:, ...}}` to match frontend expectations (connections.html.erb)
82
- - `SessionSwapper.switch_to_config` allows callers that already have a MysqlConfig to skip the config lookup
83
- - Active profile delete check stays in route handler, not in Database (Database.delete_profile has no concept of "active")
83
+ - Password field was type="text" changed to type="password" with eye toggle using data-target attribute
84
+ - Eye toggle pattern: button with class `mg-eye-toggle` and `data-target="input-id"`, toggles input.type between password/text
85
+ - SSH fields are conditionally shown via `toggleSshFields()` checkbox change listener toggles `.mg-hidden` on `#mg-ssh-fields` div
86
+ - `getFormData()` only includes SSH fields when checkbox is checked (sends ssh_enabled: 0 when unchecked)
87
+ - `fillForm()` must call `toggleSshFields()` after setting checkbox state to show/hide fields on edit
88
+ - test_connection route opens/closes SshTunnel inline when ssh_enabled — tunnel.stop in both success and rescue paths
89
+ - input[type="password"] was not in the original CSS selectors — must be added to both light and dark mode rules
90
+ - Layout/MultilineHashKeyLineBreaks cop requires each hash key on its own line in multiline hashes
84
91
  ---
85
92
 
86
93
  ## 2026-04-13 - US-005
87
- - What was implemented:
88
- - Changed `SessionSwapper.initialize` to accept `(app_class, config, database)` instead of `(app_class, config)`
89
- - Rewired `switch_to` to look up profiles via `@database.find_profile(name)` instead of `@config.profile_by_name(name)`
90
- - Added `mysql_hash_from_profile` private helper to remap `database_name` `database` for `MysqlConfig.from_hash`
91
- - Changed `switch_to_config` to create `SqliteStatsHistory.new(@database)` instead of in-memory `StatsHistory.new`
92
- - Updated the caller in `app.rb` connect route to pass `settings.database` as third arg
93
- - Rewrote `session_swapper_spec.rb` to use a tmpdir-based Database instead of Config profile lookup
94
+ - What was implemented: SSH tunnel lifecycle management during profile switching, boot, and shutdown
95
+ - Bug fix: `import_profiles` in Launcher was not importing SSH fields from YAML config to SQLite database
96
+ - Added SessionSwapper specs: SSH fields pass-through, old session/tunnel closure, tunnel_port capture for conn_proc
97
+ - Added Launcher specs: SSH field import, tunnel_port capture for stats collector, shutdown registration
98
+ - Extracted `stub_boot` helper in launcher_spec.rb to keep tests under RSpec/ExampleLength limit
94
99
  - Files changed:
95
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb` (rewired to use Database)
96
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (updated SessionSwapper.new call)
97
- - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb` (rewrote for Database)
100
+ - gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb (import_profiles now includes SSH fields)
101
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb (3 new SSH switching specs)
102
+ - gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb (4 new specs + stub_boot helper)
98
103
  - **Learnings for future iterations:**
99
- - `RSpec/MultipleMemoizedHelpers` max is 5 (subject excluded via AllowSubject). Inline tmpdir into `let(:database)` via `Dir.mktmpdir` to avoid extra helper
100
- - `mysql_hash_from_profile` is duplicated between `app.rb` and `session_swapper.rb` could be extracted to a shared module in US-006 cleanup if desired
101
- - `switch_to_config` is still used by the connect route in `app.rb` which pre-builds the MysqlConfig `switch_to` is for callers that only have a profile name
104
+ - Most SSH tunnel lifecycle was already implemented in US-003 via ActiveSession US-005 was about verifying integration points
105
+ - import_profiles must be kept in sync with any new profile fields it's easy to miss when adding columns
106
+ - Launcher `register_shutdown` uses at_exit test by capturing the arguments passed to it, not by invoking at_exit
107
+ - Use `stub_boot` helper in launcher_spec.rb for tests that need the full boot mock setup
108
+ - StatsCollector conn_proc captures tunnel_port via closure — test by capturing the proc from StatsCollector.new kwargs
102
109
  ---
103
110
 
104
111
  ## 2026-04-13 - US-006
105
- - What was implemented:
106
- - Deleted `profile_manager.rb` source file
107
- - Deleted `profile_manager_spec.rb` spec file
108
- - Removed `require "mysql_genius/desktop/profile_manager"` from `desktop.rb`
109
- - Verified no remaining `ProfileManager` or `profile_manager` references in the desktop gem
110
- - Confirmed route handlers already rescue `Database::DuplicateProfileError` / `Database::ProfileNotFoundError` (not ProfileManager versions)
111
- - Files changed:
112
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/profile_manager.rb` (deleted)
113
- - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/profile_manager_spec.rb` (deleted)
114
- - `gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb` (removed require line)
115
- - **Learnings for future iterations:**
116
- - US-004 already removed `profile_manager` require from `app.rb`, so only `desktop.rb` needed updating
117
- - All error classes were already migrated to `Database::` namespace in US-001/US-004 — cleanup was straightforward
118
- - 190 desktop specs pass, all 3 rubocops clean after removal
119
- ---
120
-
121
- ## 2026-04-13 - US-007
122
- - What was implemented:
123
- - Full green sweep: ran all 6 quality commands and verified all pass
124
- - Rails adapter rspec: 82 examples, 0 failures
125
- - Core gem rspec: 215 examples, 0 failures
126
- - Desktop gem rspec: 190 examples, 0 failures
127
- - Rails adapter rubocop: 152 files, no offenses
128
- - Core gem rubocop: 64 files, no offenses
129
- - Desktop gem rubocop: 48 files, no offenses
130
- - Verified no changes to mysql_genius-core or mysql_genius Rails adapter source files
131
- - Verified publish.yml untouched
132
- - Files changed:
133
- - `ralph/prd.json` (marked US-007 passes: true)
134
- - `ralph/progress.txt` (this entry)
135
- - **Learnings for future iterations:**
136
- - All 487 examples across 3 suites (82 + 215 + 190) pass — the SQLite migration touched only the desktop gem
137
- - The .DS_Store files appear as untracked — these should stay ignored (not committed)
112
+ - What was verified: Full green sweep across all three gems
113
+ - Results:
114
+ - Rails adapter: 94 examples, 0 failures
115
+ - Core gem: 248 examples, 0 failures
116
+ - Desktop gem: 246 examples, 0 failures
117
+ - All rubocops clean (166 + 76 + 50 files inspected, no offenses)
118
+ - No code changes required — all suites passed after US-005 commit
138
119
  ---
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.7.2
39
+ version: 0.8.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.7.2
46
+ version: 0.8.0
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: railties
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -96,6 +96,9 @@ files:
96
96
  - bin/console
97
97
  - bin/setup
98
98
  - config/routes.rb
99
+ - docs/guides/ai-features.md
100
+ - docs/guides/getting-started-rails.md
101
+ - docs/guides/ssh-tunnel-connections.md
99
102
  - docs/screenshots/ai_tools.png
100
103
  - docs/screenshots/dashboard.png
101
104
  - docs/screenshots/duplicate_indexes.png