mysql_genius 0.7.1 → 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: e6f79035cffd33253079d4365967dcca4ff97d1515e8e5960d23060b92052827
4
- data.tar.gz: f8c05c7ab9c6408d1816e36eec7c95cf0796f37b7fba0344699e806576b9e340
3
+ metadata.gz: 15b1fc129dbade000c7a7467601b927804f11f73892a0f450f92f6ffc1d4b313
4
+ data.tar.gz: d01c43dd9e9e06212c6fa85cad829fe940c982b1a1cf412ae02336de69bde376
5
5
  SHA512:
6
- metadata.gz: 5a797a52e89c12e8a266ec70b4f9264e4270450000630824789f64a19b53f4f22b5483bdcef634a97f904f9b61f7b533559f1dfa1fd37ee4ee9711f650f143db
7
- data.tar.gz: 9e46e3fafff0682dbeb29ffecc855f4d00d204efe8b15517ab2f05baaadf94da50836f12dfbd928f322a9df624d4f129442602f8f7074a043a8ce11896d5cd7a
6
+ metadata.gz: 3b9e69fee6f8b6c79e36bdccca6c838876f10fb79b333464496f37aa14db50d42b2507a4dfd18e0162c2b612af4279bf4ea731cd765da83068a23f3e303c340e
7
+ data.tar.gz: cf0730102bb3ce854f669dcf77343c3f5f0704996e1fa5bbe0c08c6a02f34110c59dfa53d9a303044f5c8ac6417f9e6f2863d3cc1d2535e84f108922b3379d0e
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ Gemfile.lock
15
15
  *.gem
16
16
  docs/superpowers/
17
17
  /ralph
18
+ .DS_Store
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
16
+ ## 0.7.2
17
+
18
+ ### Added
19
+ - **Anthropic Messages API support** — `x-api-key` auth style with `anthropic-version` header, top-level `system` parameter, `content[0].text` response parsing.
20
+ - **Configurable `max_tokens`** — new field on `Core::Ai::Config` (default 4096), sent to both OpenAI and Anthropic APIs.
21
+ - **Copy response button** on all AI result sections (schema review, migration risk, optimization, describe query, rewrite, index advisor, root cause, anomaly detection).
22
+ - **Dark mode contrast fixes** for AI result sections — proper CSS classes with dark-mode variants replace hardcoded light-mode inline styles.
23
+ - **`capability?(:standalone_header)`** guard hides the dashboard header when rendered inside a layout that already provides one.
24
+
3
25
  ## 0.7.1
4
26
 
5
27
  Lockstep version bump with `mysql_genius-core 0.7.1` which fixes missing ERB templates in the gem package.
