mysql_genius 0.7.1 → 0.7.2

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: 1d2b3f6350a4c464b3ec546a5248a9b1db4dc933e80e2da26472012367740828
4
+ data.tar.gz: 13ffee6262a381bb970cf6f9e473c95e40161d191249228225b6960aec7c9313
5
5
  SHA512:
6
- metadata.gz: 5a797a52e89c12e8a266ec70b4f9264e4270450000630824789f64a19b53f4f22b5483bdcef634a97f904f9b61f7b533559f1dfa1fd37ee4ee9711f650f143db
7
- data.tar.gz: 9e46e3fafff0682dbeb29ffecc855f4d00d204efe8b15517ab2f05baaadf94da50836f12dfbd928f322a9df624d4f129442602f8f7074a043a8ce11896d5cd7a
6
+ metadata.gz: 350005c5824825d20f187099dab6f181eb476d1b99441fdf2101f09430b4bb23abff72bbbae487f9a58bca43de02a20213e508980dbb407577678c45bbdf3f08
7
+ data.tar.gz: c372b88067e33f91b89840e994733ffb5a810ac0bdf38a582e32e240126d026871babe689911a4655bf7a1ba1d0e7664e4f3c7185ba1a96472a6f65d349ecfec
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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2
4
+
5
+ ### Added
6
+ - **Anthropic Messages API support** — `x-api-key` auth style with `anthropic-version` header, top-level `system` parameter, `content[0].text` response parsing.
7
+ - **Configurable `max_tokens`** — new field on `Core::Ai::Config` (default 4096), sent to both OpenAI and Anthropic APIs.
8
+ - **Copy response button** on all AI result sections (schema review, migration risk, optimization, describe query, rewrite, index advisor, root cause, anomaly detection).
9
+ - **Dark mode contrast fixes** for AI result sections — proper CSS classes with dark-mode variants replace hardcoded light-mode inline styles.
10
+ - **`capability?(:standalone_header)`** guard hides the dashboard header when rendered inside a layout that already provides one.
11
+
3
12
  ## 0.7.1
4
13
 
5
14
  Lockstep version bump with `mysql_genius-core 0.7.1` which fixes missing ERB templates in the gem package.
@@ -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; }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlGenius
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.2"
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.7.2")
34
34
  spec.add_dependency("railties", ">= 6.0", "< 9")
35
35
  end
data/ralph/prd.json CHANGED
@@ -1,174 +1,149 @@
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/sqlite-migration",
4
+ "description": "Replace in-memory StatsHistory and YAML ProfileManager with SQLite for persistent stats and clean profile CRUD in the desktop sidecar",
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 sqlite3 dependency and create Database class with schema",
9
+ "description": "As a developer, I need a Database class that manages a SQLite file with profiles, settings, and stats_snapshots tables.",
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 sqlite3 ~> 2.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/database.rb",
14
+ "Database.new(path) creates the SQLite file if missing and runs CREATE TABLE IF NOT EXISTS for all 3 tables",
15
+ "profiles table: name TEXT PRIMARY KEY, host TEXT NOT NULL, port INTEGER DEFAULT 3306, username TEXT NOT NULL, password TEXT DEFAULT '', database_name TEXT NOT NULL, tls_mode TEXT DEFAULT 'preferred', created_at TEXT, updated_at TEXT",
16
+ "settings table: key TEXT PRIMARY KEY, value TEXT",
17
+ "stats_snapshots table: id INTEGER PRIMARY KEY AUTOINCREMENT, digest_text TEXT NOT NULL, timestamp TEXT NOT NULL, delta_calls INTEGER DEFAULT 0, delta_total_time_ms REAL DEFAULT 0, delta_avg_time_ms REAL DEFAULT 0",
18
+ "Index on stats_snapshots(digest_text, timestamp)",
19
+ "Profile CRUD: list_profiles returns array of hashes, find_profile(name) returns hash or nil, add_profile(attrs) inserts (raises on duplicate), update_profile(name, attrs) updates (raises if not found), delete_profile(name) deletes (raises if not found)",
20
+ "Settings: get_setting(key) returns string or nil, set_setting(key, value) upserts, get_ai_config returns hash of ai.* keys with prefix stripped, set_ai_config(hash) writes each key with ai. prefix",
21
+ "Stats: record_snapshot(digest_text, snapshot_hash) inserts + prunes rows older than 24 hours, series_for(digest_text) returns ordered array last 24hr, digests returns distinct digest_texts, clear deletes all",
22
+ "Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb with tests for all CRUD operations using tmpdir",
23
+ "Desktop gem suite passes",
23
24
  "Typecheck passes"
24
25
  ],
25
26
  "priority": 1,
26
27
  "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."
