mysql_genius 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +5 -0
  3. data/.github/workflows/ci.yml +30 -7
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +24 -0
  6. data/CHANGELOG.md +32 -0
  7. data/Gemfile +7 -2
  8. data/README.md +50 -38
  9. data/Rakefile +3 -1
  10. data/app/controllers/concerns/mysql_genius/ai_features.rb +90 -52
  11. data/app/controllers/concerns/mysql_genius/database_analysis.rb +73 -45
  12. data/app/controllers/concerns/mysql_genius/query_execution.rb +18 -16
  13. data/app/controllers/mysql_genius/base_controller.rb +3 -1
  14. data/app/controllers/mysql_genius/queries_controller.rb +19 -12
  15. data/app/services/mysql_genius/ai_client.rb +10 -3
  16. data/app/services/mysql_genius/ai_optimization_service.rb +8 -4
  17. data/app/services/mysql_genius/ai_suggestion_service.rb +6 -3
  18. data/app/views/layouts/mysql_genius/application.html.erb +141 -5
  19. data/app/views/mysql_genius/queries/_tab_dashboard.html.erb +95 -0
  20. data/app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +11 -0
  21. data/app/views/mysql_genius/queries/_tab_query_explorer.html.erb +110 -0
  22. data/app/views/mysql_genius/queries/_tab_table_sizes.html.erb +6 -4
  23. data/app/views/mysql_genius/queries/_tab_unused_indexes.html.erb +11 -0
  24. data/app/views/mysql_genius/queries/index.html.erb +377 -52
  25. data/bin/console +1 -0
  26. data/config/routes.rb +2 -0
  27. data/docs/screenshots/dashboard.png +0 -0
  28. data/docs/screenshots/query_explore.png +0 -0
  29. data/docs/superpowers/plans/2026-04-08-dashboard-first-redesign.md +741 -0
  30. data/docs/superpowers/specs/2026-04-08-dashboard-first-redesign.md +87 -0
  31. data/lib/generators/mysql_genius/install/install_generator.rb +19 -0
  32. data/lib/generators/mysql_genius/install/templates/initializer.rb +56 -0
  33. data/lib/mysql_genius/configuration.rb +8 -6
  34. data/lib/mysql_genius/engine.rb +2 -0
  35. data/lib/mysql_genius/slow_query_monitor.rb +30 -25
  36. data/lib/mysql_genius/sql_validator.rb +6 -4
  37. data/lib/mysql_genius/version.rb +3 -1
  38. data/lib/mysql_genius.rb +2 -0
  39. data/mysql_genius.gemspec +9 -8
  40. metadata +31 -15
  41. data/app/views/mysql_genius/queries/_tab_sql_query.html.erb +0 -40
  42. data/app/views/mysql_genius/queries/_tab_visual_builder.html.erb +0 -61
  43. data/docs/screenshots/sql_query.png +0 -0
  44. data/docs/screenshots/visual_builder.png +0 -0
