mysql_genius 0.7.2 → 0.8.1
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/app/controllers/concerns/mysql_genius/ai_features.rb +55 -0
- data/config/routes.rb +6 -0
- data/docs/guides/ai-features.md +115 -0
- data/docs/guides/getting-started-rails.md +118 -0
- data/docs/guides/ssh-tunnel-connections.md +151 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/mysql_genius.gemspec +1 -1
- data/ralph/prd.json +58 -90
- data/ralph/progress.txt +91 -110
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 637da7a2a5441d7195dc1c978e40e7289e7f0ba5db68c55b6696363dcee3c313
|
|
4
|
+
data.tar.gz: 0b37933d2a0faa1ae8803456c6c2ebb8cf179e182b2aeaac3fe1e67b22d42d10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 26b1727acbcd3745da3d17a4ae9837efc63a3417d73a4ac3b3ae0220b423e96f735c9d2c4cf716e351fc31d8e34b6524331c1af30e7c3e2be5a711102b045f1d
|
|
7
|
+
data.tar.gz: 0ae1270e27a4bf2183e9a1f2a14dcdf2c1c1ec4ee5fe6184a807ea96f3d0a97036182dc2968f690e9f9f4e660b98ec58546f131a41b22674ef2963feb6c2c7bf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Older MySQL compatibility** — the `DIGEST` column in `performance_schema.events_statements_summary_by_digest` is not present on all MySQL/MariaDB versions. `QueryStats` now checks for column existence before including it in the SELECT. Query links in the Query Stats tab gracefully degrade to plain text when the digest is unavailable.
|
|
7
|
+
|
|
8
|
+
## 0.8.0
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **6 new AI analysis features:**
|
|
12
|
+
- **Variable Config Reviewer** — reviews my.cnf settings against observed workload
|
|
13
|
+
- **Connection Pressure Advisor** — diagnoses connection pool health
|
|
14
|
+
- **Workload Digest** — executive summary of the entire query workload
|
|
15
|
+
- **InnoDB Health Interpreter** — plain English translation of `SHOW ENGINE INNODB STATUS`
|
|
16
|
+
- **Index Consolidation Planner** — holistic drop/merge/add index plan across tables
|
|
17
|
+
- **Slow Query Pattern Grouper** — groups slow queries by shared root cause
|
|
18
|
+
- AI buttons added to Server tab, Query Stats tab, and Indexes tabs
|
|
19
|
+
- `mysql_genius` now declares runtime dependency on `mysql_genius-core ~> 0.8.0`
|
|
20
|
+
|
|
3
21
|
## 0.7.2
|
|
4
22
|
|
|
5
23
|
### 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.
|
data/lib/mysql_genius/version.rb
CHANGED
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.
|
|
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/
|
|
4
|
-
"description": "
|
|
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
|
|
9
|
-
"description": "As a developer, I need a
|
|
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
|
|
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/
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
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
|
|
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": "
|
|
33
|
-
"description": "As a developer, I need
|
|
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
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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": "
|
|
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": "
|
|
53
|
-
"description": "As a user, I want
|
|
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
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
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
|
|
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": "
|
|
73
|
-
"description": "As a user, I want
|
|
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
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
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": "
|
|
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": "
|
|
96
|
-
"description": "As a
|
|
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
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
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": "
|
|
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": "
|
|
114
|
-
"description": "As a developer, I
|
|
103
|
+
"title": "Full green sweep",
|
|
104
|
+
"description": "As a developer, I need all test suites passing.",
|
|
115
105
|
"acceptanceCriteria": [
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
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": "
|
|
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:
|
|
2
|
+
Started: Mon Apr 13 06:43:11 CDT 2026
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
## Codebase Patterns
|
|
6
|
-
- Desktop gem specs use
|
|
7
|
-
- Desktop gem
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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-
|
|
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
|
-
-
|
|
33
|
-
-
|
|
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
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
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
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
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
|
-
-
|
|
60
|
-
-
|
|
61
|
-
- Profile
|
|
62
|
-
-
|
|
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
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
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
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
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
|
-
-
|
|
100
|
-
-
|
|
101
|
-
- `
|
|
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
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
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.
|
|
4
|
+
version: 0.8.1
|
|
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.
|
|
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.
|
|
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
|