@@ -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
@@ -145,6 +145,17 @@
145
145
  code { font-size: 12px; word-break: break-all; background: #f0f1f3; padding: 2px 6px; border-radius: 3px; color: #24292f; }
146
146
  pre.mg-pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
147
147
 
148
+ /* AI result sections */
149
+ .mg-ai-section { border-radius: 4px; border: 1px solid #dee2e6; border-left-width: 4px; margin-bottom: 12px; }
150
+ .mg-ai-section-header { padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-size: 13px; }
151
+ .mg-ai-section-body { padding: 12px; font-size: 13px; }
152
+ .mg-ai-danger { border-left-color: #dc3545; background: #fff5f5; }
153
+ .mg-ai-danger .mg-ai-section-header { border-bottom-color: #f5c6cb; }
154
+ .mg-ai-warning { border-left-color: #ffc107; background: #fffbeb; }
155
+ .mg-ai-warning .mg-ai-section-header { border-bottom-color: #ffeeba; }
156
+ .mg-ai-info { border-left-color: #17a2b8; background: #f0f9ff; }
157
+ .mg-ai-info .mg-ai-section-header { border-bottom-color: #bee5eb; }
158
+
148
159
  /* Theme toggle */
149
160
  .mg-theme-toggle { background: none; border: 1px solid #ced4da; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit; }
150
161
  .mg-theme-toggle:hover { background: #e9ecef; }
@@ -220,7 +231,13 @@
220
231
 
221
232
  /* Inline code */
222
233
  [data-theme="dark"] code { background: #21262d; color: #c9d1d9; }
234
+ [data-theme="dark"] pre { background: #161b22; color: #c9d1d9; border-color: #30363d; }
223
235
  [data-theme="dark"] pre.mg-pre { background: #161b22; color: #c9d1d9; }
236
+ [data-theme="dark"] .mg-card-body { color: #c9d1d9; }
237
+ [data-theme="dark"] .mg-card-body h1, [data-theme="dark"] .mg-card-body h2, [data-theme="dark"] .mg-card-body h3, [data-theme="dark"] .mg-card-body h4 { color: #c9d1d9; }
238
+ [data-theme="dark"] .mg-card-body p, [data-theme="dark"] .mg-card-body li, [data-theme="dark"] .mg-card-body td { color: #c9d1d9; }
239
+ [data-theme="dark"] .mg-card-body strong { color: #e6edf3; }
240
+ [data-theme="dark"] .mg-card-body a { color: #58a6ff; }
224
241
 
225
242
  /* Links */
226
243
  [data-theme="dark"] .mg-link { color: #58a6ff; }
@@ -234,6 +251,16 @@
234
251
  /* Checkboxes */
235
252
  [data-theme="dark"] .mg-check .type-hint { color: #484f58; }
236
253
 
254
+ /* AI result sections (dark) */
255
+ [data-theme="dark"] .mg-ai-section { border-color: #30363d; }
256
+ [data-theme="dark"] .mg-ai-danger { background: #2d0d0d; border-left-color: #f85149; }
257
+ [data-theme="dark"] .mg-ai-danger .mg-ai-section-header { border-bottom-color: #3d1414; color: #f85149; }
258
+ [data-theme="dark"] .mg-ai-warning { background: #2d2000; border-left-color: #d29922; }
259
+ [data-theme="dark"] .mg-ai-warning .mg-ai-section-header { border-bottom-color: #3d2e00; color: #d29922; }
260
+ [data-theme="dark"] .mg-ai-info { background: #0d2a3a; border-left-color: #58a6ff; }
261
+ [data-theme="dark"] .mg-ai-info .mg-ai-section-header { border-bottom-color: #0d3a5a; color: #58a6ff; }
262
+ [data-theme="dark"] .mg-ai-section-body { color: #c9d1d9; }
263
+
237
264
  /* Theme toggle (dark) */
238
265
  [data-theme="dark"] .mg-theme-toggle { border-color: #30363d; color: #c9d1d9; }
239
266
  [data-theme="dark"] .mg-theme-toggle:hover { background: #21262d; }
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.1"
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.0")
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,174 +1,117 @@
1
1
  {
2
2
  "project": "MysqlGenius",
3
- "branchName": "ralph/query-detail-page",
4
- "description": "Query Detail Page with Lightweight History - background stats collector, in-memory ring buffer, SVG time-series charts, and a dedicated query detail page accessible from the Query Stats tab",
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 StatsHistory ring buffer to core gem",
9
- "description": "As a developer, I need a thread-safe in-memory ring buffer to store per-digest query stats snapshots.",
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
- "Create gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_history.rb",
12
- "StatsHistory.new(max_samples: 1440) initializes empty buffer",
13
- "record(digest_text, {timestamp:, calls:, total_time_ms:, avg_time_ms:}) appends a snapshot",
14
- "series_for(digest_text) returns ordered array oldest-to-newest",
15
- "series_for returns empty array for unknown digests",
16
- "Ring buffer drops oldest entry when max_samples reached",
17
- "digests returns all known digest keys",
18
- "clear empties all data",
19
- "All operations are thread-safe via Mutex",
20
- "Add require to gems/mysql_genius-core/lib/mysql_genius/core.rb",
21
- "Create spec at gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_history_spec.rb with tests for all above",
22
- "Core gem suite passes: (cd gems/mysql_genius-core && bundle exec rspec)",
11
+ "Add net-ssh ~> 7.0 to gems/mysql_genius-desktop/mysql_genius-desktop.gemspec as runtime dependency",
12
+ "Run bundle install in gems/mysql_genius-desktop",
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",
22
+ "Desktop gem suite passes",
23
23
  "Typecheck passes"
24
24
  ],
25
25
  "priority": 1,
26
26
  "passes": true,
27
- "notes": "Thread safety: wrap all reads and writes in a single Mutex. The critical section is microseconds (array append/slice). Use a Hash keyed by digest_text, each value is an Array acting as ring buffer."
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."
28
28
  },
29
29
  {
30
30
  "id": "US-002",
31
- "title": "Add StatsCollector background sampler to core gem",
32
- "description": "As a developer, I need a background thread that periodically samples performance_schema and computes delta snapshots.",
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.",
33
33
  "acceptanceCriteria": [
34
- "Create gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_collector.rb",
35
- "initialize(connection_provider:, history:, interval: 60) accepts a callable for connection",
36
- "start spawns a background Thread and returns self",
37
- "stop signals the thread to exit and joins with 5s timeout",
38
- "running? returns boolean",
39
- "Each tick: queries performance_schema for top 50 digests by SUM_TIMER_WAIT",
40
- "Computes deltas: delta_calls = current - previous, delta_total_time = current - previous",
41
- "Records delta snapshot into the StatsHistory instance",
42
- "Negative deltas (server restart) recorded as 0",
43
- "If performance_schema is unavailable, logs warning and stops (no crash loop)",
44
- "Add require to gems/mysql_genius-core/lib/mysql_genius/core.rb",
45
- "Create spec at gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb",
46
- "Core gem suite passes",
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",
39
+ "Desktop gem suite passes",
47
40
  "Typecheck passes"
48
41
  ],
49
42
  "priority": 2,
50
43
  "passes": true,
51
- "notes": "The connection_provider is a callable (lambda/proc) that returns a Core::Connection. This lets Rails pass -> { ActiveRecordAdapter.new(ActiveRecord::Base.connection) } and the sidecar pass -> { session.checkout { |a| a } }. Use the same SQL shape as QueryStats#build_sql but hardcoded to top 50 by SUM_TIMER_WAIT. Store @previous hash of {digest => {calls:, total_time_ms:}} for delta computation."
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."
52
45
  },
53
46
  {
54
47
  "id": "US-003",
55
- "title": "Add DIGEST hash to QueryStats return value",
56
- "description": "As a developer, I need the DIGEST hex hash in QueryStats output so the detail page can use it as a URL key.",
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.",
57
50
  "acceptanceCriteria": [
58
- "Modify gems/mysql_genius-core/lib/mysql_genius/core/analysis/query_stats.rb",
59
- "Add DIGEST column to the SELECT in build_sql",
60
- "Add digest: row['DIGEST'] to the transform method return hash",
61
- "Existing fields unchanged (backward compatible addition)",
62
- "Update query_stats_spec to verify the new digest field is present",
63
- "Core gem suite passes",
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",
57
+ "Desktop gem suite passes",
64
58
  "Typecheck passes"
65
59
  ],
66
60
  "priority": 3,
67
61
  "passes": true,
68
- "notes": "DIGEST is a 64-char hex string computed by MySQL. It's stable across identical query templates. The existing sql field continues to hold the truncated DIGEST_TEXT."
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": "Add query detail shared template with SVG charts",
73
- "description": "As a user, I want to see a query's SQL, current stats, and time-series performance charts on a dedicated page.",
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
- "Create gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb",
76
- "Template shows: full SQL in mg-sql-block styled container",
77
- "Template shows: Explain button that fires POST /explain",
78
- "Template shows: stats summary cards (Calls, Total Time, Avg Time, Max Time, Rows Examined, Rows Sent, First Seen, Last Seen)",
79
- "Template shows: three SVG charts stacked vertically (Total Time ms, Average Time ms, Calls)",
80
- "SVG charts drawn by a drawChart(containerId, data, label, color) JS function",
81
- "Charts use polyline with translucent fill below the line",
82
- "Charts have Y axis auto-scaled with 4-5 ticks, X axis with time labels every ~4 hours",
83
- "Charts use #89CFF0 line color in light mode, #58a6ff in dark mode",
84
- "Chart height is 200px, width responsive (100% of container)",
85
- "Page loads data via fetch to GET /api/query_history/:digest on page load",
86
- "All JS is inline in the template (no external dependencies)",
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",
78
+ "Desktop gem suite passes",
87
79
  "Typecheck passes"
88
80
  ],
89
81
  "priority": 4,
90
82
  "passes": true,
91
- "notes": "The template is standalone (not a tab partial). It will be rendered through each adapter's layout. The drawChart function creates an SVG element with: a viewBox for responsive scaling, a polyline for the data series, rect elements or text for axis labels. The Explain button reuses the existing POST /explain endpoint. Data fetched from /api/query_history/:digest returns {query: {...stats...}, history: [{timestamp, calls, total_time_ms, avg_time_ms}, ...]}."
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": "Make Query Stats tab SQL cells clickable links",
96
- "description": "As a user, I want to click a query in the stats table to see its detail page.",
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-core/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb",
99
- "In the loadQueryStats JS function, render SQL column as <a href='/queries/:digest'> link",
100
- "Link uses the digest hex hash from the query_stats API response",
101
- "Link styled with mg-link class or inline color to look clickable",
102
- "Existing query stats table layout and sorting still work",
103
- "Rails adapter suite passes: bundle exec rspec",
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",
94
+ "Desktop gem suite passes",
104
95
  "Typecheck passes"
105
96
  ],
106
97
  "priority": 5,
107
98
  "passes": true,
108
- "notes": "The loadQueryStats function is in dashboard.html.erb's inline JS. It currently renders the SQL as plain text in a td. Change it to an anchor tag. The href should use path_for pattern but since this is in JS, just hardcode '/queries/' + digest (the sidecar's PATHS hash and Rails route both serve this path). Also update the dashboard overview's Top 5 Expensive Queries to be clickable."
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."
109
100
  },
110
101
  {
111
102
  "id": "US-006",
112
- "title": "Wire stats collector and detail routes into Rails adapter",
113
- "description": "As a Rails developer, I want the stats collector to start on boot and the query detail page to be accessible.",
103
+ "title": "Full green sweep",
104
+ "description": "As a developer, I need all test suites passing.",
114
105
  "acceptanceCriteria": [
115
- "Add stats_collection config option (default true) to lib/mysql_genius/configuration.rb",
116
- "Add MysqlGenius.stats_history and MysqlGenius.stats_collector module-level accessors to lib/mysql_genius.rb",
117
- "Add initializer in lib/mysql_genius/engine.rb that starts StatsCollector when enabled",
118
- "Initializer creates StatsHistory, StatsCollector with ActiveRecordAdapter connection_provider, calls start",
119
- "Registers at_exit to stop the collector",
120
- "Add two routes to config/routes.rb: get 'queries/:digest' and get 'api/query_history/:digest'",
121
- "Add query_detail action to QueriesController that renders the shared template",
122
- "Add query_history action that returns JSON with current stats + history",
123
- "query_history looks up the digest in performance_schema and gets history from MysqlGenius.stats_history",
124
- "Add request spec for GET /queries/:digest (returns 200)",
125
- "Add request spec for GET /api/query_history/:digest (returns JSON with query and history keys)",
126
- "Rails adapter suite passes",
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",
127
110
  "Typecheck passes"
128
111
  ],
129
112
  "priority": 6,
130
113
  "passes": true,
131
- "notes": "The connection_provider for Rails is -> { Core::Connection::ActiveRecordAdapter.new(ActiveRecord::Base.connection) }. The query_detail action just renders the template with an @digest instance variable. The query_history action queries performance_schema filtered by DIGEST = :digest for current stats, and calls MysqlGenius.stats_history.series_for(digest_text) for history. If stats_history is nil (collection disabled), return empty history array."
132
- },
133
- {
134
- "id": "US-007",
135
- "title": "Wire stats collector and detail routes into desktop sidecar",
136
- "description": "As a sidecar user, I want the stats collector running and the query detail page accessible.",
137
- "acceptanceCriteria": [
138
- "Update gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb to create StatsHistory + StatsCollector and set on App",
139
- "Add App settings: :stats_history, :stats_collector",
140
- "Collector uses connection_provider that checks out from the active session",
141
- "Register at_exit to stop collector",
142
- "Update SessionSwapper to stop old collector, clear history, start new collector on profile switch",
143
- "Add GET /queries/:digest route to App (renders query_detail template through layout)",
144
- "Add GET /api/query_history/:digest route to App (returns JSON)",
145
- "Both new routes are under session-token auth",
146
- "Add request spec for the two new routes",
147
- "Desktop gem suite passes: (cd gems/mysql_genius-desktop && bundle exec rspec)",
148
- "Typecheck passes"
149
- ],
150
- "priority": 7,
151
- "passes": true,
152
- "notes": "The connection_provider for the sidecar wraps session.checkout. On profile switch, SessionSwapper should: App.settings.stats_collector&.stop, App.settings.stats_history&.clear, then after swapping the session create a new collector with the new session and start it. The render_query_detail method follows the same pattern as render_dashboard (Tilt through layout)."
153
- },
154
- {
155
- "id": "US-008",
156
- "title": "Full green sweep across all suites",
157
- "description": "As a developer, I need all test suites and linters passing before the PR.",
158
- "acceptanceCriteria": [
159
- "Rails adapter rspec passes (78+ examples)",
160
- "Core gem rspec passes (194+ examples plus new StatsHistory/StatsCollector/QueryStats specs)",
161
- "Desktop gem rspec passes (154+ examples plus new route specs)",
162
- "Rails adapter rubocop clean",
163
- "Core gem rubocop clean",
164
- "Desktop gem rubocop clean",
165
- "git diff main -- .github/workflows/publish.yml is empty (untouched)",
166
- "No version bumps committed (version bump is a separate release task)",
167
- "Typecheck passes"
168
- ],
169
- "priority": 8,
170
- "passes": false,
171
- "notes": "Run all six commands in order. If any fail, fix before proceeding. This is a verification step, not an implementation step."
114
+ "notes": ""
172
115
  }
173
116
  ]
174
117
  }
data/ralph/progress.txt CHANGED
@@ -1,141 +1,119 @@
1
1
  # Ralph Progress Log
2
- Started: Sun Apr 12 13:52:29 CDT 2026
2
+ Started: Mon Apr 13 06:43:11 CDT 2026
3
3
  ---
4
4
 
5
5
  ## Codebase Patterns
6
- - Core gem specs use `require "spec_helper"` (no Rails boot), FakeAdapter for connection stubs
7
- - Analysis classes follow `initialize(connection)` + `#call` pattern
8
- - Core gem has zero runtime dependencies no Rails-specific code
9
- - Spec files mirror the lib directory structure: `lib/mysql_genius/core/analysis/foo.rb` `spec/mysql_genius/core/analysis/foo_spec.rb`
10
- - RuboCop uses rubocop-shopify + rubocop-rspec; target Ruby 2.6
11
- - `Time#iso8601` requires `require 'time'` use `strftime("%Y-%m-%dT%H:%M:%SZ")` in core gem instead
12
- - FakeAdapter stubs are searched with `find` (first match wins) clear `@stubs` via `instance_variable_set(:@stubs, [])` before re-stubbing
13
- - Use `ConditionVariable` + `Mutex` for interruptible sleep in background threads (cleaner than polling loops)
14
- - Standalone templates (not partials) must duplicate JS helpers from dashboard since they run in a separate page scope
15
- - Shared templates use `path_for(:name)` for URLs and `@digest` instance var — adapters must set these before rendering
16
- - Desktop gem has NO ActiveSupport never use `.squish` or other AS extensions; use plain string concatenation for SQL
17
- - Desktop `Core::Result#to_a` returns raw row arrays; use `to_hashes` to get `{column => value}` hashes
18
- - Desktop `rack_helper.rb` sets `@fake_adapter` via before blockspecs using it must disable `RSpec/InstanceVariable`
19
- - Rails `ActiveRecord::Result` `to_a` returns hashes directly; for specs use `instance_double` with `to_a:` returning hash arrays
20
- - When adding new App settings to desktop, also reset them in `rack_helper.rb` after block and set in before block
21
- - `StatsCollector.new` spawns real threads mock it in request specs via `allow(StatsCollector).to receive(:new)` in rack_helper
22
- - `path_for(:query_detail)` and `path_for(:query_history)` both need `@digest` to build a complete URL; both adapters handle this in their `path_for` implementation
23
- - Routes that need a dynamic segment (digest) use base-path + value pattern in desktop PATHS hash; Rails uses named route helpers with keyword arg
24
-
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 fieldskeep 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)
25
23
  ---
26
24
 
27
- ## 2026-04-12 - US-001
28
- - Implemented `StatsHistory` ring buffer in `gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_history.rb`
29
- - Thread-safe via Mutex, supports record/series_for/digests/clear, drops oldest on cap
30
- - Added require to `gems/mysql_genius-core/lib/mysql_genius/core.rb`
31
- - Created spec at `gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_history_spec.rb` (9 examples)
32
- - Files changed: stats_history.rb (new), core.rb (require added), stats_history_spec.rb (new)
33
- - Core gem suite: 203 examples, 0 failures
34
- - RuboCop: clean
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)
35
32
  - **Learnings for future iterations:**
36
- - The core gem spec suite runs fast (~0.06s) — safe to run full suite on each story
37
- - FakeAdapter is only needed for connection-dependent classes; StatsHistory is pure Ruby
38
- - Thread safety tests work well with 4 threads × 500 iterations pattern
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
39
38
  ---
40
39
 
41
- ## 2026-04-12 - US-002
42
- - Implemented `StatsCollector` background sampler in `gems/mysql_genius-core/lib/mysql_genius/core/analysis/stats_collector.rb`
43
- - Background thread queries performance_schema for top 50 digests by SUM_TIMER_WAIT
44
- - Computes per-interval deltas, clamps negatives to 0 (server restart handling)
45
- - Uses ConditionVariable for interruptible sleep (clean stop within 5s timeout)
46
- - connection_provider callable pattern: adapters supply their own connection strategy
47
- - Added require to `gems/mysql_genius-core/lib/mysql_genius/core.rb`
48
- - Created spec at `gems/mysql_genius-core/spec/mysql_genius/core/analysis/stats_collector_spec.rb` (12 examples)
49
- - Files changed: stats_collector.rb (new), core.rb (require added), stats_collector_spec.rb (new)
50
- - Core gem suite: 215 examples, 0 failures
51
- - RuboCop: clean
40
+ ## 2026-04-13 - US-002
41
+ - What was implemented: Added SSH tunnel columns to the profiles SQLite table with backward-compatible migration
42
+ - Files changed:
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)
52
45
  - **Learnings for future iterations:**
53
- - `Time#iso8601` is not available without `require 'time'` — use `strftime` in zero-dep core gem
54
- - FakeAdapter stubs match first-registered-first-matched; clear `@stubs` array before re-stubbing between ticks
55
- - Testing background threads: set `@running = true` in `start` (before thread spawn) to eliminate race; use `send(:tick)` to test delta logic directly without timing sensitivity
56
- - SQL shape mirrors QueryStats#build_sql but selects only DIGEST_TEXT, COUNT_STAR, total_time_ms
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)
57
52
  ---
58
53
 
59
- ## 2026-04-12 - US-003
60
- - Added `DIGEST` column to `build_sql` SELECT and `digest:` key to `transform` return hash in QueryStats
61
- - Updated spec columns/rows fixtures to include DIGEST as first column, verified new field in assertions
62
- - Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/analysis/query_stats.rb`, `gems/mysql_genius-core/spec/mysql_genius/core/analysis/query_stats_spec.rb`
63
- - Core gem suite: 215 examples, 0 failures
64
- - Rails adapter suite: 78 examples, 0 failures
65
- - RuboCop: clean
54
+ ## 2026-04-13 - US-003
55
+ - What was implemented: Wired SSH tunnel into ActiveSession connection flow with tunnel lifecycle management
56
+ - Files changed:
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)
66
65
  - **Learnings for future iterations:**
67
- - QueryStats spec uses positional arrays for row data — when adding a new column to the SELECT, prepend/insert it in ALL test row arrays (easy to miss the truncation test at the bottom)
68
- - The `transform` method uses case-insensitive column access (`row["DIGEST"] || row["digest"]`) for MariaDB compatibility follow this pattern for any new columns
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
69
73
  ---
70
74
 
71
- ## 2026-04-12 - US-004
72
- - Created standalone `query_detail.html.erb` shared template in core gem views
73
- - Template includes: full SQL display with syntax highlighting, Copy and EXPLAIN buttons
74
- - Stats summary cards: Calls, Total Time, Avg Time, Max Time, Rows Examined, Rows Sent, First Seen, Last Seen
75
- - Three SVG time-series charts: Total Time (ms), Average Time (ms), Calls
76
- - SVG charts use polyline with translucent fill, auto-scaled Y axis with nice tick values, responsive via viewBox
77
- - Light mode line color #89CFF0, dark mode #58a6ff (detected via data-theme attribute)
78
- - Data loaded via fetch to GET /api/query_history/:digest, EXPLAIN via POST /explain
79
- - All JS is inline (no external dependencies), replicates helper functions from dashboard (highlightSql, formatDuration, ajax, etc.)
80
- - Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/query_detail.html.erb` (new)
81
- - Core gem suite: 215 examples, 0 failures
82
- - Rails adapter suite: 78 examples, 0 failures
83
- - RuboCop: clean
75
+ ## 2026-04-13 - US-004
76
+ - What was implemented: Added SSH tunnel UI fields to the connections page profile form
77
+ - Files changed:
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)
84
82
  - **Learnings for future iterations:**
85
- - Standalone templates (not tab partials) need their own copies of JS helper functions since they don't share the dashboard's IIFE scope
86
- - The template uses `path_for(:root)` for the back link, `path_for(:explain)` for EXPLAIN, `path_for(:query_history)` for data these routes must exist in both Rails adapter and sidecar
87
- - SVG chart viewBox approach (800x200 with preserveAspectRatio="none") makes charts responsive without JS resize handlers
88
- - Y-axis nice step calculation: ceil(rawStep/magnitude)*magnitude ensures clean round-number ticks
89
- - The `@digest` instance variable must be set by the controller action before rendering this template
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
90
91
  ---
91
92
 
92
- ## 2026-04-11 - US-005
93
- - Updated `dashboard.html.erb` to linkify SQL cells in both loadQueryStats and Top 5 Expensive Queries
94
- - Added `query_detail: '/queries/'` to ROUTES JS object (base path, digest appended via JS)
95
- - SQL cells now render `<a href="/queries/:digest" class="mg-link">` when digest is present; fallback to plain text if no digest
96
- - Files changed: `gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/dashboard.html.erb`
97
- - Rails adapter suite: 78 examples, 0 failures
98
- - RuboCop: clean
93
+ ## 2026-04-13 - US-005
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
99
+ - Files changed:
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)
99
103
  - **Learnings for future iterations:**
100
- - The ROUTES JS object uses hardcoded base paths (not server-rendered route helpers) for dynamic routes append the value in JS
101
- - `mg-link` class is defined in layout.html.erb (both adapters share the layout's CSS)
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
- ## 2026-04-11 - US-006
105
- - Added `stats_collection` config option (default: true) to `lib/mysql_genius/configuration.rb`
106
- - Added `MysqlGenius.stats_history` and `MysqlGenius.stats_collector` module-level accessors to `lib/mysql_genius.rb`
107
- - Added `config.after_initialize` block in engine.rb that starts StatsCollector when `stats_collection` is enabled
108
- - Added routes: `get "queries/:digest"` and `get "api/query_history/:digest"` to `config/routes.rb`
109
- - Added `query_detail` and `query_history` actions to `QueriesController`
110
- - Updated `SharedViewHelpers#path_for` to auto-inject `@digest` for `:query_detail` and `:query_history` routes
111
- - Added private helpers `fetch_query_history_current`, `fetch_query_history_series`, `lookup_digest_text`
112
- - Added request specs in `spec/requests/mysql_genius/query_detail_spec.rb` (4 examples)
113
- - Files changed: configuration.rb, lib/mysql_genius.rb, engine.rb, config/routes.rb, queries_controller.rb, shared_view_helpers.rb, query_detail_spec.rb (new)
114
- - Rails adapter suite: 82 examples, 0 failures
115
- - RuboCop: clean
116
- - **Learnings for future iterations:**
117
- - `ActiveRecord::Result` `instance_double` — use `to_a:` returning hash arrays; use `result.to_a.first` not `result.first` (which needs Enumerable)
118
- - Engine's `config.after_initialize` creates the collector in test env too, causing a benign "StatsCollector stopped" warning in tests — this is expected
119
- - `path_for` needs to know about digest routes: use an allowlist `%i[query_detail query_history]` and conditionally pass `digest:` param
120
- ---
121
-
122
- ## 2026-04-11 - US-007
123
- - Updated `Launcher#call` to create StatsHistory + StatsCollector, set on App, register collector in at_exit
124
- - Added `set :stats_history, nil` and `set :stats_collector, nil` to App settings
125
- - Updated App `path_for` helper to append `@digest` for `:query_detail` and `:query_history` routes
126
- - Added `GET /queries/:digest` and `GET /api/query_history/:digest` routes to App (under session-token auth)
127
- - Added `render_query_detail`, `fetch_query_history_current`, `fetch_query_history_series` private methods
128
- - Updated SessionSwapper to stop old collector, clear history, create new collector after session swap
129
- - Added `:root`, `:query_detail`, `:query_history` to desktop PATHS hash
130
- - Updated `rack_helper.rb` to reset stats settings and stub `StatsCollector.new` globally to prevent background threads in specs
131
- - Updated `session_swapper_spec.rb` to mock StatsCollector/StatsHistory and handle new `set` keys
132
- - Added `spec/requests/query_detail_spec.rb` for desktop (4 examples)
133
- - Files changed: app.rb, paths.rb, launcher.rb, session_swapper.rb, rack_helper.rb, session_swapper_spec.rb, query_detail_spec.rb (new)
134
- - Desktop suite: 158 examples, 0 failures (was 154)
135
- - RuboCop: clean
136
- - **Learnings for future iterations:**
137
- - Desktop gem has NO ActiveSupport — never use `.squish`; use explicit string concatenation
138
- - `Core::Result#to_a` returns raw row arrays (not hashes) — use `to_hashes` for hash access
139
- - When new `SessionSwapper#switch_to` logic creates real background objects, existing integration tests can fail — mock the constructor in rack_helper to prevent threads
140
- - Add `# rubocop:disable RSpec/InstanceVariable` at file top + `enable` at bottom when using `@fake_adapter` directly in specs
111
+ ## 2026-04-13 - US-006
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
141
119
  ---
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-12 00:00:00.000000000 Z
11
+ date: 2026-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.7.0
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.0
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