mysql_genius 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +21 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/README.md +27 -0
- data/app/controllers/concerns/mysql_genius/ai_features.rb +31 -11
- data/app/controllers/concerns/mysql_genius/database_analysis.rb +15 -263
- data/app/controllers/concerns/mysql_genius/query_execution.rb +39 -81
- data/app/views/mysql_genius/queries/_tab_query_stats.html.erb +0 -10
- data/app/views/mysql_genius/queries/index.html.erb +1 -5
- data/lib/mysql_genius/core/connection/active_record_adapter.rb +77 -0
- data/lib/mysql_genius/version.rb +1 -1
- data/lib/mysql_genius.rb +2 -1
- data/mysql_genius.gemspec +2 -1
- metadata +17 -8
- data/app/services/mysql_genius/ai_client.rb +0 -91
- data/app/services/mysql_genius/ai_optimization_service.rb +0 -60
- data/app/services/mysql_genius/ai_suggestion_service.rb +0 -59
- data/docs/superpowers/plans/2026-04-08-dashboard-first-redesign.md +0 -741
- data/docs/superpowers/specs/2026-04-08-dashboard-first-redesign.md +0 -87
- data/lib/mysql_genius/sql_validator.rb +0 -57
|
@@ -1,741 +0,0 @@
|
|
|
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 →</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 →</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> </label>
|
|
241
|
-
<button id="vb-run" class="mg-btn mg-btn-primary" disabled>▶ Run Query</button>
|
|
242
|
-
<button id="vb-explain" class="mg-btn mg-btn-outline" disabled>🔎 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">⚡ 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">⚡ 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> </label>
|
|
303
|
-
<button id="sql-run" class="mg-btn mg-btn-primary">▶ Run Query</button>
|
|
304
|
-
<button id="sql-explain" class="mg-btn mg-btn-outline">🔎 Explain</button>
|
|
305
|
-
<% if @ai_enabled %>
|
|
306
|
-
<button id="sql-describe" class="mg-btn mg-btn-outline mg-btn-sm" style="margin-left:8px;">⚡ Describe</button>
|
|
307
|
-
<button id="sql-rewrite" class="mg-btn mg-btn-outline mg-btn-sm">⚡ 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>🐘 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;">✓ 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;">✓ 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
|
-

|
|
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
|
-

|
|
685
|
-
|
|
686
|
-
### Server Dashboard
|
|
687
|
-
Server health: version, connections, InnoDB buffer pool, and query activity with AI-powered diagnostics.
|
|
688
|
-
|
|
689
|
-

|
|
690
|
-
|
|
691
|
-
### Table Sizes
|
|
692
|
-
View row counts, data size, index size, fragmentation, and a visual size chart for every table.
|
|
693
|
-
|
|
694
|
-

|
|
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
|
-

|
|
700
|
-
|
|
701
|
-
### Query Explorer
|
|
702
|
-
Build queries visually or write raw SQL. Optional AI assistant generates queries from plain English descriptions.
|
|
703
|
-
|
|
704
|
-

|
|
705
|
-
|
|
706
|
-
### AI Tools
|
|
707
|
-
Schema review that finds anti-patterns -- missing primary keys, nullable foreign keys, inappropriate column types, and more.
|
|
708
|
-
|
|
709
|
-

|
|
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
|
-
```
|