28
+ "notes": "Use raw sqlite3 gem calls, no ORM. Thread safety: SQLite WAL mode handles concurrent reads. Error classes: Database::DuplicateProfileError, Database::ProfileNotFoundError. The spec should use Dir.mktmpdir for isolated DB files per test."
28
29
  },
29
30
  {
30
31
  "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.",
32
+ "title": "Create SqliteStatsHistory as drop-in replacement for StatsHistory",
33
+ "description": "As a developer, I need a SQLite-backed stats history with the same API as the in-memory StatsHistory so the StatsCollector works unchanged.",
33
34
  "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",
35
+ "Create gems/mysql_genius-desktop/lib/mysql_genius/desktop/sqlite_stats_history.rb",
36
+ "SqliteStatsHistory.new(database) takes a Database instance",
37
+ "record(digest_text, snapshot_hash) delegates to database.record_snapshot",
38
+ "series_for(digest_text) delegates to database.series_for, returns array of hashes with symbol keys (timestamp:, calls:, total_time_ms:, avg_time_ms:)",
39
+ "digests delegates to database.digests",
40
+ "clear delegates to database.clear",
41
+ "Same public API as MysqlGenius::Core::Analysis::StatsHistory (record, series_for, digests, clear)",
42
+ "Create spec at gems/mysql_genius-desktop/spec/mysql_genius/desktop/sqlite_stats_history_spec.rb",
43
+ "Desktop gem suite passes",
47
44
  "Typecheck passes"
48
45
  ],
49
46
  "priority": 2,
50
47
  "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."
48
+ "notes": "The StatsCollector passes snapshot hashes with string keys from its tick method. SqliteStatsHistory should accept both string and symbol keys. The series_for return format must match what the query_detail template JS expects: [{timestamp:, calls:, total_time_ms:, avg_time_ms:}]."
52
49
  },
53
50
  {
54
51
  "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.",
52
+ "title": "Add first-boot YAML import logic to Launcher",
53
+ "description": "As a user, I want my existing YAML profiles and AI config automatically imported into SQLite on first boot.",
57
54
  "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",
55
+ "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb",
56
+ "After Config.load, create Database.new at ~/.config/mysql_genius/mysql_genius.db (create dir if needed)",
57
+ "If database.list_profiles is empty AND config.profiles is not empty, import each profile into SQLite",
58
+ "If database.get_setting('ai.endpoint') is nil AND config.ai.endpoint is present, import AI config into SQLite",
59
+ "Set App.set(:database, db)",
60
+ "Create SqliteStatsHistory.new(db) and pass to StatsCollector instead of StatsHistory.new",
61
+ "Add require statements for database and sqlite_stats_history to desktop.rb",
62
+ "Existing desktop specs still pass (rack_helper needs updating to inject a test Database)",
63
+ "Desktop gem suite passes",
64
64
  "Typecheck passes"
65
65
  ],
66
66
  "priority": 3,
67
67
  "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."
68
+ "notes": "The rack_helper.rb needs to create a tmpdir-based Database and set it on the App via App.set(:database, db). Ensure mkdir_p for ~/.config/mysql_genius/ before creating the DB. The import only runs when the DB is empty — deleting mysql_genius.db forces re-import on next boot."
69
69
  },
70
70
  {
71
71
  "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.",
72
+ "title": "Rewire profile API routes to use Database",
73
+ "description": "As a user, I want the /connections page to read and write profiles from SQLite instead of YAML.",
74
74
  "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)",
75
+ "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb",
76
+ "GET /api/profiles reads from settings.database.list_profiles + settings.current_profile_name",
77
+ "POST /api/profiles calls settings.database.add_profile, returns updated list",
78
+ "PUT /api/profiles/:name calls settings.database.update_profile, returns updated list",
79
+ "DELETE /api/profiles/:name calls settings.database.delete_profile (rejects active profile), returns updated list",
80
+ "POST /api/test_connection unchanged (no storage involved)",
81
+ "POST /api/profiles/:name/connect reads profile from settings.database.find_profile",
82
+ "GET /api/ai_config reads from settings.database.get_ai_config",
83
+ "PUT /api/ai_config writes to settings.database.set_ai_config + reloads into running app",
84
+ "Remove require for profile_manager from app.rb",
85
+ "Update profiles_api_spec.rb to use Database instead of YAML tmpdir",
86
+ "Desktop gem suite passes",
87
87
  "Typecheck passes"
88
88
  ],
89
89
  "priority": 4,
90
90
  "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}, ...]}."
91
+ "notes": "Error mapping: Database::DuplicateProfileError -> 409, Database::ProfileNotFoundError -> 404. The active-profile-delete check stays in the route handler (check settings.current_profile_name before calling delete). The connect route builds a MysqlConfig from the DB hash: Config::MysqlConfig.from_hash(profile_hash)."
92
92
  },
