mysql_genius 0.1.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +53 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.md +13 -0
  6. data/Gemfile +15 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +295 -0
  9. data/Rakefile +6 -0
  10. data/app/controllers/concerns/mysql_genius/ai_features.rb +360 -0
  11. data/app/controllers/concerns/mysql_genius/database_analysis.rb +259 -0
  12. data/app/controllers/concerns/mysql_genius/query_execution.rb +129 -0
  13. data/app/controllers/mysql_genius/base_controller.rb +18 -0
  14. data/app/controllers/mysql_genius/queries_controller.rb +54 -0
  15. data/app/services/mysql_genius/ai_client.rb +84 -0
  16. data/app/services/mysql_genius/ai_optimization_service.rb +56 -0
  17. data/app/services/mysql_genius/ai_suggestion_service.rb +56 -0
  18. data/app/views/layouts/mysql_genius/application.html.erb +116 -0
  19. data/app/views/mysql_genius/queries/_shared_results.html.erb +56 -0
  20. data/app/views/mysql_genius/queries/_tab_ai_tools.html.erb +43 -0
  21. data/app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +24 -0
  22. data/app/views/mysql_genius/queries/_tab_query_stats.html.erb +36 -0
  23. data/app/views/mysql_genius/queries/_tab_server.html.erb +54 -0
  24. data/app/views/mysql_genius/queries/_tab_slow_queries.html.erb +17 -0
  25. data/app/views/mysql_genius/queries/_tab_sql_query.html.erb +40 -0
  26. data/app/views/mysql_genius/queries/_tab_table_sizes.html.erb +31 -0
  27. data/app/views/mysql_genius/queries/_tab_unused_indexes.html.erb +25 -0
  28. data/app/views/mysql_genius/queries/_tab_visual_builder.html.erb +61 -0
  29. data/app/views/mysql_genius/queries/index.html.erb +1185 -0
  30. data/bin/console +14 -0
  31. data/bin/setup +8 -0
  32. data/config/routes.rb +24 -0
  33. data/docs/screenshots/ai_tools.png +0 -0
  34. data/docs/screenshots/duplicate_indexes.png +0 -0
  35. data/docs/screenshots/query_stats.png +0 -0
  36. data/docs/screenshots/server.png +0 -0
  37. data/docs/screenshots/sql_query.png +0 -0
  38. data/docs/screenshots/table_sizes.png +0 -0
  39. data/docs/screenshots/visual_builder.png +0 -0
  40. data/lib/mysql_genius/configuration.rb +96 -0
  41. data/lib/mysql_genius/engine.rb +12 -0
  42. data/lib/mysql_genius/slow_query_monitor.rb +38 -0
  43. data/lib/mysql_genius/sql_validator.rb +55 -0
  44. data/lib/mysql_genius/version.rb +3 -0
  45. data/lib/mysql_genius.rb +23 -0
  46. data/mysql_genius.gemspec +34 -0
  47. metadata +122 -0