@@ -0,0 +1,741 @@
1
+ # Dashboard-First Redesign Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the default landing page a PgHero-style monitoring dashboard instead of a query builder, reorder tabs to lead with monitoring, and merge Visual Builder + SQL Query into one "Query Explorer" tab.
6
+
7
+ **Architecture:** The dashboard tab is a new partial that reuses all existing AJAX endpoints (server_overview, slow_queries, query_stats, duplicate_indexes, unused_indexes) with no new routes. The query explorer merges two existing partials into one with a mode toggle. The only backend change is adding a `limit` param to the `query_stats` action.
8
+
9
+ **Tech Stack:** Rails ERB partials, vanilla JavaScript (matching existing patterns), existing CSS utility classes.
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | File | Action | Responsibility |
16
+ |------|--------|---------------|
17
+ | `app/controllers/concerns/mysql_genius/database_analysis.rb` | Modify | Add `limit` param to `query_stats` |
18
+ | `app/views/mysql_genius/queries/_tab_dashboard.html.erb` | Create | Dashboard HTML structure |
19
+ | `app/views/mysql_genius/queries/_tab_query_explorer.html.erb` | Create | Merged visual builder + SQL query with mode toggle |
20
+ | `app/views/mysql_genius/queries/index.html.erb` | Modify | Tab bar reorder, partial renders, JS for dashboard + query explorer |
21
+ | `app/views/mysql_genius/queries/_tab_visual_builder.html.erb` | Delete | Content moved to query explorer |
22
+ | `app/views/mysql_genius/queries/_tab_sql_query.html.erb` | Delete | Content moved to query explorer |
23
+ | `mysql_genius.gemspec` | Modify | Update description |
24
+ | `README.md` | Modify | Update usage section and screenshot order |
25
+
26
+ ---
27
+
28
+ ### Task 1: Add `limit` param to `query_stats` action
29
+
30
+ **Files:**
31
+ - Modify: `app/controllers/concerns/mysql_genius/database_analysis.rb:112`
32
+
33
+ - [ ] **Step 1: Add limit param support**
34
+
35
+ In `app/controllers/concerns/mysql_genius/database_analysis.rb`, change the hardcoded `LIMIT 50` to use a param with a cap:
36
+
37
+ ```ruby
38
+ limit = [params.fetch(:limit, 50).to_i, 50].min
39
+
40
+ results = connection.exec_query(<<~SQL)
41
+ SELECT
42
+ DIGEST_TEXT,
43
+ COUNT_STAR AS calls,
44
+ ROUND(SUM_TIMER_WAIT / 1000000000, 1) AS total_time_ms,
45
+ ROUND(AVG_TIMER_WAIT / 1000000000, 1) AS avg_time_ms,
46
+ ROUND(MAX_TIMER_WAIT / 1000000000, 1) AS max_time_ms,
47
+ SUM_ROWS_EXAMINED AS rows_examined,
48
+ SUM_ROWS_SENT AS rows_sent,
49
+ SUM_CREATED_TMP_DISK_TABLES AS tmp_disk_tables,
50
+ SUM_SORT_ROWS AS sort_rows,
51
+ FIRST_SEEN,
52
+ LAST_SEEN
53
+ FROM performance_schema.events_statements_summary_by_digest
54
+ WHERE SCHEMA_NAME = #{connection.quote(connection.current_database)}
55
+ AND DIGEST_TEXT IS NOT NULL
56
+ AND DIGEST_TEXT NOT LIKE 'EXPLAIN%'
57
+ ORDER BY #{order_clause}
58
+ LIMIT #{limit}
59
+ SQL
60
+ ```
61
+
62
+ The key change: replace `LIMIT 50` (line 112) with `LIMIT #{limit}` and add the `limit` local variable before the query. The `[..., 50].min` cap ensures the dashboard's `limit=5` works while the full Query Stats tab still gets up to 50.
63
+
64
+ - [ ] **Step 2: Run RuboCop to verify**
65
+
66
+ Run: `bundle exec rubocop app/controllers/concerns/mysql_genius/database_analysis.rb`
67
+ Expected: no offenses
68
+
69
+ - [ ] **Step 3: Commit**
70
+
71
+ ```bash
72
+ git add app/controllers/concerns/mysql_genius/database_analysis.rb
73
+ git commit -m "Add limit param to query_stats action for dashboard use"
74
+ ```
75
+
76
+ ---
77
+
78
+ ### Task 2: Create the Dashboard tab partial
79
+
80
+ **Files:**
81
+ - Create: `app/views/mysql_genius/queries/_tab_dashboard.html.erb`
82
+
83
+ - [ ] **Step 1: Create the dashboard partial**
84
+
85
+ Create `app/views/mysql_genius/queries/_tab_dashboard.html.erb` with this content:
86
+
87
+ ```erb
88
+ <!-- Dashboard Tab -->
89
+ <div class="mg-tab-content active" id="tab-dashboard">
90
+ <div id="dash-loading" class="mg-text-center"><span class="mg-spinner"></span> Loading dashboard...</div>
91
+ <div id="dash-error" class="mg-hidden"></div>
92
+ <div id="dash-content" class="mg-hidden">
93
+
94
+ <!-- Server Summary -->
95
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-bottom:16px;">
96
+ <div class="mg-card">
97
+ <div class="mg-card-header"><strong>Server</strong></div>
98
+ <div class="mg-card-body">
99
+ <div class="mg-stat-grid" id="dash-server-info"></div>
100
+ </div>
101
+ </div>
102
+ <div class="mg-card">
103
+ <div class="mg-card-header"><strong>Connections</strong></div>
104
+ <div class="mg-card-body">
105
+ <div id="dash-conn-bar" style="margin-bottom:8px;"></div>
106
+ <div class="mg-stat-grid" id="dash-conn-info"></div>
107
+ </div>
108
+ </div>
109
+ <div class="mg-card">
110
+ <div class="mg-card-header"><strong>InnoDB Buffer Pool</strong></div>
111
+ <div class="mg-card-body">
112
+ <div id="dash-innodb-bar" style="margin-bottom:8px;"></div>
113
+ <div class="mg-stat-grid" id="dash-innodb-info"></div>
114
+ </div>
115
+ </div>
116
+ <div class="mg-card">
117
+ <div class="mg-card-header"><strong>Query Activity</strong></div>
118
+ <div class="mg-card-body">
119
+ <div class="mg-stat-grid" id="dash-query-info"></div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Top 5 Slow Queries -->
125
+ <div class="mg-card mg-mb">
126
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
127
+ <strong>Slow Queries</strong>
128
+ <button class="mg-btn mg-btn-outline-secondary mg-btn-sm dash-jump-tab" data-target="slow">View all &rarr;</button>
129
+ </div>
130
+ <div class="mg-card-body">
131
+ <div id="dash-slow-empty" class="mg-text-muted mg-hidden"></div>
132
+ <div id="dash-slow-table" class="mg-table-wrap mg-hidden">
133
+ <table class="mg-table">
134
+ <thead><tr><th style="width:100px">Duration</th><th style="width:160px">Time</th><th>SQL</th></tr></thead>
135
+ <tbody id="dash-slow-tbody"></tbody>
136
+ </table>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Top 5 Expensive Queries -->
142
+ <div class="mg-card mg-mb">
143
+ <div class="mg-card-header" style="display:flex;justify-content:space-between;align-items:center;">
144
+ <strong>Most Expensive Queries</strong>
145
+ <button class="mg-btn mg-btn-outline-secondary mg-btn-sm dash-jump-tab" data-target="qstats">View all &rarr;</button>
146
+ </div>
147
+ <div class="mg-card-body">
148
+ <div id="dash-qstats-empty" class="mg-text-muted mg-hidden"></div>
149
+ <div id="dash-qstats-error" class="mg-hidden"></div>
150
+ <div id="dash-qstats-table" class="mg-table-wrap mg-hidden">
151
+ <table class="mg-table">
152
+ <thead><tr><th>Query</th><th style="text-align:right">Calls</th><th style="text-align:right">Total Time</th><th style="text-align:right">Avg Time</th></tr></thead>
153
+ <tbody id="dash-qstats-tbody"></tbody>
154
+ </table>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Index Alerts -->
160
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;">
161
+ <div class="mg-card dash-jump-tab" data-target="indexes" style="cursor:pointer;">
162
+ <div class="mg-card-body mg-text-center">
163
+ <div id="dash-dup-count" style="font-size:24px;font-weight:700;">--</div>
164
+ <div class="mg-text-muted">Duplicate Indexes</div>
165
+ </div>
166
+ </div>
167
+ <div class="mg-card dash-jump-tab" data-target="unused" style="cursor:pointer;">
168
+ <div class="mg-card-body mg-text-center">
169
+ <div id="dash-unused-count" style="font-size:24px;font-weight:700;">--</div>
170
+ <div class="mg-text-muted">Unused Indexes</div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ </div>
176
+ </div>
177
+ ```
178
+
179
+ - [ ] **Step 2: Verify file renders valid HTML**
180
+
181
+ Run: `ruby -e "puts File.read('app/views/mysql_genius/queries/_tab_dashboard.html.erb').length"` from project root.
182
+ Expected: prints a number (file exists and is readable)
183
+
184
+ - [ ] **Step 3: Commit**
185
+
186
+ ```bash
187
+ git add app/views/mysql_genius/queries/_tab_dashboard.html.erb
188
+ git commit -m "Add dashboard tab partial with server summary, top queries, and index alerts"
189
+ ```
190
+
191
+ ---
192
+
193
+ ### Task 3: Create the Query Explorer tab partial
194
+
195
+ **Files:**
196
+ - Create: `app/views/mysql_genius/queries/_tab_query_explorer.html.erb`
197
+
198
+ - [ ] **Step 1: Create the query explorer partial**
199
+
200
+ Create `app/views/mysql_genius/queries/_tab_query_explorer.html.erb`. This combines the content from `_tab_visual_builder.html.erb` and `_tab_sql_query.html.erb` with a mode toggle. The visual builder content keeps its existing `id="tab-visual"` inner div (now `id="qe-visual"`) and the SQL content keeps `id="tab-sql"` inner div (now `id="qe-sql"`) so existing JS references work after updating.
201
+
202
+ ```erb
203
+ <!-- Query Explorer Tab -->
204
+ <div class="mg-tab-content" id="tab-explorer">
205
+ <div style="margin-bottom:12px;">
206
+ <button class="mg-btn mg-btn-sm qe-mode active" data-mode="visual">Visual Builder</button>
207
+ <button class="mg-btn mg-btn-sm mg-btn-outline qe-mode" data-mode="sql">SQL Editor</button>
208
+ </div>
209
+
210
+ <!-- Visual Builder Mode -->
211
+ <div id="qe-visual">
212
+ <div class="mg-row mg-mb">
213
+ <div class="mg-col-4 mg-field">
214
+ <label for="vb-table">Table</label>
215
+ <select id="vb-table">
216
+ <option value="">-- Select a table --</option>
217
+ <% if @featured_tables != @all_tables %>
218
+ <optgroup label="Featured">
219
+ <% @featured_tables.each do |table| %>
220
+ <option value="<%= table %>"><%= table %></option>
221
+ <% end %>
222
+ </optgroup>
223
+ <optgroup label="All Tables">
224
+ <% (@all_tables - @featured_tables).each do |table| %>
225
+ <option value="<%= table %>"><%= table %></option>
226
+ <% end %>
227
+ </optgroup>
228
+ <% else %>
229
+ <% @all_tables.each do |table| %>
230
+ <option value="<%= table %>"><%= table %></option>
231
+ <% end %>
232
+ <% end %>
233
+ </select>
234
+ </div>
235
+ <div class="mg-col-2 mg-field">
236
+ <label for="vb-row-limit">Row Limit</label>
237
+ <input type="number" id="vb-row-limit" value="25" min="1" max="<%= MysqlGenius.configuration.max_row_limit %>">
238
+ </div>
239
+ <div class="mg-field">
240
+ <label>&nbsp;</label>
241
+ <button id="vb-run" class="mg-btn mg-btn-primary" disabled>&#9654; Run Query</button>
242
+ <button id="vb-explain" class="mg-btn mg-btn-outline" disabled>&#128270; Explain</button>
243
+ </div>
244
+ </div>
245
+
246
+ <div id="vb-columns-section" class="mg-mb mg-hidden">
247
+ <label>Columns
248
+ <span id="vb-toggle-all" class="mg-link">Toggle All</span>
249
+ <span id="vb-show-defaults" class="mg-link">Reset Defaults</span>
250
+ </label>
251
+ <div id="vb-columns" class="mg-checks"></div>
252
+ </div>
253
+
254
+ <div id="vb-filters-section" class="mg-mb mg-hidden">
255
+ <label>Filters</label>
256
+ <div id="vb-filters"></div>
257
+ <button id="vb-add-filter" class="mg-btn mg-btn-outline-secondary mg-btn-sm" style="margin-top:4px;">+ Add Filter</button>
258
+ </div>
259
+
260
+ <div id="vb-order-section" class="mg-mb mg-hidden">
261
+ <label>Order By</label>
262
+ <div id="vb-orders"></div>
263
+ <button id="vb-add-order" class="mg-btn mg-btn-outline-secondary mg-btn-sm" style="margin-top:4px;">+ Add Sort</button>
264
+ </div>
265
+
266
+ <div id="vb-generated-sql" class="mg-mb mg-hidden">
267
+ <label>Generated SQL</label>
268
+ <textarea id="vb-sql-preview" rows="2" readonly></textarea>
269
+ </div>
270
+ </div>
271
+
272
+ <!-- SQL Editor Mode -->
273
+ <div id="qe-sql" class="mg-hidden">
274
+ <% if @ai_enabled %>
275
+ <div class="mg-card mg-mb">
276
+ <div class="mg-card-header">
277
+ <span class="mg-card-toggle" id="ai-toggle">&#9889; AI Assistant <span class="mg-text-muted">- Describe what you want in plain English</span></span>
278
+ </div>
279
+ <div class="mg-card-body mg-hidden" id="ai-panel">
280
+ <div class="mg-field">
281
+ <textarea id="ai-prompt" rows="2" placeholder="e.g. Show me all users created in the last 30 days"></textarea>
282
+ <div class="mg-text-muted" style="margin-top:2px;">Only table names and column names are sent to the AI. No row data leaves the system.</div>
283
+ </div>
284
+ <button id="ai-suggest" class="mg-btn mg-btn-primary mg-btn-sm mg-mb">&#9889; Suggest Query</button>
285
+ <div id="ai-result" class="mg-hidden">
286
+ <div id="ai-explanation" class="mg-alert mg-alert-info"></div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ <% end %>
291
+
292
+ <div class="mg-field">
293
+ <label for="sql-input">SQL Query</label>
294
+ <textarea id="sql-input" rows="5" placeholder="SELECT * FROM users LIMIT 10"></textarea>
295
+ </div>
296
+ <div class="mg-row">
297
+ <div class="mg-col-2 mg-field">
298
+ <label for="sql-row-limit">Row Limit</label>
299
+ <input type="number" id="sql-row-limit" value="25" min="1" max="<%= MysqlGenius.configuration.max_row_limit %>">
300
+ </div>
301
+ <div class="mg-field">
302
+ <label>&nbsp;</label>
303
+ <button id="sql-run" class="mg-btn mg-btn-primary">&#9654; Run Query</button>
304
+ <button id="sql-explain" class="mg-btn mg-btn-outline">&#128270; Explain</button>
305
+ <% if @ai_enabled %>
306
+ <button id="sql-describe" class="mg-btn mg-btn-outline mg-btn-sm" style="margin-left:8px;">&#9889; Describe</button>
307
+ <button id="sql-rewrite" class="mg-btn mg-btn-outline mg-btn-sm">&#9889; Rewrite</button>
308
+ <% end %>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ ```
314
+
315
+ - [ ] **Step 2: Commit**
316
+
317
+ ```bash
318
+ git add app/views/mysql_genius/queries/_tab_query_explorer.html.erb
319
+ git commit -m "Add query explorer partial merging visual builder and SQL editor"
320
+ ```
321
+
322
+ ---
323
+
324
+ ### Task 4: Update index.html.erb — tab bar and partials
325
+
326
+ **Files:**
327
+ - Modify: `app/views/mysql_genius/queries/index.html.erb:1-29`
328
+ - Delete: `app/views/mysql_genius/queries/_tab_visual_builder.html.erb`
329
+ - Delete: `app/views/mysql_genius/queries/_tab_sql_query.html.erb`
330
+
331
+ - [ ] **Step 1: Replace the tab bar and partial renders**
332
+
333
+ Replace lines 1-29 of `app/views/mysql_genius/queries/index.html.erb` (the header, tab buttons, and partial renders) with:
334
+
335
+ ```erb
336
+ <h4>&#128024; MySQLGenius</h4>
337
+
338
+ <div class="mg-tabs">
339
+ <button class="mg-tab active" data-tab="dashboard">Dashboard</button>
340
+ <button class="mg-tab" data-tab="slow">Slow Queries</button>
341
+ <button class="mg-tab" data-tab="qstats">Query Stats</button>
342
+ <button class="mg-tab" data-tab="server">Server</button>
343
+ <button class="mg-tab" data-tab="sizes">Table Sizes</button>
344
+ <button class="mg-tab" data-tab="unused">Unused Indexes</button>
345
+ <button class="mg-tab" data-tab="indexes">Duplicate Indexes</button>
346
+ <button class="mg-tab" data-tab="explorer">Query Explorer</button>
347
+ <% if @ai_enabled %>
348
+ <button class="mg-tab" data-tab="aitools">AI Tools</button>
349
+ <% end %>
350
+ </div>
351
+
352
+ <%= render "mysql_genius/queries/tab_dashboard" %>
353
+ <%= render "mysql_genius/queries/tab_slow_queries" %>
354
+ <%= render "mysql_genius/queries/tab_query_stats" %>
355
+ <%= render "mysql_genius/queries/tab_server" %>
356
+ <%= render "mysql_genius/queries/tab_table_sizes" %>
357
+ <%= render "mysql_genius/queries/tab_unused_indexes" %>
358
+ <%= render "mysql_genius/queries/tab_duplicate_indexes" %>
359
+ <%= render "mysql_genius/queries/tab_query_explorer" %>
360
+ <% if @ai_enabled %>
361
+ <%= render "mysql_genius/queries/tab_ai_tools" %>
362
+ <% end %>
363
+
364
+ <%= render "mysql_genius/queries/shared_results" %>
365
+ ```
366
+
367
+ - [ ] **Step 2: Delete the old partials**
368
+
369
+ ```bash
370
+ rm app/views/mysql_genius/queries/_tab_visual_builder.html.erb
371
+ rm app/views/mysql_genius/queries/_tab_sql_query.html.erb
372
+ ```
373
+
374
+ - [ ] **Step 3: Commit**
375
+
376
+ ```bash
377
+ git add app/views/mysql_genius/queries/index.html.erb
378
+ git add app/views/mysql_genius/queries/_tab_visual_builder.html.erb
379
+ git add app/views/mysql_genius/queries/_tab_sql_query.html.erb
380
+ git commit -m "Reorder tabs to lead with dashboard, remove old visual builder and SQL partials"
381
+ ```
382
+
383
+ ---
384
+
385
+ ### Task 5: Update JavaScript — tab switching, dashboard loader, query explorer toggle
386
+
387
+ **Files:**
388
+ - Modify: `app/views/mysql_genius/queries/index.html.erb` (JS section)
389
+
390
+ - [ ] **Step 1: Update the tab click handler**
391
+
392
+ Replace the tab click handler (lines 120-133) with:
393
+
394
+ ```javascript
395
+ qsa('.mg-tab').forEach(function(tab) {
396
+ tab.addEventListener('click', function() {
397
+ qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
398
+ qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
399
+ tab.classList.add('active');
400
+ el('tab-' + tab.dataset.tab).classList.add('active');
401
+ if (tab.dataset.tab === 'dashboard') loadDashboard();
402
+ if (tab.dataset.tab === 'slow') loadSlowQueries();
403
+ if (tab.dataset.tab === 'indexes') loadDuplicateIndexes();
404
+ if (tab.dataset.tab === 'sizes') loadTableSizes();
405
+ if (tab.dataset.tab === 'qstats') loadQueryStats();
406
+ if (tab.dataset.tab === 'unused') loadUnusedIndexes();
407
+ if (tab.dataset.tab === 'server') loadServerOverview();
408
+ });
409
+ });
410
+ ```
411
+
412
+ - [ ] **Step 2: Add the `loadDashboard` function**
413
+
414
+ Insert this function before the `// --- Visual Builder ---` comment (around line 135). This function fires 4 parallel AJAX calls and populates the dashboard sections:
415
+
416
+ ```javascript
417
+ // --- Dashboard ---
418
+
419
+ function loadDashboard() {
420
+ show(el('dash-loading'));
421
+ hide(el('dash-content')); hide(el('dash-error'));
422
+
423
+ var loaded = { server: false, slow: false, qstats: false, dup: false, unused: false };
424
+
425
+ function checkAllLoaded() {
426
+ if (!loaded.server || !loaded.slow || !loaded.qstats || !loaded.dup || !loaded.unused) return;
427
+ hide(el('dash-loading'));
428
+ show(el('dash-content'));
429
+ }
430
+
431
+ // Server overview
432
+ ajaxGet(ROUTES.server_overview, {}, function(data) {
433
+ if (data.error) { loaded.server = true; checkAllLoaded(); return; }
434
+ var s = data.server;
435
+ var c = data.connections;
436
+ var db = data.innodb;
437
+ var q = data.queries;
438
+
439
+ el('dash-server-info').innerHTML =
440
+ statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
441
+ statRow('Uptime', escHtml(s.uptime)) +
442
+ statRow('Queries/sec', q.qps);
443
+
444
+ el('dash-conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
445
+ el('dash-conn-info').innerHTML =
446
+ statRow('Threads Running', c.threads_running) +
447
+ statRow('Max Used', c.max_used);
448
+
449
+ var poolUsedPct = db.buffer_pool_pages_total > 0
450
+ ? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
451
+ : 0;
452
+ el('dash-innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
453
+ el('dash-innodb-info').innerHTML =
454
+ statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
455
+ statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString());
456
+
457
+ var tmpBadge = q.tmp_disk_pct > 25
458
+ ? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
459
+ : q.tmp_disk_pct + '%';
460
+ el('dash-query-info').innerHTML =
461
+ statRow('Slow Queries', Number(q.slow_queries).toLocaleString()) +
462
+ statRow('Tmp Disk Tables', tmpBadge);
463
+
464
+ loaded.server = true;
465
+ checkAllLoaded();
466
+ }, function() { loaded.server = true; checkAllLoaded(); });
467
+
468
+ // Top 5 slow queries
469
+ ajaxGet(ROUTES.slow_queries, {}, function(data) {
470
+ if (!data || !data.length) {
471
+ el('dash-slow-empty').textContent = '<%= MysqlGenius.configuration.redis_url ? "No slow queries recorded." : "Configure redis_url to monitor slow queries." %>';
472
+ show(el('dash-slow-empty'));
473
+ } else {
474
+ var top5 = data.slice(0, 5);
475
+ el('dash-slow-tbody').innerHTML = top5.map(function(q) {
476
+ var d = q.duration_ms;
477
+ var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
478
+ var sqlShort = escHtml(q.sql).length > 120 ? escHtml(q.sql).substring(0, 120) + '...' : escHtml(q.sql);
479
+ return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
480
+ '<td><small>' + escHtml(q.timestamp) + '</small></td>' +
481
+ '<td><code>' + sqlShort + '</code></td></tr>';
482
+ }).join('');
483
+ show(el('dash-slow-table'));
484
+ }
485
+ loaded.slow = true;
486
+ checkAllLoaded();
487
+ }, function() {
488
+ el('dash-slow-empty').textContent = 'Configure redis_url to monitor slow queries.';
489
+ show(el('dash-slow-empty'));
490
+ loaded.slow = true;
491
+ checkAllLoaded();
492
+ });
493
+
494
+ // Top 5 expensive queries
495
+ ajaxGet(ROUTES.query_stats, { sort: 'total_time', limit: 5 }, function(data) {
496
+ if (data.error) {
497
+ el('dash-qstats-error').innerHTML = '<div class="mg-text-muted">' + escHtml(data.error) + '</div>';
498
+ show(el('dash-qstats-error'));
499
+ } else if (!data.length) {
500
+ el('dash-qstats-empty').textContent = 'No query statistics available.';
501
+ show(el('dash-qstats-empty'));
502
+ } else {
503
+ el('dash-qstats-tbody').innerHTML = data.map(function(q) {
504
+ var sqlShort = q.sql.length > 120 ? q.sql.substring(0, 120) + '...' : q.sql;
505
+ return '<tr>' +
506
+ '<td><code style="font-size:11px;word-break:break-all;">' + escHtml(sqlShort) + '</code></td>' +
507
+ '<td style="text-align:right">' + Number(q.calls).toLocaleString() + '</td>' +
508
+ '<td style="text-align:right">' + formatDuration(q.total_time_ms) + '</td>' +
509
+ '<td style="text-align:right">' + formatDuration(q.avg_time_ms) + '</td></tr>';
510
+ }).join('');
511
+ show(el('dash-qstats-table'));
512
+ }
513
+ loaded.qstats = true;
514
+ checkAllLoaded();
515
+ }, function() {
516
+ el('dash-qstats-empty').textContent = 'Query statistics unavailable.';
517
+ show(el('dash-qstats-empty'));
518
+ loaded.qstats = true;
519
+ checkAllLoaded();
520
+ });
521
+
522
+ // Duplicate indexes count
523
+ ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
524
+ var count = Array.isArray(data) ? data.length : 0;
525
+ var countEl = el('dash-dup-count');
526
+ if (count === 0) {
527
+ countEl.innerHTML = '<span style="color:#28a745;">&#10003; 0</span>';
528
+ } else {
529
+ countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
530
+ }
531
+ loaded.dup = true;
532
+ checkAllLoaded();
533
+ }, function() { el('dash-dup-count').textContent = '?'; loaded.dup = true; checkAllLoaded(); });
534
+
535
+ // Unused indexes count
536
+ ajaxGet(ROUTES.unused_indexes, {}, function(data) {
537
+ var count = Array.isArray(data) ? data.length : 0;
538
+ var countEl = el('dash-unused-count');
539
+ if (count === 0) {
540
+ countEl.innerHTML = '<span style="color:#28a745;">&#10003; 0</span>';
541
+ } else {
542
+ countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
543
+ }
544
+ loaded.unused = true;
545
+ checkAllLoaded();
546
+ }, function() { el('dash-unused-count').textContent = '?'; loaded.unused = true; checkAllLoaded(); });
547
+ }
548
+
549
+ // Dashboard tab-jump buttons
550
+ document.addEventListener('click', function(e) {
551
+ var jumpBtn = e.target.closest('.dash-jump-tab');
552
+ if (jumpBtn) {
553
+ var target = jumpBtn.dataset.target;
554
+ qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
555
+ qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
556
+ qs('[data-tab="' + target + '"]').classList.add('active');
557
+ el('tab-' + target).classList.add('active');
558
+ if (target === 'slow') loadSlowQueries();
559
+ if (target === 'indexes') loadDuplicateIndexes();
560
+ if (target === 'qstats') loadQueryStats();
561
+ if (target === 'unused') loadUnusedIndexes();
562
+ }
563
+ });
564
+
565
+ // Load dashboard on page load
566
+ loadDashboard();
567
+ ```
568
+
569
+ - [ ] **Step 3: Add the query explorer mode toggle**
570
+
571
+ Insert this after the dashboard section, before the existing visual builder JS (the `var typeLabels = ...` line):
572
+
573
+ ```javascript
574
+ // --- Query Explorer Mode Toggle ---
575
+
576
+ qsa('.qe-mode').forEach(function(btn) {
577
+ btn.addEventListener('click', function() {
578
+ qsa('.qe-mode').forEach(function(b) {
579
+ b.classList.remove('active');
580
+ b.classList.add('mg-btn-outline');
581
+ });
582
+ btn.classList.add('active');
583
+ btn.classList.remove('mg-btn-outline');
584
+ if (btn.dataset.mode === 'visual') {
585
+ show(el('qe-visual'));
586
+ hide(el('qe-sql'));
587
+ } else {
588
+ hide(el('qe-visual'));
589
+ show(el('qe-sql'));
590
+ }
591
+ });
592
+ });
593
+ ```
594
+
595
+ - [ ] **Step 4: Update the slow query "Use" button handler**
596
+
597
+ Find the existing handler (around line 522-529) that switches to the SQL tab when clicking "Use" on a slow query. Update it to switch to the Query Explorer tab in SQL mode instead:
598
+
599
+ Change:
600
+ ```javascript
601
+ qs('[data-tab="sql"]').classList.add('active');
602
+ el('tab-sql').classList.add('active');
603
+ ```
604
+
605
+ To:
606
+ ```javascript
607
+ qs('[data-tab="explorer"]').classList.add('active');
608
+ el('tab-explorer').classList.add('active');
609
+ // Switch to SQL mode within Query Explorer
610
+ qsa('.qe-mode').forEach(function(b) { b.classList.remove('active'); b.classList.add('mg-btn-outline'); });
611
+ qs('.qe-mode[data-mode="sql"]').classList.add('active');
612
+ qs('.qe-mode[data-mode="sql"]').classList.remove('mg-btn-outline');
613
+ hide(el('qe-visual'));
614
+ show(el('qe-sql'));
615
+ ```
616
+
617
+ - [ ] **Step 5: Run specs and RuboCop**
618
+
619
+ Run: `bundle exec rspec && bundle exec rubocop`
620
+ Expected: all specs pass, no offenses
621
+
622
+ - [ ] **Step 6: Commit**
623
+
624
+ ```bash
625
+ git add app/views/mysql_genius/queries/index.html.erb
626
+ git commit -m "Add dashboard JS loader, query explorer toggle, and auto-load on page mount"
627
+ ```
628
+
629
+ ---
630
+
631
+ ### Task 6: Update gemspec description
632
+
633
+ **Files:**
634
+ - Modify: `mysql_genius.gemspec:14-16`
635
+
636
+ - [ ] **Step 1: Update the description**
637
+
638
+ Change the description in `mysql_genius.gemspec` from:
639
+
640
+ ```ruby
641
+ spec.description = "MysqlGenius gives Rails apps a mountable admin dashboard for MySQL databases. " \
642
+ "Includes a safe SQL query explorer with visual builder, EXPLAIN analysis, " \
643
+ "slow query monitoring, audit logging, and optional AI-powered query suggestions and optimization."
644
+ ```
645
+
646
+ To:
647
+
648
+ ```ruby
649
+ spec.description = "MysqlGenius gives Rails apps a mountable performance dashboard for MySQL databases. " \
650
+ "Monitor slow queries, analyze query statistics from performance_schema, detect unused and duplicate indexes, " \
651
+ "and explore your database with optional AI-powered optimization."
652
+ ```
653
+
654
+ - [ ] **Step 2: Commit**
655
+
656
+ ```bash
657
+ git add mysql_genius.gemspec
658
+ git commit -m "Update gemspec description to lead with monitoring features"
659
+ ```
660
+
661
+ ---
662
+
663
+ ### Task 7: Update README
664
+
665
+ **Files:**
666
+ - Modify: `README.md`
667
+
668
+ - [ ] **Step 1: Reorder screenshots section**
669
+
670
+ Move the screenshots section so Dashboard is first. Replace the current screenshots block (lines 6-41) with:
671
+
672
+ ```markdown
673
+ ### Dashboard
674
+ At-a-glance server health, top slow queries, most expensive queries, and index alerts.
675
+
676
+ ![Dashboard](docs/screenshots/dashboard.png)
677
+
678
+ ### Slow Queries
679
+ SELECT queries exceeding the configured threshold, captured via ActiveSupport notifications and Redis.
680
+
681
+ ### Query Stats
682
+ Top queries from `performance_schema` sorted by total time, with call counts, avg/max time, and rows examined.
683
+
684
+ ![Query Stats](docs/screenshots/query_stats.png)
685
+
686
+ ### Server Dashboard
687
+ Server health: version, connections, InnoDB buffer pool, and query activity with AI-powered diagnostics.
688
+
689
+ ![Server](docs/screenshots/server.png)
690
+
691
+ ### Table Sizes
692
+ View row counts, data size, index size, fragmentation, and a visual size chart for every table.
693
+
694
+ ![Table Sizes](docs/screenshots/table_sizes.png)
695
+
696
+ ### Duplicate Index Detection
697
+ Find redundant indexes whose columns are a left-prefix of another index, with ready-to-run `DROP INDEX` statements.
698
+
699
+ ![Duplicate Indexes](docs/screenshots/duplicate_indexes.png)
700
+
701
+ ### Query Explorer
702
+ Build queries visually or write raw SQL. Optional AI assistant generates queries from plain English descriptions.
703
+
704
+ ![Visual Builder](docs/screenshots/visual_builder.png)
705
+
706
+ ### AI Tools
707
+ Schema review that finds anti-patterns -- missing primary keys, nullable foreign keys, inappropriate column types, and more.
708
+
709
+ ![AI Tools](docs/screenshots/ai_tools.png)
710
+ ```
711
+
712
+ - [ ] **Step 2: Update the Usage section**
713
+
714
+ Replace the current "Usage" section (starting around line 218) with:
715
+
716
+ ```markdown
717
+ ## Usage
718
+
719
+ Visit `/mysql_genius` in your browser. The dashboard loads automatically with an overview of your database health.
720
+
721
+ ### Dashboard
722
+ The default landing page shows server health cards, top 5 slow queries, top 5 most expensive queries (from performance_schema), and index alert badges for duplicate and unused indexes. Each section links to its detailed tab.
723
+
724
+ ### Query Explorer
725
+ Combines the visual query builder and raw SQL editor in one tab. Toggle between Visual mode (point-and-click with column selection, filters, and ordering) and SQL mode (raw SQL with optional AI assistant). Generated SQL syncs between modes.
726
+
727
+ ### Monitoring Tabs
728
+ - **Slow Queries** -- slow SELECT queries captured from your application in real time, with Explain and Optimize actions
729
+ - **Query Stats** -- top queries from `performance_schema` sorted by total time, avg time, calls, or rows examined
730
+ - **Server** -- connections, InnoDB buffer pool, query activity, with AI-powered diagnostics
731
+ - **Table Sizes** -- row counts, data size, index size, fragmentation for all tables
732
+ - **Unused Indexes** -- indexes with zero reads since server restart
733
+ - **Duplicate Indexes** -- redundant indexes with ready-to-run DROP statements
734
+ ```
735
+
736
+ - [ ] **Step 3: Commit**
737
+
738
+ ```bash
739
+ git add README.md
740
+ git commit -m "Update README to reflect dashboard-first layout and new tab order"
741
+ ```