93
93
  {
94
94
  "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.",
95
+ "title": "Rewire SessionSwapper to use Database",
96
+ "description": "As a developer, I need SessionSwapper to look up profiles from SQLite instead of Config.",
97
97
  "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",
98
+ "Modify gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb",
99
+ "Change initialize to accept (app_class, config, database) instead of (app_class, config)",
100
+ "switch_to looks up profile via @database.find_profile(name) instead of @config.profile_by_name(name)",
101
+ "Build MysqlConfig from the DB hash via Config::MysqlConfig.from_hash(profile)",
102
+ "Update all callers of SessionSwapper.new to pass the database",
103
+ "Update session_swapper_spec.rb",
104
+ "Desktop gem suite passes",
104
105
  "Typecheck passes"
105
106
  ],
106
107
  "priority": 5,
107
108
  "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."
109
+ "notes": "The profile hash from Database has string keys (host, port, etc). MysqlConfig.from_hash handles both string and symbol keys via transform_keys. The build_switch_config method still creates a Config.allocate with profiles array for ActiveSession compatibility."
109
110
  },
110
111
  {
111
112
  "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.",
113
+ "title": "Delete ProfileManager and clean up",
114
+ "description": "As a developer, I want to remove the dead ProfileManager code now that Database handles everything.",
114
115
  "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",
116
+ "Delete gems/mysql_genius-desktop/lib/mysql_genius/desktop/profile_manager.rb",
117
+ "Delete gems/mysql_genius-desktop/spec/mysql_genius/desktop/profile_manager_spec.rb",
118
+ "Remove require 'mysql_genius/desktop/profile_manager' from desktop.rb",
119
+ "Remove require 'mysql_genius/desktop/profile_manager' from app.rb if present",
120
+ "Grep the entire desktop gem for 'ProfileManager' references and remove any remaining",
121
+ "Desktop gem suite passes",
122
+ "All 3 rubocops pass",
127
123
  "Typecheck passes"
128
124
  ],
129
125
  "priority": 6,
130
126
  "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."
127
+ "notes": "Make sure no spec files reference ProfileManager. The Database class error classes (DuplicateProfileError, ProfileNotFoundError) replace ProfileManager's error classes verify the route handlers rescue the correct class names."
132
128
  },
133
129
  {
134
130
  "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
131
  "title": "Full green sweep across all suites",
157
132
  "description": "As a developer, I need all test suites and linters passing before the PR.",
158
133
  "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)",
134
+ "Rails adapter rspec passes (82+ examples)",
135
+ "Core gem rspec passes (215+ examples)",
136
+ "Desktop gem rspec passes (150+ examples)",
162
137
  "Rails adapter rubocop clean",
163
138
  "Core gem rubocop clean",
164
139
  "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)",
140
+ "No changes to mysql_genius-core or mysql_genius Rails adapter source files",
141
+ "publish.yml untouched",
167
142
  "Typecheck passes"
168
143
  ],
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."
144
+ "priority": 7,
145
+ "passes": true,
146
+ "notes": "Run all six commands. If any fail, fix before marking complete."
172
147
  }
173
148
  ]
174
149
  }
data/ralph/progress.txt CHANGED
@@ -1,141 +1,138 @@
1
1
  # Ralph Progress Log
2
- Started: Sun Apr 12 13:52:29 CDT 2026
2
+ Started: Sun Apr 12 23:47:10 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 block — specs 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
6
+ - Desktop gem specs use `Dir.mktmpdir` for isolated file-based test fixtures (DB files, YAML configs)
7
+ - Desktop gem rubocop config inherits from root `.rubocop.yml` via `inherit_from`
8
+ - Desktop gem Gemfile.lock is not tracked in git (gem convention)
9
+ - Use `results_as_hash = true` on SQLite3::Database to get hash-style row access
10
+ - Desktop app settings are injected via `App.set(:key, value)` (Sinatra pattern)
11
+ - Launcher private methods can be stubbed via `allow(launcher).to(receive(:method_name))` in RSpec
12
+ - First-boot import logic: only runs when DB is empty (list_profiles.empty? / get_setting returns nil)
13
+ - rack_helper.rb must set `App.set(:database, ...)` for request specs that depend on DB settings
14
+ - Database returns `database_name` key; `MysqlConfig.from_hash` expects `database` remap via `mysql_hash_from_profile`
15
+ - Profile API response must be `{name:, mysql: {host:, port:, database:, ...}}` — use `format_profile` to reshape DB rows
24
16
 
25
17
  ---
26
18
 
27
19
  ## 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
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)
31
+ - Files changed:
32
+ - `gems/mysql_genius-desktop/mysql_genius-desktop.gemspec` (added sqlite3 dep)
33
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/database.rb` (new)
34
+ - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/database_spec.rb` (new)
35
35
  - **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