@@ -0,0 +1,1185 @@
1
+ <h4>&#128024; MySQLGenius</h4>
2
+
3
+ <div class="mg-tabs">
4
+ <button class="mg-tab active" data-tab="visual">Visual Builder</button>
5
+ <button class="mg-tab" data-tab="sql">SQL Query</button>
6
+ <button class="mg-tab" data-tab="slow">Slow Queries</button>
7
+ <button class="mg-tab" data-tab="indexes">Duplicate Indexes</button>
8
+ <button class="mg-tab" data-tab="sizes">Table Sizes</button>
9
+ <button class="mg-tab" data-tab="qstats">Query Stats</button>
10
+ <button class="mg-tab" data-tab="unused">Unused Indexes</button>
11
+ <button class="mg-tab" data-tab="server">Server</button>
12
+ <% if @ai_enabled %>
13
+ <button class="mg-tab" data-tab="aitools">AI Tools</button>
14
+ <% end %>
15
+ </div>
16
+
17
+ <%= render "mysql_genius/queries/tab_visual_builder" %>
18
+ <%= render "mysql_genius/queries/tab_sql_query" %>
19
+ <%= render "mysql_genius/queries/tab_slow_queries" %>
20
+ <%= render "mysql_genius/queries/tab_duplicate_indexes" %>
21
+ <%= render "mysql_genius/queries/tab_table_sizes" %>
22
+ <%= render "mysql_genius/queries/tab_query_stats" %>
23
+ <%= render "mysql_genius/queries/tab_unused_indexes" %>
24
+ <%= render "mysql_genius/queries/tab_server" %>
25
+ <% if @ai_enabled %>
26
+ <%= render "mysql_genius/queries/tab_ai_tools" %>
27
+ <% end %>
28
+
29
+ <%= render "mysql_genius/queries/shared_results" %>
30
+
31
+ <script>
32
+ (function() {
33
+ "use strict";
34
+
35
+ var ROUTES = {
36
+ columns: '<%= mysql_genius.columns_path %>',
37
+ execute: '<%= mysql_genius.execute_path %>',
38
+ explain: '<%= mysql_genius.explain_path %>',
39
+ suggest: '<%= mysql_genius.suggest_path %>',
40
+ optimize: '<%= mysql_genius.optimize_path %>',
41
+ slow_queries: '<%= mysql_genius.slow_queries_path %>',
42
+ duplicate_indexes: '<%= mysql_genius.duplicate_indexes_path %>',
43
+ table_sizes: '<%= mysql_genius.table_sizes_path %>',
44
+ query_stats: '<%= mysql_genius.query_stats_path %>',
45
+ unused_indexes: '<%= mysql_genius.unused_indexes_path %>',
46
+ server_overview: '<%= mysql_genius.server_overview_path %>',
47
+ describe_query: '<%= mysql_genius.describe_query_path %>',
48
+ schema_review: '<%= mysql_genius.schema_review_path %>',
49
+ rewrite_query: '<%= mysql_genius.rewrite_query_path %>',
50
+ index_advisor: '<%= mysql_genius.index_advisor_path %>',
51
+ anomaly_detection: '<%= mysql_genius.anomaly_detection_path %>',
52
+ root_cause: '<%= mysql_genius.root_cause_path %>',
53
+ migration_risk: '<%= mysql_genius.migration_risk_path %>'
54
+ };
55
+
56
+ var csrfMeta = document.querySelector('meta[name="csrf-token"]');
57
+ var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
58
+ var currentColumns = [];
59
+ var columnTypeMap = {};
60
+ var lastExplainSql = '';
61
+ var lastExplainRows = [];
62
+
63
+ // --- Helpers ---
64
+
65
+ function el(id) { return document.getElementById(id); }
66
+ function qs(sel, parent) { return (parent || document).querySelector(sel); }
67
+ function qsa(sel, parent) { return Array.from((parent || document).querySelectorAll(sel)); }
68
+ function show(e) { e.classList.remove('mg-hidden'); }
69
+ function hide(e) { e.classList.add('mg-hidden'); }
70
+ function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
71
+
72
+ function ajax(method, url, data, onSuccess, onError) {
73
+ var xhr = new XMLHttpRequest();
74
+ xhr.open(method, url, true);
75
+ xhr.setRequestHeader('X-CSRF-Token', csrfToken);
76
+ xhr.setRequestHeader('Accept', 'application/json');
77
+ xhr.onload = function() {
78
+ var json;
79
+ try { json = JSON.parse(xhr.responseText); } catch(e) { json = null; }
80
+ if (xhr.status >= 200 && xhr.status < 300) {
81
+ onSuccess(json);
82
+ } else {
83
+ (onError || function(){})(json, xhr.status);
84
+ }
85
+ };
86
+ xhr.onerror = function() { (onError || function(){})(null, 0); };
87
+ if (data && method !== 'GET') {
88
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
89
+ var parts = [];
90
+ for (var k in data) {
91
+ if (Array.isArray(data[k])) {
92
+ data[k].forEach(function(v, i) {
93
+ if (typeof v === 'object') {
94
+ for (var vk in v) parts.push(encodeURIComponent(k + '[' + i + '][' + vk + ']') + '=' + encodeURIComponent(v[vk]));
95
+ } else {
96
+ parts.push(encodeURIComponent(k + '[]') + '=' + encodeURIComponent(v));
97
+ }
98
+ });
99
+ } else {
100
+ parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
101
+ }
102
+ }
103
+ xhr.send(parts.join('&'));
104
+ } else if (method === 'GET' && data) {
105
+ xhr.send();
106
+ } else {
107
+ xhr.send();
108
+ }
109
+ }
110
+
111
+ function ajaxGet(url, params, onSuccess, onError) {
112
+ var query = Object.keys(params).map(function(k) {
113
+ return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
114
+ }).join('&');
115
+ ajax('GET', url + '?' + query, null, onSuccess, onError);
116
+ }
117
+
118
+ // --- Tabs ---
119
+
120
+ qsa('.mg-tab').forEach(function(tab) {
121
+ tab.addEventListener('click', function() {
122
+ qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
123
+ qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
124
+ tab.classList.add('active');
125
+ el('tab-' + tab.dataset.tab).classList.add('active');
126
+ if (tab.dataset.tab === 'slow') loadSlowQueries();
127
+ if (tab.dataset.tab === 'indexes') loadDuplicateIndexes();
128
+ if (tab.dataset.tab === 'sizes') loadTableSizes();
129
+ if (tab.dataset.tab === 'qstats') loadQueryStats();
130
+ if (tab.dataset.tab === 'unused') loadUnusedIndexes();
131
+ if (tab.dataset.tab === 'server') loadServerOverview();
132
+ });
133
+ });
134
+
135
+ // --- Visual Builder ---
136
+
137
+ var typeLabels = { string: 'text', text: 'text', integer: 'number', boolean: 'yes/no', date: 'date', datetime: 'date/time', decimal: 'decimal', float: 'decimal' };
138
+ function colTypeLabel(type) { return typeLabels[type] || type; }
139
+
140
+ function operatorsForType(type) {
141
+ switch(type) {
142
+ case 'boolean': return ['=', '!=', 'IS NULL', 'IS NOT NULL'];
143
+ case 'date': case 'datetime': return ['=', '!=', '>', '<', '>=', '<=', 'BETWEEN', 'IS NULL', 'IS NOT NULL'];
144
+ case 'integer': case 'decimal': case 'float': return ['=', '!=', '>', '<', '>=', '<=', 'IS NULL', 'IS NOT NULL'];
145
+ default: return ['=', '!=', 'LIKE', '>', '<', 'IS NULL', 'IS NOT NULL'];
146
+ }
147
+ }
148
+
149
+ function loadColumnsForTable(table, callback) {
150
+ el('vb-columns').innerHTML = '';
151
+ el('vb-filters').innerHTML = '';
152
+ el('vb-orders').innerHTML = '';
153
+ columnTypeMap = {};
154
+ if (!table) {
155
+ hide(el('vb-columns-section')); hide(el('vb-filters-section'));
156
+ hide(el('vb-order-section')); hide(el('vb-generated-sql'));
157
+ el('vb-run').disabled = true; el('vb-explain').disabled = true;
158
+ return;
159
+ }
160
+ ajaxGet(ROUTES.columns, { table: table }, function(cols) {
161
+ currentColumns = cols;
162
+ cols.forEach(function(c) { columnTypeMap[c.name] = c.type; });
163
+ var html = '';
164
+ cols.forEach(function(col) {
165
+ var checked = col['default'] ? ' checked' : '';
166
+ html += '<label class="mg-check"><input type="checkbox" class="vb-col-check" value="' + escHtml(col.name) + '"' + checked +
167
+ ' data-default="' + (col['default'] ? '1' : '0') + '">' + escHtml(col.name) +
168
+ ' <span class="type-hint">(' + colTypeLabel(col.type) + ')</span></label>';
169
+ });
170
+ el('vb-columns').innerHTML = html;
171
+ show(el('vb-columns-section')); show(el('vb-filters-section'));
172
+ show(el('vb-order-section')); show(el('vb-generated-sql'));
173
+ el('vb-run').disabled = false; el('vb-explain').disabled = false;
174
+ if (callback) callback(cols); else updateGeneratedSql();
175
+ });
176
+ }
177
+
178
+ el('vb-table').addEventListener('change', function() { loadColumnsForTable(this.value); });
179
+
180
+ el('vb-toggle-all').addEventListener('click', function() {
181
+ var checks = qsa('.vb-col-check');
182
+ var allChecked = checks.every(function(c) { return c.checked; });
183
+ checks.forEach(function(c) { c.checked = !allChecked; });
184
+ updateGeneratedSql();
185
+ });
186
+
187
+ el('vb-show-defaults').addEventListener('click', function() {
188
+ qsa('.vb-col-check').forEach(function(c) { c.checked = c.dataset['default'] === '1'; });
189
+ updateGeneratedSql();
190
+ });
191
+
192
+ document.addEventListener('change', function(e) {
193
+ if (e.target.classList.contains('vb-col-check')) updateGeneratedSql();
194
+ });
195
+
196
+ function columnOptions(useAll) {
197
+ var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
198
+ var html = '<option value="">-- column --</option>';
199
+ cols.forEach(function(c) { html += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + ' (' + colTypeLabel(c.type) + ')</option>'; });
200
+ return html;
201
+ }
202
+
203
+ function addFilterRow(useAll) {
204
+ var div = document.createElement('div');
205
+ div.className = 'mg-row mg-mb vb-filter-row';
206
+ div.style.alignItems = 'center';
207
+ div.innerHTML =
208
+ '<div class="mg-col-3"><select class="vb-filter-col">' + columnOptions(useAll) + '</select></div>' +
209
+ '<div class="mg-col-2"><select class="vb-filter-op"><option>=</option><option>!=</option><option>LIKE</option><option>></option><option><</option><option>IS NULL</option><option>IS NOT NULL</option></select></div>' +
210
+ '<div class="mg-col-4 vb-filter-val-container"><input type="text" class="vb-filter-val" placeholder="value"></div>' +
211
+ '<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-filter">&#10005;</button></div>';
212
+ el('vb-filters').appendChild(div);
213
+ }
214
+
215
+ el('vb-add-filter').addEventListener('click', function() { addFilterRow(false); updateGeneratedSql(); });
216
+
217
+ document.addEventListener('change', function(e) {
218
+ if (e.target.classList.contains('vb-filter-col')) {
219
+ var row = e.target.closest('.vb-filter-row');
220
+ var colType = columnTypeMap[e.target.value] || 'string';
221
+ var opSel = qs('.vb-filter-op', row);
222
+ var ops = operatorsForType(colType);
223
+ opSel.innerHTML = ops.map(function(o) { return '<option>' + o + '</option>'; }).join('');
224
+ updateValueInput(row, colType, opSel.value);
225
+ updateGeneratedSql();
226
+ }
227
+ if (e.target.classList.contains('vb-filter-op')) {
228
+ var row = e.target.closest('.vb-filter-row');
229
+ var colName = qs('.vb-filter-col', row).value;
230
+ var colType = columnTypeMap[colName] || 'string';
231
+ updateValueInput(row, colType, e.target.value);
232
+ updateGeneratedSql();
233
+ }
234
+ });
235
+
236
+ document.addEventListener('input', function(e) {
237
+ if (e.target.classList.contains('vb-filter-val')) updateGeneratedSql();
238
+ });
239
+
240
+ document.addEventListener('click', function(e) {
241
+ if (e.target.closest('.vb-remove-filter')) { e.target.closest('.vb-filter-row').remove(); updateGeneratedSql(); }
242
+ if (e.target.closest('.vb-remove-order')) { e.target.closest('.vb-order-row').remove(); updateGeneratedSql(); }
243
+ });
244
+
245
+ function updateValueInput(row, colType, op) {
246
+ var container = qs('.vb-filter-val-container', row);
247
+ if (op === 'IS NULL' || op === 'IS NOT NULL') { container.innerHTML = ''; return; }
248
+ if (colType === 'boolean') {
249
+ container.innerHTML = '<select class="vb-filter-val"><option value="1">True</option><option value="0">False</option></select>';
250
+ } else if ((colType === 'date' || colType === 'datetime') && op === 'BETWEEN') {
251
+ container.innerHTML = '<input type="date" class="vb-filter-val" style="width:45%;display:inline-block;"> <span class="mg-text-muted">and</span> <input type="date" class="vb-filter-val-end" style="width:45%;display:inline-block;">';
252
+ } else if (colType === 'date' || colType === 'datetime') {
253
+ container.innerHTML = '<input type="date" class="vb-filter-val">';
254
+ } else if (colType === 'integer') {
255
+ container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="1">';
256
+ } else if (colType === 'decimal' || colType === 'float') {
257
+ container.innerHTML = '<input type="number" class="vb-filter-val" placeholder="number" step="any">';
258
+ } else {
259
+ var ph = (op === 'LIKE') ? 'use % as wildcard' : 'value';
260
+ container.innerHTML = '<input type="text" class="vb-filter-val" placeholder="' + ph + '">';
261
+ }
262
+ }
263
+
264
+ // --- Order By ---
265
+
266
+ function addOrderRow(useAll) {
267
+ var cols = useAll ? currentColumns : currentColumns.filter(function(c) { return c['default']; });
268
+ var options = '<option value="">-- column --</option>';
269
+ cols.forEach(function(c) { options += '<option value="' + escHtml(c.name) + '">' + escHtml(c.name) + '</option>'; });
270
+ var div = document.createElement('div');
271
+ div.className = 'mg-row mg-mb vb-order-row';
272
+ div.style.alignItems = 'center';
273
+ div.innerHTML =
274
+ '<div class="mg-col-3"><select class="vb-order-col">' + options + '</select></div>' +
275
+ '<div class="mg-col-2"><select class="vb-order-dir"><option value="ASC">Ascending</option><option value="DESC">Descending</option></select></div>' +
276
+ '<div><button class="mg-btn mg-btn-outline-danger mg-btn-sm vb-remove-order">&#10005;</button></div>';
277
+ el('vb-orders').appendChild(div);
278
+ }
279
+
280
+ el('vb-add-order').addEventListener('click', function() { addOrderRow(false); updateGeneratedSql(); });
281
+
282
+ document.addEventListener('change', function(e) {
283
+ if (e.target.classList.contains('vb-order-col') || e.target.classList.contains('vb-order-dir')) updateGeneratedSql();
284
+ });
285
+
286
+ // --- Build SQL ---
287
+
288
+ function buildSql() {
289
+ var table = el('vb-table').value;
290
+ if (!table) return '';
291
+ var cols = [];
292
+ qsa('.vb-col-check:checked').forEach(function(c) { cols.push('`' + c.value + '`'); });
293
+ if (!cols.length) cols = ['*'];
294
+ var sql = 'SELECT ' + cols.join(', ') + ' FROM `' + table + '`';
295
+ var wheres = [];
296
+ qsa('.vb-filter-row').forEach(function(row) {
297
+ var col = qs('.vb-filter-col', row).value;
298
+ var op = qs('.vb-filter-op', row).value;
299
+ var valEl = qs('.vb-filter-val', row);
300
+ var val = valEl ? valEl.value : '';
301
+ if (!col) return;
302
+ if (op === 'IS NULL' || op === 'IS NOT NULL') {
303
+ wheres.push('`' + col + '` ' + op);
304
+ } else if (op === 'BETWEEN') {
305
+ var endEl = qs('.vb-filter-val-end', row);
306
+ var endVal = endEl ? endEl.value : '';
307
+ wheres.push("`" + col + "` BETWEEN '" + val.replace(/'/g, "''") + "' AND '" + endVal.replace(/'/g, "''") + "'");
308
+ } else if (op === 'LIKE') {
309
+ wheres.push("`" + col + "` LIKE '" + val.replace(/'/g, "''") + "'");
310
+ } else {
311
+ wheres.push("`" + col + "` " + op + " '" + val.replace(/'/g, "''") + "'");
312
+ }
313
+ });
314
+ if (wheres.length) sql += ' WHERE ' + wheres.join(' AND ');
315
+ var orders = [];
316
+ qsa('.vb-order-row').forEach(function(row) {
317
+ var col = qs('.vb-order-col', row).value;
318
+ var dir = qs('.vb-order-dir', row).value;
319
+ if (col) orders.push('`' + col + '` ' + dir);
320
+ });
321
+ if (orders.length) sql += ' ORDER BY ' + orders.join(', ');
322
+ return sql;
323
+ }
324
+
325
+ function updateGeneratedSql() {
326
+ var sql = buildSql();
327
+ el('vb-sql-preview').value = sql;
328
+ el('sql-input').value = sql;
329
+ }
330
+
331
+ // --- Run Query ---
332
+
333
+ el('vb-run').addEventListener('click', function() {
334
+ var sql = buildSql();
335
+ if (sql) runQuery(sql, parseInt(el('vb-row-limit').value) || 25);
336
+ });
337
+
338
+ el('sql-run').addEventListener('click', function() {
339
+ var sql = el('sql-input').value.trim();
340
+ if (sql) runQuery(sql, parseInt(el('sql-row-limit').value) || 25);
341
+ });
342
+
343
+ el('sql-input').addEventListener('input', function() { el('vb-sql-preview').value = this.value; });
344
+
345
+ function runQuery(sql, rowLimit) {
346
+ clearResults();
347
+ setBtnLoading(['vb-run', 'sql-run'], true);
348
+ ajax('POST', ROUTES.execute, { sql: sql, row_limit: rowLimit }, function(data) {
349
+ renderResults(data);
350
+ setBtnLoading(['vb-run', 'sql-run'], false);
351
+ }, function(json) {
352
+ var cls = (json && json.timeout) ? 'mg-alert-warning' : 'mg-alert-danger';
353
+ el('results-alert').innerHTML = '<div class="mg-alert ' + cls + '">' + escHtml((json && json.error) || 'An unexpected error occurred.') + '</div>';
354
+ show(el('results-alert'));
355
+ setBtnLoading(['vb-run', 'sql-run'], false);
356
+ });
357
+ }
358
+
359
+ function clearResults() {
360
+ hide(el('results-alert')); hide(el('results-stats'));
361
+ hide(el('results-table-wrapper')); hide(el('results-empty'));
362
+ hide(el('results-truncated'));
363
+ el('results-thead').innerHTML = '';
364
+ el('results-tbody').innerHTML = '';
365
+ }
366
+
367
+ function renderResults(data) {
368
+ if (data.row_count === 0) {
369
+ show(el('results-empty')); show(el('results-stats'));
370
+ el('results-row-count').textContent = '0 rows';
371
+ el('results-time').textContent = data.execution_time_ms + ' ms';
372
+ return;
373
+ }
374
+ el('results-row-count').textContent = data.row_count + ' row' + (data.row_count !== 1 ? 's' : '');
375
+ el('results-time').textContent = data.execution_time_ms + ' ms';
376
+ if (data.truncated) show(el('results-truncated'));
377
+ show(el('results-stats'));
378
+
379
+ el('results-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
380
+ el('results-tbody').innerHTML = data.rows.map(function(row) {
381
+ return '<tr>' + row.map(function(val) {
382
+ if (val === null) return '<td><em class="null">NULL</em></td>';
383
+ if (val === '[REDACTED]') return '<td><span class="redacted">[REDACTED]</span></td>';
384
+ return '<td>' + escHtml(String(val)) + '</td>';
385
+ }).join('') + '</tr>';
386
+ }).join('');
387
+ show(el('results-table-wrapper'));
388
+ }
389
+
390
+ // --- Explain ---
391
+
392
+ el('vb-explain').addEventListener('click', function() { var sql = buildSql(); if (sql) runExplain(sql); });
393
+ el('sql-explain').addEventListener('click', function() { var sql = el('sql-input').value.trim(); if (sql) runExplain(sql); });
394
+ el('explain-close').addEventListener('click', function() { hide(el('explain-results')); });
395
+
396
+ function runExplain(sql, fromSlowQuery) {
397
+ lastExplainSql = sql;
398
+ hide(el('explain-results')); hide(el('optimize-results'));
399
+ el('explain-thead').innerHTML = '';
400
+ el('explain-tbody').innerHTML = '';
401
+ setBtnLoading(['vb-explain', 'sql-explain'], true);
402
+
403
+ var postData = { sql: sql };
404
+ if (fromSlowQuery) postData.from_slow_query = 'true';
405
+ ajax('POST', ROUTES.explain, postData, function(data) {
406
+ lastExplainRows = data.rows;
407
+ el('explain-thead').innerHTML = '<tr>' + data.columns.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
408
+ el('explain-tbody').innerHTML = data.rows.map(function(row) {
409
+ return '<tr>' + row.map(function(val) {
410
+ return val === null ? '<td><em class="null">NULL</em></td>' : '<td>' + escHtml(String(val)) + '</td>';
411
+ }).join('') + '</tr>';
412
+ }).join('');
413
+ show(el('explain-results'));
414
+ setBtnLoading(['vb-explain', 'sql-explain'], false);
415
+ }, function(json) {
416
+ el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'Explain failed.') + '</div>';
417
+ show(el('results-alert'));
418
+ setBtnLoading(['vb-explain', 'sql-explain'], false);
419
+ });
420
+ }
421
+
422
+ // --- AI ---
423
+
424
+ var aiToggle = el('ai-toggle');
425
+ if (aiToggle) {
426
+ aiToggle.addEventListener('click', function() {
427
+ var panel = el('ai-panel');
428
+ panel.classList.toggle('mg-hidden');
429
+ });
430
+ }
431
+
432
+ var aiSuggest = el('ai-suggest');
433
+ if (aiSuggest) {
434
+ aiSuggest.addEventListener('click', function() {
435
+ var prompt = el('ai-prompt').value.trim();
436
+ if (!prompt) return;
437
+ aiSuggest.disabled = true;
438
+ aiSuggest.innerHTML = '<span class="mg-spinner"></span> Thinking...';
439
+ hide(el('ai-result'));
440
+
441
+ ajax('POST', ROUTES.suggest, { prompt: prompt }, function(data) {
442
+ el('sql-input').value = data.sql || '';
443
+ if (data.explanation) {
444
+ el('ai-explanation').textContent = data.explanation;
445
+ show(el('ai-result'));
446
+ }
447
+ parseSqlToBuilder(data.sql);
448
+ aiSuggest.disabled = false;
449
+ aiSuggest.innerHTML = '&#9889; Suggest Query';
450
+ }, function(json) {
451
+ el('results-alert').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml((json && json.error) || 'AI suggestion failed.') + '</div>';
452
+ show(el('results-alert'));
453
+ aiSuggest.disabled = false;
454
+ aiSuggest.innerHTML = '&#9889; Suggest Query';
455
+ });
456
+ });
457
+ }
458
+
459
+ var explainOptimize = el('explain-optimize');
460
+ if (explainOptimize) {
461
+ explainOptimize.addEventListener('click', function() {
462
+ explainOptimize.disabled = true;
463
+ explainOptimize.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
464
+ hide(el('optimize-results'));
465
+
466
+ var data = { sql: lastExplainSql };
467
+ lastExplainRows.forEach(function(row, i) {
468
+ row.forEach(function(val, j) {
469
+ data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
470
+ });
471
+ });
472
+
473
+ ajax('POST', ROUTES.optimize, data, function(resp) {
474
+ el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.');
475
+ show(el('optimize-results'));
476
+ explainOptimize.disabled = false;
477
+ explainOptimize.innerHTML = '&#9889; AI Optimization';
478
+ }, function(json) {
479
+ el('optimize-content').textContent = (json && json.error) || 'Optimization failed.';
480
+ show(el('optimize-results'));
481
+ explainOptimize.disabled = false;
482
+ explainOptimize.innerHTML = '&#9889; AI Optimization';
483
+ });
484
+ });
485
+ }
486
+
487
+ // --- Slow Queries ---
488
+
489
+ function loadSlowQueries() {
490
+ show(el('slow-loading'));
491
+ hide(el('slow-empty')); hide(el('slow-table-wrapper'));
492
+ el('slow-tbody').innerHTML = '';
493
+
494
+ ajaxGet(ROUTES.slow_queries, {}, function(data) {
495
+ hide(el('slow-loading'));
496
+ if (!data || !data.length) { show(el('slow-empty')); el('slow-count').textContent = '0'; return; }
497
+ el('slow-count').textContent = data.length + ' queries';
498
+ el('slow-tbody').innerHTML = data.map(function(q) {
499
+ var d = q.duration_ms;
500
+ var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
501
+ var sqlEsc = escHtml(q.sql);
502
+ var sqlShort = sqlEsc.length > 200 ? sqlEsc.substring(0, 200) + '...' : sqlEsc;
503
+ return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
504
+ '<td><small>' + escHtml(q.timestamp) + '</small></td>' +
505
+ '<td><code title="' + sqlEsc + '">' + sqlShort + '</code></td>' +
506
+ '<td><button class="mg-btn mg-btn-outline mg-btn-sm slow-explain-btn" data-sql="' + sqlEsc.replace(/"/g, '&quot;') + '">Explain</button> ' +
507
+ '<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' + sqlEsc.replace(/"/g, '&quot;') + '">Use</button></td></tr>';
508
+ }).join('');
509
+ show(el('slow-table-wrapper'));
510
+ }, function() {
511
+ hide(el('slow-loading'));
512
+ el('slow-empty').textContent = 'Failed to load slow queries.';
513
+ show(el('slow-empty'));
514
+ });
515
+ }
516
+
517
+ el('slow-refresh').addEventListener('click', loadSlowQueries);
518
+
519
+ document.addEventListener('click', function(e) {
520
+ var btn = e.target.closest('.slow-explain-btn');
521
+ if (btn) runExplain(btn.dataset.sql, true);
522
+ var useBtn = e.target.closest('.slow-use-btn');
523
+ if (useBtn) {
524
+ el('sql-input').value = useBtn.dataset.sql;
525
+ qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
526
+ qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
527
+ qs('[data-tab="sql"]').classList.add('active');
528
+ el('tab-sql').classList.add('active');
529
+ }
530
+ });
531
+
532
+ // --- Duplicate Indexes ---
533
+
534
+ function loadDuplicateIndexes() {
535
+ show(el('dup-loading'));
536
+ hide(el('dup-empty')); hide(el('dup-table-wrapper'));
537
+ el('dup-tbody').innerHTML = '';
538
+
539
+ ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
540
+ hide(el('dup-loading'));
541
+ if (!data || !data.length) { show(el('dup-empty')); el('dup-count').textContent = '0'; return; }
542
+ el('dup-count').textContent = data.length + ' found';
543
+ el('dup-tbody').innerHTML = data.map(function(d) {
544
+ var dropSql = 'ALTER TABLE `' + d.table + '` DROP INDEX `' + d.duplicate_index + '`;';
545
+ return '<tr>' +
546
+ '<td><strong>' + escHtml(d.table) + '</strong></td>' +
547
+ '<td><code>' + escHtml(d.duplicate_index) + '</code>' + (d.unique ? ' <span class="mg-badge mg-badge-warning">UNIQUE</span>' : '') + '</td>' +
548
+ '<td>' + d.duplicate_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
549
+ '<td><code>' + escHtml(d.covered_by_index) + '</code></td>' +
550
+ '<td>' + d.covered_by_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
551
+ '<td><code style="font-size:11px;user-select:all;cursor:pointer;" title="Click to select">' + escHtml(dropSql) + '</code></td>' +
552
+ '</tr>';
553
+ }).join('');
554
+ show(el('dup-table-wrapper'));
555
+ }, function() {
556
+ hide(el('dup-loading'));
557
+ el('dup-empty').textContent = 'Failed to scan indexes.';
558
+ show(el('dup-empty'));
559
+ });
560
+ }
561
+
562
+ el('dup-refresh').addEventListener('click', loadDuplicateIndexes);
563
+
564
+ // --- Table Sizes ---
565
+
566
+ function formatMb(mb) {
567
+ if (mb >= 1024) return (mb / 1024).toFixed(2) + ' GB';
568
+ if (mb >= 1) return mb.toFixed(2) + ' MB';
569
+ return (mb * 1024).toFixed(0) + ' KB';
570
+ }
571
+
572
+ function sizeBar(pct, color) {
573
+ return '<div style="background:#e9ecef;border-radius:3px;height:8px;width:100%;">' +
574
+ '<div style="background:' + color + ';border-radius:3px;height:8px;width:' + Math.max(pct, 1) + '%;"></div></div>';
575
+ }
576
+
577
+ var PIE_COLORS = [
578
+ '#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f',
579
+ '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac',
580
+ '#86bcb6', '#8cd17d', '#b6992d', '#499894', '#d37295'
581
+ ];
582
+
583
+ function drawPieChart(canvas, slices, legendEl) {
584
+ var ctx = canvas.getContext('2d');
585
+ var w = canvas.width;
586
+ var h = canvas.height;
587
+ var cx = w / 2;
588
+ var cy = h / 2;
589
+ var r = Math.min(cx, cy) - 4;
590
+ var total = slices.reduce(function(s, sl) { return s + sl.value; }, 0);
591
+ if (total === 0) return;
592
+
593
+ ctx.clearRect(0, 0, w, h);
594
+ var startAngle = -Math.PI / 2;
595
+
596
+ slices.forEach(function(sl, i) {
597
+ var sliceAngle = (sl.value / total) * 2 * Math.PI;
598
+ ctx.beginPath();
599
+ ctx.moveTo(cx, cy);
600
+ ctx.arc(cx, cy, r, startAngle, startAngle + sliceAngle);
601
+ ctx.closePath();
602
+ ctx.fillStyle = PIE_COLORS[i % PIE_COLORS.length];
603
+ ctx.fill();
604
+ ctx.strokeStyle = '#fff';
605
+ ctx.lineWidth = 1.5;
606
+ ctx.stroke();
607
+ startAngle += sliceAngle;
608
+ });
609
+
610
+ // Legend
611
+ legendEl.innerHTML = slices.map(function(sl, i) {
612
+ var pct = ((sl.value / total) * 100).toFixed(1);
613
+ return '<div style="display:flex;align-items:center;gap:6px;">' +
614
+ '<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + PIE_COLORS[i % PIE_COLORS.length] + ';flex-shrink:0;"></span>' +
615
+ '<span>' + escHtml(sl.label) + '</span>' +
616
+ '<span class="mg-text-muted">' + formatMb(sl.value) + ' (' + pct + '%)</span></div>';
617
+ }).join('');
618
+ }
619
+
620
+ function loadTableSizes() {
621
+ show(el('sizes-loading'));
622
+ hide(el('sizes-table-wrapper')); hide(el('sizes-chart-wrapper'));
623
+ el('sizes-tbody').innerHTML = '';
624
+
625
+ ajaxGet(ROUTES.table_sizes, {}, function(data) {
626
+ hide(el('sizes-loading'));
627
+ if (!data || !data.length) { el('sizes-total').textContent = '0 tables'; show(el('sizes-table-wrapper')); return; }
628
+
629
+ var totalMb = data.reduce(function(sum, t) { return sum + t.total_mb; }, 0);
630
+ el('sizes-total').textContent = data.length + ' tables, ' + formatMb(totalMb) + ' total';
631
+ var maxMb = data[0].total_mb || 1;
632
+
633
+ // Pie chart: top 10 tables + "Other"
634
+ var topN = data.slice(0, 10);
635
+ var otherMb = data.slice(10).reduce(function(s, t) { return s + t.total_mb; }, 0);
636
+ var slices = topN.map(function(t) { return { label: t.table, value: t.total_mb }; });
637
+ if (otherMb > 0) slices.push({ label: 'Other (' + (data.length - 10) + ' tables)', value: otherMb });
638
+ drawPieChart(el('sizes-pie'), slices, el('sizes-legend'));
639
+ show(el('sizes-chart-wrapper'));
640
+
641
+ el('sizes-tbody').innerHTML = data.map(function(t) {
642
+ var pct = (t.total_mb / maxMb) * 100;
643
+ var color = t.total_mb >= 100 ? '#dc3545' : t.total_mb >= 10 ? '#ffc107' : '#28a745';
644
+ var rows = t.rows != null ? Number(t.rows).toLocaleString() : '?';
645
+ return '<tr>' +
646
+ '<td><strong>' + escHtml(t.table) + '</strong></td>' +
647
+ '<td style="text-align:right">' + rows + '</td>' +
648
+ '<td style="text-align:right">' + formatMb(t.data_mb) + '</td>' +
649
+ '<td style="text-align:right">' + formatMb(t.index_mb) + '</td>' +
650
+ '<td style="text-align:right"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
651
+ '<td style="text-align:right">' + (t.fragmented_mb > 0 ? formatMb(t.fragmented_mb) : '-') + '</td>' +
652
+ '<td>' + sizeBar(pct, color) + '</td>' +
653
+ '</tr>';
654
+ }).join('');
655
+ show(el('sizes-table-wrapper'));
656
+ }, function() {
657
+ hide(el('sizes-loading'));
658
+ el('sizes-total').textContent = 'Failed to load';
659
+ });
660
+ }
661
+
662
+ el('sizes-refresh').addEventListener('click', loadTableSizes);
663
+
664
+ // --- Query Stats ---
665
+
666
+ function loadQueryStats() {
667
+ show(el('qstats-loading'));
668
+ hide(el('qstats-empty')); hide(el('qstats-table-wrapper')); hide(el('qstats-error'));
669
+ el('qstats-tbody').innerHTML = '';
670
+
671
+ var sort = el('qstats-sort').value;
672
+ ajaxGet(ROUTES.query_stats, { sort: sort }, function(data) {
673
+ hide(el('qstats-loading'));
674
+ if (data.error) {
675
+ el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
676
+ show(el('qstats-error')); return;
677
+ }
678
+ if (!data.length) { show(el('qstats-empty')); el('qstats-count').textContent = '0'; return; }
679
+ el('qstats-count').textContent = data.length + ' queries';
680
+ el('qstats-tbody').innerHTML = data.map(function(q) {
681
+ var ratioClass = q.rows_ratio > 100 ? 'mg-badge-danger' : q.rows_ratio > 10 ? 'mg-badge-warning' : '';
682
+ var sqlShort = q.sql.length > 120 ? q.sql.substring(0, 120) + '...' : q.sql;
683
+ return '<tr>' +
684
+ '<td><code style="font-size:11px;word-break:break-all;" title="' + escHtml(q.sql) + '">' + escHtml(sqlShort) + '</code></td>' +
685
+ '<td style="text-align:right">' + Number(q.calls).toLocaleString() + '</td>' +
686
+ '<td style="text-align:right">' + formatDuration(q.total_time_ms) + '</td>' +
687
+ '<td style="text-align:right">' + formatDuration(q.avg_time_ms) + '</td>' +
688
+ '<td style="text-align:right">' + formatDuration(q.max_time_ms) + '</td>' +
689
+ '<td style="text-align:right">' + Number(q.rows_examined).toLocaleString() + '</td>' +
690
+ '<td style="text-align:right">' + Number(q.rows_sent).toLocaleString() + '</td>' +
691
+ '<td style="text-align:right">' + (ratioClass ? '<span class="mg-badge ' + ratioClass + '">' + q.rows_ratio + 'x</span>' : q.rows_ratio + 'x') + '</td>' +
692
+ '</tr>';
693
+ }).join('');
694
+ show(el('qstats-table-wrapper'));
695
+ }, function(json) {
696
+ hide(el('qstats-loading'));
697
+ el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query stats.') + '</div>';
698
+ show(el('qstats-error'));
699
+ });
700
+ }
701
+
702
+ function formatDuration(ms) {
703
+ if (ms >= 60000) return (ms / 60000).toFixed(1) + ' min';
704
+ if (ms >= 1000) return (ms / 1000).toFixed(1) + ' s';
705
+ return ms.toFixed(1) + ' ms';
706
+ }
707
+
708
+ el('qstats-refresh').addEventListener('click', loadQueryStats);
709
+ el('qstats-sort').addEventListener('change', loadQueryStats);
710
+
711
+ // --- Unused Indexes ---
712
+
713
+ function loadUnusedIndexes() {
714
+ show(el('unused-loading'));
715
+ hide(el('unused-empty')); hide(el('unused-table-wrapper')); hide(el('unused-error'));
716
+ el('unused-tbody').innerHTML = '';
717
+
718
+ ajaxGet(ROUTES.unused_indexes, {}, function(data) {
719
+ hide(el('unused-loading'));
720
+ if (data.error) {
721
+ el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
722
+ show(el('unused-error')); return;
723
+ }
724
+ if (!data.length) { show(el('unused-empty')); el('unused-count').textContent = '0'; return; }
725
+ el('unused-count').textContent = data.length + ' found';
726
+ el('unused-tbody').innerHTML = data.map(function(d) {
727
+ return '<tr>' +
728
+ '<td><strong>' + escHtml(d.table) + '</strong></td>' +
729
+ '<td><code>' + escHtml(d.index_name) + '</code></td>' +
730
+ '<td style="text-align:right">' + d.reads + '</td>' +
731
+ '<td style="text-align:right">' + Number(d.writes).toLocaleString() + '</td>' +
732
+ '<td style="text-align:right">' + Number(d.table_rows).toLocaleString() + '</td>' +
733
+ '<td><code style="font-size:11px;user-select:all;cursor:pointer;" title="Click to select">' + escHtml(d.drop_sql) + '</code></td>' +
734
+ '</tr>';
735
+ }).join('');
736
+ show(el('unused-table-wrapper'));
737
+ }, function(json) {
738
+ hide(el('unused-loading'));
739
+ el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load unused indexes.') + '</div>';
740
+ show(el('unused-error'));
741
+ });
742
+ }
743
+
744
+ el('unused-refresh').addEventListener('click', loadUnusedIndexes);
745
+
746
+ // --- Server Overview ---
747
+
748
+ function usageBar(pct, label) {
749
+ var color = pct >= 90 ? '#dc3545' : pct >= 70 ? '#ffc107' : '#28a745';
750
+ return '<div class="mg-usage-bar">' +
751
+ '<div class="mg-usage-bar-fill" style="width:' + Math.min(pct, 100) + '%;background:' + color + ';"></div>' +
752
+ '<div class="mg-usage-bar-text">' + label + '</div></div>';
753
+ }
754
+
755
+ function statRow(label, value) {
756
+ return '<div class="mg-stat-label">' + label + '</div><div class="mg-stat-value">' + value + '</div>';
757
+ }
758
+
759
+ function loadServerOverview() {
760
+ show(el('server-loading'));
761
+ hide(el('server-content')); hide(el('server-error'));
762
+
763
+ ajaxGet(ROUTES.server_overview, {}, function(data) {
764
+ hide(el('server-loading'));
765
+ if (data.error) {
766
+ el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml(data.error) + '</div>';
767
+ show(el('server-error')); return;
768
+ }
769
+
770
+ var s = data.server;
771
+ var c = data.connections;
772
+ var db = data.innodb;
773
+ var q = data.queries;
774
+
775
+ // Server info
776
+ el('server-info').innerHTML =
777
+ statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
778
+ statRow('Uptime', escHtml(s.uptime)) +
779
+ statRow('Queries/sec', q.qps) +
780
+ statRow('Total Queries', Number(q.questions).toLocaleString()) +
781
+ statRow('Slow Queries', Number(q.slow_queries).toLocaleString());
782
+
783
+ // Connections
784
+ el('conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
785
+ el('conn-info').innerHTML =
786
+ statRow('Threads Running', c.threads_running) +
787
+ statRow('Threads Cached', c.threads_cached) +
788
+ statRow('Threads Created', Number(c.threads_created).toLocaleString()) +
789
+ statRow('Max Used', c.max_used) +
790
+ statRow('Aborted Connects', Number(c.aborted_connects).toLocaleString()) +
791
+ statRow('Aborted Clients', Number(c.aborted_clients).toLocaleString());
792
+
793
+ // InnoDB
794
+ var poolUsedPct = db.buffer_pool_pages_total > 0
795
+ ? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
796
+ : 0;
797
+ el('innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
798
+ el('innodb-info').innerHTML =
799
+ statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
800
+ statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString()) +
801
+ statRow('Free Pages', Number(db.buffer_pool_pages_free).toLocaleString()) +
802
+ statRow('Row Lock Waits', Number(db.row_lock_waits).toLocaleString()) +
803
+ statRow('Row Lock Time', formatDuration(db.row_lock_time_ms));
804
+
805
+ // Query activity
806
+ var tmpBadge = q.tmp_disk_pct > 25
807
+ ? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
808
+ : q.tmp_disk_pct + '%';
809
+ el('query-info').innerHTML =
810
+ statRow('Tmp Tables (disk)', Number(q.tmp_disk_tables).toLocaleString() + ' / ' + Number(q.tmp_tables).toLocaleString() + ' ' + tmpBadge) +
811
+ statRow('Full Joins (no index)', Number(q.select_full_join).toLocaleString()) +
812
+ statRow('Sort Merge Passes', Number(q.sort_merge_passes).toLocaleString());
813
+
814
+ show(el('server-content'));
815
+ }, function(json) {
816
+ hide(el('server-loading'));
817
+ el('server-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load server overview.') + '</div>';
818
+ show(el('server-error'));
819
+ });
820
+ }
821
+
822
+ el('server-refresh').addEventListener('click', loadServerOverview);
823
+
824
+ // --- AI Feature Handlers ---
825
+
826
+ // Generic AI call helper
827
+ function aiCall(url, data, onSuccess, onError) {
828
+ ajax('POST', url, data, function(result) {
829
+ if (result.error) { onError(result.error); return; }
830
+ onSuccess(result);
831
+ }, function(json) {
832
+ onError((json && json.error) || 'AI request failed.');
833
+ });
834
+ }
835
+
836
+ function showAiQueryResult(title, html) {
837
+ el('ai-query-title').innerHTML = '<strong>&#9889; ' + escHtml(title) + '</strong>';
838
+ el('ai-query-content').innerHTML = html;
839
+ show(el('ai-query-result'));
840
+ }
841
+
842
+ el('ai-query-close').addEventListener('click', function() { hide(el('ai-query-result')); });
843
+
844
+ // Describe Query
845
+ var sqlDescribe = el('sql-describe');
846
+ if (sqlDescribe) {
847
+ sqlDescribe.addEventListener('click', function() {
848
+ var sql = el('sql-input').value.trim();
849
+ if (!sql) return;
850
+ sqlDescribe.disabled = true;
851
+ sqlDescribe.innerHTML = '<span class="mg-spinner"></span>';
852
+ hide(el('ai-query-result'));
853
+ aiCall(ROUTES.describe_query, { sql: sql }, function(data) {
854
+ showAiQueryResult('Query Description', formatMarkdown(data.explanation || data.raw || 'No explanation returned.'));
855
+ sqlDescribe.disabled = false;
856
+ sqlDescribe.innerHTML = '&#9889; Describe';
857
+ }, function(err) {
858
+ showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
859
+ sqlDescribe.disabled = false;
860
+ sqlDescribe.innerHTML = '&#9889; Describe';
861
+ });
862
+ });
863
+ }
864
+
865
+ // Rewrite Query
866
+ var sqlRewrite = el('sql-rewrite');
867
+ if (sqlRewrite) {
868
+ sqlRewrite.addEventListener('click', function() {
869
+ var sql = el('sql-input').value.trim();
870
+ if (!sql) return;
871
+ sqlRewrite.disabled = true;
872
+ sqlRewrite.innerHTML = '<span class="mg-spinner"></span>';
873
+ hide(el('ai-query-result'));
874
+ aiCall(ROUTES.rewrite_query, { sql: sql }, function(data) {
875
+ var html = '';
876
+ if (data.rewritten) {
877
+ html += '<strong>Rewritten Query:</strong><pre class="mg-pre"><code>' + escHtml(data.rewritten) + '</code></pre>';
878
+ }
879
+ if (data.changes) {
880
+ html += '<strong>Changes:</strong><br>' + formatMarkdown(data.changes);
881
+ }
882
+ if (!data.rewritten && !data.changes) {
883
+ html = formatMarkdown(data.raw || 'No rewrite suggestions.');
884
+ }
885
+ showAiQueryResult('Query Rewrite', html);
886
+ sqlRewrite.disabled = false;
887
+ sqlRewrite.innerHTML = '&#9889; Rewrite';
888
+ }, function(err) {
889
+ showAiQueryResult('Error', '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>');
890
+ sqlRewrite.disabled = false;
891
+ sqlRewrite.innerHTML = '&#9889; Rewrite';
892
+ });
893
+ });
894
+ }
895
+
896
+ // Index Advisor (on EXPLAIN results)
897
+ var indexAdvisor = el('explain-index-advisor');
898
+ if (indexAdvisor) {
899
+ indexAdvisor.addEventListener('click', function() {
900
+ indexAdvisor.disabled = true;
901
+ indexAdvisor.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
902
+ hide(el('optimize-results'));
903
+
904
+ var data = { sql: lastExplainSql };
905
+ lastExplainRows.forEach(function(row, i) {
906
+ row.forEach(function(val, j) {
907
+ data['explain_rows[' + i + '][' + j + ']'] = val == null ? '' : val;
908
+ });
909
+ });
910
+
911
+ aiCall(ROUTES.index_advisor, data, function(resp) {
912
+ el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.');
913
+ show(el('optimize-results'));
914
+ indexAdvisor.disabled = false;
915
+ indexAdvisor.innerHTML = '&#9889; Index Advisor';
916
+ }, function(err) {
917
+ el('optimize-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
918
+ show(el('optimize-results'));
919
+ indexAdvisor.disabled = false;
920
+ indexAdvisor.innerHTML = '&#9889; Index Advisor';
921
+ });
922
+ });
923
+ }
924
+
925
+ // Server: Root Cause Analysis
926
+ var rootCauseBtn = el('server-root-cause');
927
+ if (rootCauseBtn) {
928
+ rootCauseBtn.addEventListener('click', function() {
929
+ rootCauseBtn.disabled = true;
930
+ rootCauseBtn.innerHTML = '<span class="mg-spinner"></span> Diagnosing...';
931
+ hide(el('server-ai-result'));
932
+ aiCall(ROUTES.root_cause, {}, function(data) {
933
+ el('server-ai-title').innerHTML = '<strong>&#9889; Root Cause Analysis</strong>';
934
+ el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.');
935
+ show(el('server-ai-result'));
936
+ rootCauseBtn.disabled = false;
937
+ rootCauseBtn.innerHTML = '&#9889; Why is it slow?';
938
+ }, function(err) {
939
+ el('server-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
940
+ el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
941
+ show(el('server-ai-result'));
942
+ rootCauseBtn.disabled = false;
943
+ rootCauseBtn.innerHTML = '&#9889; Why is it slow?';
944
+ });
945
+ });
946
+ }
947
+
948
+ el('server-ai-close').addEventListener('click', function() { hide(el('server-ai-result')); });
949
+
950
+ // Server: Anomaly Detection
951
+ var anomalyBtn = el('server-anomaly');
952
+ if (anomalyBtn) {
953
+ anomalyBtn.addEventListener('click', function() {
954
+ anomalyBtn.disabled = true;
955
+ anomalyBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
956
+ hide(el('server-ai-result'));
957
+ aiCall(ROUTES.anomaly_detection, {}, function(data) {
958
+ el('server-ai-title').innerHTML = '<strong>&#9889; Query Health Report</strong>';
959
+ el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.');
960
+ show(el('server-ai-result'));
961
+ anomalyBtn.disabled = false;
962
+ anomalyBtn.innerHTML = '&#9889; Anomaly Detection';
963
+ }, function(err) {
964
+ el('server-ai-title').innerHTML = '<strong>&#9889; Error</strong>';
965
+ el('server-ai-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
966
+ show(el('server-ai-result'));
967
+ anomalyBtn.disabled = false;
968
+ anomalyBtn.innerHTML = '&#9889; Anomaly Detection';
969
+ });
970
+ });
971
+ }
972
+
973
+ // Schema Review
974
+ var schemaBtn = el('schema-review-btn');
975
+ if (schemaBtn) {
976
+ schemaBtn.addEventListener('click', function() {
977
+ schemaBtn.disabled = true;
978
+ schemaBtn.innerHTML = '<span class="mg-spinner"></span> Analyzing...';
979
+ hide(el('schema-result'));
980
+ var table = el('schema-table').value;
981
+ aiCall(ROUTES.schema_review, { table: table }, function(data) {
982
+ el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '');
983
+ show(el('schema-result'));
984
+ schemaBtn.disabled = false;
985
+ schemaBtn.innerHTML = '&#9889; Analyze Schema';
986
+ }, function(err) {
987
+ el('schema-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
988
+ show(el('schema-result'));
989
+ schemaBtn.disabled = false;
990
+ schemaBtn.innerHTML = '&#9889; Analyze Schema';
991
+ });
992
+ });
993
+ }
994
+
995
+ // Migration Risk
996
+ var migrationBtn = el('migration-assess-btn');
997
+ if (migrationBtn) {
998
+ migrationBtn.addEventListener('click', function() {
999
+ var migration = el('migration-input').value.trim();
1000
+ if (!migration) {
1001
+ el('migration-risk-badge').innerHTML = '';
1002
+ el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-warning">Please paste a Rails migration or DDL statement above.</div>';
1003
+ show(el('migration-result'));
1004
+ return;
1005
+ }
1006
+ migrationBtn.disabled = true;
1007
+ migrationBtn.innerHTML = '<span class="mg-spinner"></span> Assessing...';
1008
+ hide(el('migration-result'));
1009
+ aiCall(ROUTES.migration_risk, { migration: migration }, function(data) {
1010
+ var level = (data.risk_level || '').toLowerCase();
1011
+ var badgeClass = level === 'critical' ? 'mg-badge-danger' : level === 'high' ? 'mg-badge-danger' : level === 'medium' ? 'mg-badge-warning' : 'mg-badge-info';
1012
+ el('migration-risk-badge').innerHTML = level ? '<span class="mg-badge ' + badgeClass + '" style="font-size:14px;padding:4px 12px;">Risk: ' + level.toUpperCase() + '</span>' : '';
1013
+ el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '');
1014
+ show(el('migration-result'));
1015
+ migrationBtn.disabled = false;
1016
+ migrationBtn.innerHTML = '&#9889; Assess Risk';
1017
+ }, function(err) {
1018
+ el('migration-risk-badge').innerHTML = '';
1019
+ el('migration-result-content').innerHTML = '<div class="mg-alert mg-alert-danger">' + escHtml(err) + '</div>';
1020
+ show(el('migration-result'));
1021
+ migrationBtn.disabled = false;
1022
+ migrationBtn.innerHTML = '&#9889; Assess Risk';
1023
+ });
1024
+ });
1025
+ }
1026
+
1027
+ // --- SQL to Builder sync ---
1028
+
1029
+ function parseSqlToBuilder(sql) {
1030
+ if (!sql) return;
1031
+ if (sql.match(/\b(JOIN|GROUP\s+BY|HAVING|UNION)\b/i) || (sql.match(/SELECT/gi) || []).length > 1) return;
1032
+ var tableMatch = sql.match(/FROM\s+`?(\w+)`?/i);
1033
+ if (!tableMatch) return;
1034
+ var tableName = tableMatch[1];
1035
+ if (!qs('#vb-table option[value="' + tableName + '"]')) return;
1036
+
1037
+ var colMatch = sql.match(/SELECT\s+(.*?)\s+FROM/i);
1038
+ var selectedCols = [];
1039
+ if (colMatch && colMatch[1].trim() !== '*') {
1040
+ colMatch[1].split(',').forEach(function(p) { var n = p.trim().replace(/`/g, ''); if (n) selectedCols.push(n); });
1041
+ }
1042
+
1043
+ var whereMatch = sql.match(/WHERE\s+(.*?)(?:\s+ORDER\s+BY\b|\s+LIMIT\b|\s*$)/i);
1044
+ var conditions = whereMatch ? parseWhereClause(whereMatch[1]) : [];
1045
+
1046
+ var orderMatch = sql.match(/ORDER\s+BY\s+(.*?)(?:\s+LIMIT\b|\s*$)/i);
1047
+ var orders = [];
1048
+ if (orderMatch) {
1049
+ orderMatch[1].split(',').forEach(function(p) {
1050
+ var m = p.trim().match(/`?(\w+)`?\s*(ASC|DESC)?/i);
1051
+ if (m) orders.push({ column: m[1], direction: (m[2] || 'ASC').toUpperCase() });
1052
+ });
1053
+ }
1054
+
1055
+ el('vb-table').value = tableName;
1056
+ loadColumnsForTable(tableName, function() {
1057
+ if (selectedCols.length) {
1058
+ qsa('.vb-col-check').forEach(function(c) { c.checked = selectedCols.indexOf(c.value) !== -1; });
1059
+ }
1060
+ el('vb-filters').innerHTML = '';
1061
+ conditions.forEach(function(cond) {
1062
+ addFilterRow(true);
1063
+ var row = el('vb-filters').lastElementChild;
1064
+ qs('.vb-filter-col', row).value = cond.column;
1065
+ var colType = columnTypeMap[cond.column] || 'string';
1066
+ var opSel = qs('.vb-filter-op', row);
1067
+ opSel.innerHTML = operatorsForType(colType).map(function(o) { return '<option>' + o + '</option>'; }).join('');
1068
+ opSel.value = cond.operator;
1069
+ updateValueInput(row, colType, cond.operator);
1070
+ if (cond.operator !== 'IS NULL' && cond.operator !== 'IS NOT NULL') {
1071
+ var valEl = qs('.vb-filter-val', row);
1072
+ if (valEl) valEl.value = cond.value;
1073
+ if (cond.operator === 'BETWEEN') { var endEl = qs('.vb-filter-val-end', row); if (endEl) endEl.value = cond.endValue; }
1074
+ }
1075
+ });
1076
+ el('vb-orders').innerHTML = '';
1077
+ orders.forEach(function(ord) {
1078
+ addOrderRow(true);
1079
+ var row = el('vb-orders').lastElementChild;
1080
+ qs('.vb-order-col', row).value = ord.column;
1081
+ qs('.vb-order-dir', row).value = ord.direction;
1082
+ });
1083
+ updateGeneratedSql();
1084
+ });
1085
+ }
1086
+
1087
+ function parseWhereClause(str) {
1088
+ var conditions = [];
1089
+ var betweenRe = /`?(\w+)`?\s+BETWEEN\s+'([^']*)'\s+AND\s+'([^']*)'/gi;
1090
+ var m;
1091
+ while ((m = betweenRe.exec(str)) !== null) conditions.push({ column: m[1], operator: 'BETWEEN', value: m[2], endValue: m[3] });
1092
+ var remaining = str.replace(betweenRe, '{{B}}');
1093
+ remaining.split(/\s+AND\s+/i).forEach(function(part) {
1094
+ part = part.trim();
1095
+ if (!part || part === '{{B}}') return;
1096
+ var m;
1097
+ if ((m = part.match(/`?(\w+)`?\s+IS\s+NOT\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NOT NULL', value: '' }); return; }
1098
+ if ((m = part.match(/`?(\w+)`?\s+IS\s+NULL/i))) { conditions.push({ column: m[1], operator: 'IS NULL', value: '' }); return; }
1099
+ if ((m = part.match(/`?(\w+)`?\s+LIKE\s+'([^']*)'/i))) { conditions.push({ column: m[1], operator: 'LIKE', value: m[2] }); return; }
1100
+ if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*'([^']*)'/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
1101
+ if ((m = part.match(/`?(\w+)`?\s*(!=|>=|<=|=|>|<)\s*(\d+\.?\d*)/))) { conditions.push({ column: m[1], operator: m[2], value: m[3] }); return; }
1102
+ });
1103
+ return conditions;
1104
+ }
1105
+
1106
+ // --- Utilities ---
1107
+
1108
+ function setBtnLoading(ids, loading) {
1109
+ ids.forEach(function(id) {
1110
+ var btn = el(id);
1111
+ if (!btn) return;
1112
+ btn.disabled = loading;
1113
+ if (loading) { btn.dataset.origHtml = btn.innerHTML; btn.innerHTML = '<span class="mg-spinner"></span>'; }
1114
+ else if (btn.dataset.origHtml) { btn.innerHTML = btn.dataset.origHtml; }
1115
+ });
1116
+ }
1117
+
1118
+ function formatMarkdown(text) {
1119
+ if (!text) return '';
1120
+ // Normalize literal \n from JSON strings to actual newlines
1121
+ text = text.replace(/\\n/g, '\n');
1122
+ return text
1123
+ .replace(/```sql\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
1124
+ .replace(/```\n?([\s\S]*?)```/g, '<pre class="mg-pre"><code>$1</code></pre>')
1125
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
1126
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
1127
+ .replace(/^####\s+(.+)$/gm, '<h5 style="margin:12px 0 4px;">$1</h5>')
1128
+ .replace(/^###\s+(.+)$/gm, '<h4 style="margin:16px 0 6px;">$1</h4>')
1129
+ .replace(/^##\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
1130
+ .replace(/^#\s+(.+)$/gm, '<h3 style="margin:20px 0 8px;">$1</h3>')
1131
+ .replace(/^---+$/gm, '<hr style="margin:12px 0;">')
1132
+ .replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>')
1133
+ .replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>')
1134
+ .replace(/(<li>[\s\S]*?<\/li>)/g, '<ul style="margin:4px 0 4px 16px;padding:0;">$1</ul>')
1135
+ .replace(/<\/ul>\s*<ul[^>]*>/g, '')
1136
+ .replace(/\n\n/g, '<br><br>')
1137
+ .replace(/\n/g, '<br>');
1138
+ }
1139
+
1140
+ function formatFindings(text) {
1141
+ if (!text) return '<div class="mg-text-muted">No findings.</div>';
1142
+ text = text.replace(/\\n/g, '\n');
1143
+
1144
+ // Split into severity sections
1145
+ var sections = [];
1146
+ var current = null;
1147
+ text.split('\n').forEach(function(line) {
1148
+ var heading = line.match(/^#{1,3}\s+(.+)/);
1149
+ if (heading) {
1150
+ var title = heading[1].replace(/\*\*/g, '');
1151
+ var severity = 'info';
1152
+ var titleLower = title.toLowerCase();
1153
+ if (titleLower.indexOf('critical') !== -1) severity = 'danger';
1154
+ else if (titleLower.indexOf('warning') !== -1) severity = 'warning';
1155
+ else if (titleLower.indexOf('suggestion') !== -1 || titleLower.indexOf('info') !== -1) severity = 'info';
1156
+ current = { title: title, severity: severity, lines: [] };
1157
+ sections.push(current);
1158
+ } else if (current) {
1159
+ current.lines.push(line);
1160
+ } else {
1161
+ if (!sections.length) sections.push({ title: '', severity: 'info', lines: [] });
1162
+ if (!current) current = sections[0];
1163
+ current.lines.push(line);
1164
+ }
1165
+ });
1166
+
1167
+ if (!sections.length) return formatMarkdown(text);
1168
+
1169
+ var badgeColors = { danger: '#dc3545', warning: '#ffc107', info: '#17a2b8' };
1170
+ var bgColors = { danger: '#fff5f5', warning: '#fffbeb', info: '#f0f9ff' };
1171
+ var borderColors = { danger: '#f5c6cb', warning: '#ffeeba', info: '#bee5eb' };
1172
+
1173
+ return sections.map(function(sec) {
1174
+ var content = formatMarkdown(sec.lines.join('\n').trim());
1175
+ if (!content || content === '<br>') return '';
1176
+ var badge = badgeColors[sec.severity] || badgeColors.info;
1177
+ var bg = bgColors[sec.severity] || bgColors.info;
1178
+ var border = borderColors[sec.severity] || borderColors.info;
1179
+ return '<div class="mg-card mg-mb" style="border-left:4px solid ' + badge + ';background:' + bg + ';border-color:' + border + ';">' +
1180
+ (sec.title ? '<div class="mg-card-header" style="background:transparent;border-bottom:1px solid ' + border + ';"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
1181
+ '<div class="mg-card-body" style="font-size:13px;">' + content + '</div></div>';
1182
+ }).filter(function(s) { return s; }).join('');
1183
+ }
1184
+ })();
1185
+ </script>