36
+ - ISO 8601 timestamps have second precision `sleep(0.01)` is not enough to test timestamp changes, need `sleep(1.1)`
37
+ - RSpec `let` is lazy must reference the variable in the test body before asserting on side effects like file creation
38
+ - `Naming/AccessorMethodName` cop flags `get_`/`set_` prefixes disable inline when API names are mandated by the PRD
39
+ - `add_profile` accepts both `database_name` and `database` keys for compatibility with existing config hash shapes
39
40
  ---
40
41
 
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
42
+ ## 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
52
+ - Files changed:
53
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/launcher.rb` (modified)
54
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (added database setting)
55
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb` (added requires)
56
+ - `gems/mysql_genius-desktop/spec/rack_helper.rb` (inject test Database)
57
+ - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/launcher_spec.rb` (updated)
52
58
  - **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
59
+ - `open_database` extracted as private method so specs can stub it without touching filesystem
60
+ - Import logic checks DB emptiness, not config emptiness — deleting mysql_genius.db forces re-import
61
+ - Profile import maps `mysql.database` `database_name` for Database schema compatibility
62
+ - rack_helper cleanup: must `FileUtils.remove_entry` tmpdir in after block to avoid leftover test DBs
57
63
  ---
58
64
 
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
65
+ ## 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
75
+ - Files changed:
76
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (rewired routes, removed ProfileManager require)
77
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb` (added switch_to_config method)
78
+ - `gems/mysql_genius-desktop/spec/requests/profiles_api_spec.rb` (rewrote for Database)
66
79
  - **Learnings for future iterations:**
67
- - QueryStats spec uses positional arrays for row datawhen 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
80
+ - Database returns `database_name` key but `MysqlConfig.from_hash` expects `database`need `mysql_hash_from_profile` helper to remap
81
+ - Profile API response format must be `{name:, mysql: {host:, port:, database:, ...}}` to match frontend expectations (connections.html.erb)
82
+ - `SessionSwapper.switch_to_config` allows callers that already have a MysqlConfig to skip the config lookup
83
+ - Active profile delete check stays in route handler, not in Database (Database.delete_profile has no concept of "active")
69
84
  ---
70
85
 
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
86
+ ## 2026-04-13 - US-005
87
+ - What was implemented:
88
+ - Changed `SessionSwapper.initialize` to accept `(app_class, config, database)` instead of `(app_class, config)`
89
+ - Rewired `switch_to` to look up profiles via `@database.find_profile(name)` instead of `@config.profile_by_name(name)`
90
+ - Added `mysql_hash_from_profile` private helper to remap `database_name` `database` for `MysqlConfig.from_hash`
91
+ - Changed `switch_to_config` to create `SqliteStatsHistory.new(@database)` instead of in-memory `StatsHistory.new`
92
+ - Updated the caller in `app.rb` connect route to pass `settings.database` as third arg
93
+ - Rewrote `session_swapper_spec.rb` to use a tmpdir-based Database instead of Config profile lookup
94
+ - Files changed:
95
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/session_swapper.rb` (rewired to use Database)
96
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/app.rb` (updated SessionSwapper.new call)
97
+ - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/session_swapper_spec.rb` (rewrote for Database)
84
98
  - **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
99
+ - `RSpec/MultipleMemoizedHelpers` max is 5 (subject excluded via AllowSubject). Inline tmpdir into `let(:database)` via `Dir.mktmpdir` to avoid extra helper
100
+ - `mysql_hash_from_profile` is duplicated between `app.rb` and `session_swapper.rb` could be extracted to a shared module in US-006 cleanup if desired
101
+ - `switch_to_config` is still used by the connect route in `app.rb` which pre-builds the MysqlConfig — `switch_to` is for callers that only have a profile name
90
102
  ---
91
103
 
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
104
+ ## 2026-04-13 - US-006
105
+ - What was implemented:
106
+ - Deleted `profile_manager.rb` source file
107
+ - Deleted `profile_manager_spec.rb` spec file
108
+ - Removed `require "mysql_genius/desktop/profile_manager"` from `desktop.rb`
109
+ - Verified no remaining `ProfileManager` or `profile_manager` references in the desktop gem
110
+ - Confirmed route handlers already rescue `Database::DuplicateProfileError` / `Database::ProfileNotFoundError` (not ProfileManager versions)
111
+ - Files changed:
112
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop/profile_manager.rb` (deleted)
113
+ - `gems/mysql_genius-desktop/spec/mysql_genius/desktop/profile_manager_spec.rb` (deleted)
114
+ - `gems/mysql_genius-desktop/lib/mysql_genius/desktop.rb` (removed require line)
99
115
  - **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)
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
102
119
  ---
103
120
 
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
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)
116
135
  - **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
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)
141
138
  ---
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.7.2
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.7.2
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.7.2
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: railties
49
49
  requirement: !ruby/object:Gem::Requirement