mysql_genius 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +5 -0
- data/.github/workflows/ci.yml +30 -7
- data/.github/workflows/publish.yml +32 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +24 -0
- data/CHANGELOG.md +41 -0
- data/Gemfile +7 -2
- data/README.md +61 -218
- data/Rakefile +3 -1
- data/app/controllers/concerns/mysql_genius/ai_features.rb +90 -52
- data/app/controllers/concerns/mysql_genius/database_analysis.rb +81 -45
- data/app/controllers/concerns/mysql_genius/query_execution.rb +18 -16
- data/app/controllers/mysql_genius/base_controller.rb +3 -1
- data/app/controllers/mysql_genius/queries_controller.rb +19 -12
- data/app/services/mysql_genius/ai_client.rb +9 -2
- data/app/services/mysql_genius/ai_optimization_service.rb +8 -4
- data/app/services/mysql_genius/ai_suggestion_service.rb +5 -2
- data/app/views/layouts/mysql_genius/application.html.erb +147 -5
- data/app/views/mysql_genius/queries/_tab_dashboard.html.erb +95 -0
- data/app/views/mysql_genius/queries/_tab_duplicate_indexes.html.erb +11 -0
- data/app/views/mysql_genius/queries/_tab_query_explorer.html.erb +110 -0
- data/app/views/mysql_genius/queries/_tab_query_stats.html.erb +2 -2
- data/app/views/mysql_genius/queries/_tab_table_sizes.html.erb +7 -5
- data/app/views/mysql_genius/queries/_tab_unused_indexes.html.erb +11 -0
- data/app/views/mysql_genius/queries/index.html.erb +436 -52
- data/bin/console +1 -0
- data/config/routes.rb +2 -0
- data/docs/screenshots/dashboard.png +0 -0
- data/docs/screenshots/query_explore.png +0 -0
- data/docs/superpowers/plans/2026-04-08-dashboard-first-redesign.md +741 -0
- data/docs/superpowers/specs/2026-04-08-dashboard-first-redesign.md +87 -0
- data/lib/generators/mysql_genius/install/install_generator.rb +19 -0
- data/lib/generators/mysql_genius/install/templates/initializer.rb +56 -0
- data/lib/mysql_genius/configuration.rb +8 -6
- data/lib/mysql_genius/engine.rb +2 -0
- data/lib/mysql_genius/slow_query_monitor.rb +29 -25
- data/lib/mysql_genius/sql_validator.rb +6 -4
- data/lib/mysql_genius/version.rb +3 -1
- data/lib/mysql_genius.rb +2 -0
- data/mysql_genius.gemspec +9 -8
- metadata +23 -15
- data/app/views/mysql_genius/queries/_tab_sql_query.html.erb +0 -40
- data/app/views/mysql_genius/queries/_tab_visual_builder.html.erb +0 -61
- data/docs/screenshots/sql_query.png +0 -0
- data/docs/screenshots/visual_builder.png +0 -0
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
2
|
+
<h4>🐘 MySQLGenius</h4>
|
|
3
|
+
<button class="mg-theme-toggle" id="mg-theme-btn" title="Toggle dark/light theme" onclick="(function(){var d=document.documentElement,t=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',t);localStorage.setItem('mg-theme',t);document.getElementById('mg-theme-btn').textContent=t==='dark'?'\u2600\uFE0F':'\uD83C\uDF19';})()">
|
|
4
|
+
<script>document.write(document.documentElement.getAttribute('data-theme')==='dark'?'\u2600\uFE0F':'\uD83C\uDF19')</script>
|
|
5
|
+
</button>
|
|
6
|
+
</div>
|
|
2
7
|
|
|
3
8
|
<div class="mg-tabs">
|
|
4
|
-
<button class="mg-tab active" data-tab="
|
|
5
|
-
<button class="mg-tab" data-tab="sql">SQL Query</button>
|
|
9
|
+
<button class="mg-tab active" data-tab="dashboard">Dashboard</button>
|
|
6
10
|
<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
11
|
<button class="mg-tab" data-tab="qstats">Query Stats</button>
|
|
10
|
-
<button class="mg-tab" data-tab="unused">Unused Indexes</button>
|
|
11
12
|
<button class="mg-tab" data-tab="server">Server</button>
|
|
13
|
+
<button class="mg-tab" data-tab="tables">Tables</button>
|
|
14
|
+
<button class="mg-tab" data-tab="unused">Unused Indexes</button>
|
|
15
|
+
<button class="mg-tab" data-tab="indexes">Duplicate Indexes</button>
|
|
16
|
+
<button class="mg-tab" data-tab="explorer">Query Explorer</button>
|
|
12
17
|
<% if @ai_enabled %>
|
|
13
18
|
<button class="mg-tab" data-tab="aitools">AI Tools</button>
|
|
14
19
|
<% end %>
|
|
15
20
|
</div>
|
|
16
21
|
|
|
17
|
-
<%= render "mysql_genius/queries/
|
|
18
|
-
<%= render "mysql_genius/queries/tab_sql_query" %>
|
|
22
|
+
<%= render "mysql_genius/queries/tab_dashboard" %>
|
|
19
23
|
<%= render "mysql_genius/queries/tab_slow_queries" %>
|
|
20
|
-
<%= render "mysql_genius/queries/tab_duplicate_indexes" %>
|
|
21
|
-
<%= render "mysql_genius/queries/tab_table_sizes" %>
|
|
22
24
|
<%= render "mysql_genius/queries/tab_query_stats" %>
|
|
23
|
-
<%= render "mysql_genius/queries/tab_unused_indexes" %>
|
|
24
25
|
<%= render "mysql_genius/queries/tab_server" %>
|
|
26
|
+
<%= render "mysql_genius/queries/tab_table_sizes" %>
|
|
27
|
+
<%= render "mysql_genius/queries/tab_unused_indexes" %>
|
|
28
|
+
<%= render "mysql_genius/queries/tab_duplicate_indexes" %>
|
|
29
|
+
<%= render "mysql_genius/queries/tab_query_explorer" %>
|
|
25
30
|
<% if @ai_enabled %>
|
|
26
31
|
<%= render "mysql_genius/queries/tab_ai_tools" %>
|
|
27
32
|
<% end %>
|
|
@@ -53,6 +58,8 @@
|
|
|
53
58
|
migration_risk: '<%= mysql_genius.migration_risk_path %>'
|
|
54
59
|
};
|
|
55
60
|
|
|
61
|
+
var RAILS_MIGRATION_VERSION = '<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>';
|
|
62
|
+
|
|
56
63
|
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
57
64
|
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
|
58
65
|
var currentColumns = [];
|
|
@@ -69,6 +76,168 @@
|
|
|
69
76
|
function hide(e) { e.classList.add('mg-hidden'); }
|
|
70
77
|
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
71
78
|
|
|
79
|
+
// --- Table Sorting ---
|
|
80
|
+
|
|
81
|
+
function parseSortValue(text) {
|
|
82
|
+
var m = text.match(/([\d.]+)\s*(s|ms|KB|MB|GB)/i);
|
|
83
|
+
if (m) {
|
|
84
|
+
var n = parseFloat(m[1]);
|
|
85
|
+
var unit = m[2].toLowerCase();
|
|
86
|
+
if (unit === 's') return n * 1000;
|
|
87
|
+
if (unit === 'gb') return n * 1024;
|
|
88
|
+
if (unit === 'mb') return n;
|
|
89
|
+
if (unit === 'kb') return n / 1024;
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
return parseFloat(text.replace(/[^0-9.\-]/g, '')) || 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function makeSortable(table) {
|
|
96
|
+
if (table.dataset.sortable) return;
|
|
97
|
+
table.dataset.sortable = '1';
|
|
98
|
+
|
|
99
|
+
var headers = Array.from(table.querySelectorAll('th'));
|
|
100
|
+
headers.forEach(function(th, colIdx) {
|
|
101
|
+
if (th.dataset.noSort || th.textContent.trim() === '' || th.textContent.trim() === 'Actions') return;
|
|
102
|
+
th.classList.add('mg-sortable');
|
|
103
|
+
th.addEventListener('click', function() {
|
|
104
|
+
var tbody = table.querySelector('tbody');
|
|
105
|
+
if (!tbody) return;
|
|
106
|
+
var rows = Array.from(tbody.querySelectorAll('tr'));
|
|
107
|
+
if (rows.length === 0) return;
|
|
108
|
+
|
|
109
|
+
var asc = !th.classList.contains('mg-sort-asc');
|
|
110
|
+
headers.forEach(function(h) { h.classList.remove('mg-sort-asc', 'mg-sort-desc'); });
|
|
111
|
+
th.classList.add(asc ? 'mg-sort-asc' : 'mg-sort-desc');
|
|
112
|
+
|
|
113
|
+
var isNum = th.style.textAlign === 'right' || th.classList.contains('mg-num');
|
|
114
|
+
|
|
115
|
+
rows.sort(function(a, b) {
|
|
116
|
+
var cellA = a.children[colIdx];
|
|
117
|
+
var cellB = b.children[colIdx];
|
|
118
|
+
if (!cellA || !cellB) return 0;
|
|
119
|
+
var valA = cellA.textContent.trim();
|
|
120
|
+
var valB = cellB.textContent.trim();
|
|
121
|
+
|
|
122
|
+
if (isNum) {
|
|
123
|
+
return asc ? parseSortValue(valA) - parseSortValue(valB) : parseSortValue(valB) - parseSortValue(valA);
|
|
124
|
+
}
|
|
125
|
+
return asc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
rows.forEach(function(row) { tbody.appendChild(row); });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- SQL Syntax Highlighting (single-pass tokenizer) ---
|
|
134
|
+
|
|
135
|
+
var SQL_KW_SET = {};
|
|
136
|
+
'SELECT FROM WHERE JOIN LEFT RIGHT INNER OUTER CROSS FULL ON AND OR NOT IN EXISTS BETWEEN LIKE IS NULL AS DISTINCT ALL UNION INTERSECT EXCEPT INSERT INTO VALUES UPDATE SET DELETE CREATE ALTER DROP TABLE INDEX VIEW TRIGGER PROCEDURE FUNCTION DATABASE SCHEMA IF THEN ELSE END CASE WHEN HAVING LIMIT OFFSET ASC DESC USING FORCE USE IGNORE WITH RECURSIVE OVER EXPLAIN ANALYZE SHOW DESCRIBE'.split(' ').forEach(function(w) { SQL_KW_SET[w] = true; });
|
|
137
|
+
|
|
138
|
+
var SQL_FN_SET = {};
|
|
139
|
+
'COUNT SUM AVG MIN MAX COALESCE IFNULL NULLIF CAST CONVERT CONCAT CONCAT_WS GROUP_CONCAT SUBSTRING SUBSTR REPLACE TRIM LTRIM RTRIM UPPER LOWER UCASE LCASE LENGTH CHAR_LENGTH NOW CURDATE CURTIME DATE_FORMAT DATE_ADD DATE_SUB DATEDIFF TIMESTAMPDIFF UNIX_TIMESTAMP FROM_UNIXTIME IF ELT FIELD FIND_IN_SET FORMAT HEX UNHEX MD5 SHA1 SHA2 AES_ENCRYPT AES_DECRYPT ROUND CEIL CEILING FLOOR ABS MOD POWER SQRT LOG LOG2 LOG10 RAND GREATEST LEAST JSON_EXTRACT JSON_UNQUOTE JSON_SET JSON_OBJECT JSON_ARRAY JSON_CONTAINS JSON_LENGTH ROW_NUMBER RANK DENSE_RANK LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE'.split(' ').forEach(function(w) { SQL_FN_SET[w] = true; });
|
|
140
|
+
|
|
141
|
+
// Single-pass tokenizer regex: matches tokens in order of priority
|
|
142
|
+
// 1) single-quoted strings 2) backtick identifiers 3) numbers 4) words 5) operators 6) punctuation 7) ? placeholder 8) * star 9) whitespace 10) anything else
|
|
143
|
+
var SQL_TOKEN_RE = /'[^']*'|`[^`]*`|\b\d+(?:\.\d+)?\b|[A-Za-z_]\w*|\?\|>=|<=|<>|!=|[><=]|[(),;*]|\s+|./g;
|
|
144
|
+
|
|
145
|
+
function highlightSql(sql) {
|
|
146
|
+
if (!sql) return '';
|
|
147
|
+
var result = [];
|
|
148
|
+
var match;
|
|
149
|
+
SQL_TOKEN_RE.lastIndex = 0;
|
|
150
|
+
while ((match = SQL_TOKEN_RE.exec(sql)) !== null) {
|
|
151
|
+
var tok = match[0];
|
|
152
|
+
var ch = tok.charAt(0);
|
|
153
|
+
|
|
154
|
+
if (ch === "'") {
|
|
155
|
+
// String literal
|
|
156
|
+
result.push('<span class="mg-sql-str">' + escHtml(tok) + '</span>');
|
|
157
|
+
} else if (ch === '`') {
|
|
158
|
+
// Backtick-quoted identifier
|
|
159
|
+
result.push('<span class="mg-sql-tbl">' + escHtml(tok) + '</span>');
|
|
160
|
+
} else if (/^\d/.test(tok)) {
|
|
161
|
+
// Number
|
|
162
|
+
result.push('<span class="mg-sql-num">' + escHtml(tok) + '</span>');
|
|
163
|
+
} else if (/^[A-Za-z_]/.test(tok)) {
|
|
164
|
+
// Word: check if keyword, function, or plain identifier
|
|
165
|
+
var upper = tok.toUpperCase();
|
|
166
|
+
// Check for compound keywords (GROUP BY, ORDER BY, PARTITION BY)
|
|
167
|
+
var rest = sql.substring(SQL_TOKEN_RE.lastIndex);
|
|
168
|
+
var compoundMatch = rest.match(/^(\s+)(BY)\b/i);
|
|
169
|
+
if ((upper === 'GROUP' || upper === 'ORDER' || upper === 'PARTITION') && compoundMatch) {
|
|
170
|
+
result.push('<span class="mg-sql-kw">' + escHtml(tok) + escHtml(compoundMatch[1]) + escHtml(compoundMatch[2].toUpperCase()) + '</span>');
|
|
171
|
+
SQL_TOKEN_RE.lastIndex += compoundMatch[0].length;
|
|
172
|
+
} else if (SQL_FN_SET[upper] && rest.match(/^\s*\(/)) {
|
|
173
|
+
result.push('<span class="mg-sql-fn">' + escHtml(tok.toUpperCase()) + '</span>');
|
|
174
|
+
} else if (SQL_KW_SET[upper]) {
|
|
175
|
+
result.push('<span class="mg-sql-kw">' + escHtml(tok.toUpperCase()) + '</span>');
|
|
176
|
+
} else {
|
|
177
|
+
result.push(escHtml(tok));
|
|
178
|
+
}
|
|
179
|
+
} else if (tok === '?') {
|
|
180
|
+
result.push('<span class="mg-sql-placeholder">?</span>');
|
|
181
|
+
} else if (tok === '*') {
|
|
182
|
+
result.push('<span class="mg-sql-star">*</span>');
|
|
183
|
+
} else if (/^(>=|<=|<>|!=|[><=])$/.test(tok)) {
|
|
184
|
+
result.push('<span class="mg-sql-op">' + escHtml(tok) + '</span>');
|
|
185
|
+
} else if (/^[(),;]$/.test(tok)) {
|
|
186
|
+
result.push('<span class="mg-sql-punc">' + escHtml(tok) + '</span>');
|
|
187
|
+
} else {
|
|
188
|
+
// Whitespace or unknown
|
|
189
|
+
result.push(escHtml(tok));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result.join('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function sqlBlock(sql, maxLen) {
|
|
196
|
+
var truncated = maxLen && sql.length > maxLen;
|
|
197
|
+
var displaySql = truncated ? sql.substring(0, maxLen) + '...' : sql;
|
|
198
|
+
return '<code class="mg-sql-block" title="' + escHtml(sql) + '">' + highlightSql(displaySql) + '</code>';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Duration Formatting with Color ---
|
|
202
|
+
|
|
203
|
+
function formatDurationStyled(ms) {
|
|
204
|
+
var text = formatDuration(ms);
|
|
205
|
+
var cls = 'mg-dur-fast';
|
|
206
|
+
if (ms >= 1000) cls = 'mg-dur-slow';
|
|
207
|
+
else if (ms >= 100) cls = 'mg-dur-moderate';
|
|
208
|
+
return '<span class="' + cls + '">' + text + '</span>';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function migrationTimestamp() {
|
|
212
|
+
var now = new Date();
|
|
213
|
+
return now.getFullYear().toString() +
|
|
214
|
+
('0' + (now.getMonth() + 1)).slice(-2) +
|
|
215
|
+
('0' + now.getDate()).slice(-2) +
|
|
216
|
+
('0' + now.getHours()).slice(-2) +
|
|
217
|
+
('0' + now.getMinutes()).slice(-2) +
|
|
218
|
+
('0' + now.getSeconds()).slice(-2);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function copyToClipboard(text, btn) {
|
|
222
|
+
var done = function() {
|
|
223
|
+
btn.innerHTML = '✓ Copied';
|
|
224
|
+
setTimeout(function() { btn.innerHTML = '📋 Copy'; }, 2000);
|
|
225
|
+
};
|
|
226
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
227
|
+
navigator.clipboard.writeText(text).then(done);
|
|
228
|
+
} else {
|
|
229
|
+
var ta = document.createElement('textarea');
|
|
230
|
+
ta.value = text;
|
|
231
|
+
ta.style.position = 'fixed';
|
|
232
|
+
ta.style.opacity = '0';
|
|
233
|
+
document.body.appendChild(ta);
|
|
234
|
+
ta.select();
|
|
235
|
+
document.execCommand('copy');
|
|
236
|
+
document.body.removeChild(ta);
|
|
237
|
+
done();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
72
241
|
function ajax(method, url, data, onSuccess, onError) {
|
|
73
242
|
var xhr = new XMLHttpRequest();
|
|
74
243
|
xhr.open(method, url, true);
|
|
@@ -117,18 +286,191 @@
|
|
|
117
286
|
|
|
118
287
|
// --- Tabs ---
|
|
119
288
|
|
|
289
|
+
function activateTab(name) {
|
|
290
|
+
qsa('.mg-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
291
|
+
qsa('.mg-tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
292
|
+
var btn = qs('[data-tab="' + name + '"]');
|
|
293
|
+
if (!btn) { name = 'dashboard'; btn = qs('[data-tab="dashboard"]'); }
|
|
294
|
+
btn.classList.add('active');
|
|
295
|
+
el('tab-' + name).classList.add('active');
|
|
296
|
+
history.replaceState(null, '', location.pathname + '#' + name);
|
|
297
|
+
if (name === 'dashboard') loadDashboard();
|
|
298
|
+
if (name === 'slow') loadSlowQueries();
|
|
299
|
+
if (name === 'indexes') loadDuplicateIndexes();
|
|
300
|
+
if (name === 'tables') loadTableSizes();
|
|
301
|
+
if (name === 'qstats') loadQueryStats();
|
|
302
|
+
if (name === 'unused') loadUnusedIndexes();
|
|
303
|
+
if (name === 'server') loadServerOverview();
|
|
304
|
+
}
|
|
305
|
+
|
|
120
306
|
qsa('.mg-tab').forEach(function(tab) {
|
|
121
|
-
tab.addEventListener('click', function() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
307
|
+
tab.addEventListener('click', function() { activateTab(tab.dataset.tab); });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// --- Dashboard ---
|
|
311
|
+
|
|
312
|
+
function loadDashboard() {
|
|
313
|
+
show(el('dash-loading'));
|
|
314
|
+
hide(el('dash-content')); hide(el('dash-error'));
|
|
315
|
+
// Reset previous data
|
|
316
|
+
hide(el('dash-slow-table')); hide(el('dash-slow-empty'));
|
|
317
|
+
hide(el('dash-qstats-table')); hide(el('dash-qstats-empty')); hide(el('dash-qstats-error'));
|
|
318
|
+
el('dash-slow-tbody').innerHTML = '';
|
|
319
|
+
el('dash-qstats-tbody').innerHTML = '';
|
|
320
|
+
el('dash-dup-count').textContent = '--';
|
|
321
|
+
el('dash-unused-count').textContent = '--';
|
|
322
|
+
|
|
323
|
+
var loaded = { server: false, slow: false, qstats: false, dup: false, unused: false };
|
|
324
|
+
|
|
325
|
+
function checkAllLoaded() {
|
|
326
|
+
if (!loaded.server || !loaded.slow || !loaded.qstats || !loaded.dup || !loaded.unused) return;
|
|
327
|
+
hide(el('dash-loading'));
|
|
328
|
+
show(el('dash-content'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Server overview
|
|
332
|
+
ajaxGet(ROUTES.server_overview, {}, function(data) {
|
|
333
|
+
if (data.error) { loaded.server = true; checkAllLoaded(); return; }
|
|
334
|
+
var s = data.server;
|
|
335
|
+
var c = data.connections;
|
|
336
|
+
var db = data.innodb;
|
|
337
|
+
var q = data.queries;
|
|
338
|
+
|
|
339
|
+
el('dash-server-info').innerHTML =
|
|
340
|
+
statRow('Version', '<code>' + escHtml(s.version) + '</code>') +
|
|
341
|
+
statRow('Uptime', escHtml(s.uptime)) +
|
|
342
|
+
statRow('Queries/sec', q.qps);
|
|
343
|
+
|
|
344
|
+
el('dash-conn-bar').innerHTML = usageBar(c.usage_pct, c.current + ' / ' + c.max + ' (' + c.usage_pct + '%)');
|
|
345
|
+
el('dash-conn-info').innerHTML =
|
|
346
|
+
statRow('Threads Running', c.threads_running) +
|
|
347
|
+
statRow('Max Used', c.max_used);
|
|
348
|
+
|
|
349
|
+
var poolUsedPct = db.buffer_pool_pages_total > 0
|
|
350
|
+
? (((db.buffer_pool_pages_total - db.buffer_pool_pages_free) / db.buffer_pool_pages_total) * 100).toFixed(1)
|
|
351
|
+
: 0;
|
|
352
|
+
el('dash-innodb-bar').innerHTML = usageBar(parseFloat(poolUsedPct), db.buffer_pool_mb + ' MB (' + poolUsedPct + '% used)');
|
|
353
|
+
el('dash-innodb-info').innerHTML =
|
|
354
|
+
statRow('Hit Rate', db.buffer_pool_hit_rate + '%') +
|
|
355
|
+
statRow('Dirty Pages', Number(db.buffer_pool_pages_dirty).toLocaleString());
|
|
356
|
+
|
|
357
|
+
var tmpBadge = q.tmp_disk_pct > 25
|
|
358
|
+
? '<span class="mg-badge mg-badge-danger">' + q.tmp_disk_pct + '%</span>'
|
|
359
|
+
: q.tmp_disk_pct + '%';
|
|
360
|
+
el('dash-query-info').innerHTML =
|
|
361
|
+
statRow('Slow Queries', Number(q.slow_queries).toLocaleString()) +
|
|
362
|
+
statRow('Tmp Disk Tables', tmpBadge);
|
|
363
|
+
|
|
364
|
+
loaded.server = true;
|
|
365
|
+
checkAllLoaded();
|
|
366
|
+
}, function() { loaded.server = true; checkAllLoaded(); });
|
|
367
|
+
|
|
368
|
+
// Top 5 slow queries
|
|
369
|
+
ajaxGet(ROUTES.slow_queries, {}, function(data) {
|
|
370
|
+
if (!data || !data.length) {
|
|
371
|
+
el('dash-slow-empty').textContent = 'No slow queries recorded.';
|
|
372
|
+
show(el('dash-slow-empty'));
|
|
373
|
+
} else {
|
|
374
|
+
var top5 = data.slice(0, 5);
|
|
375
|
+
el('dash-slow-tbody').innerHTML = top5.map(function(q) {
|
|
376
|
+
var d = q.duration_ms;
|
|
377
|
+
var cls = d >= 2000 ? 'mg-badge-danger' : d >= 1000 ? 'mg-badge-warning' : 'mg-badge-info';
|
|
378
|
+
return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
|
|
379
|
+
'<td><small>' + escHtml(q.timestamp) + '</small></td>' +
|
|
380
|
+
'<td>' + sqlBlock(q.sql, 120) + '</td></tr>';
|
|
381
|
+
}).join('');
|
|
382
|
+
show(el('dash-slow-table'));
|
|
383
|
+
}
|
|
384
|
+
loaded.slow = true;
|
|
385
|
+
checkAllLoaded();
|
|
386
|
+
}, function() {
|
|
387
|
+
el('dash-slow-empty').textContent = 'Configure redis_url to monitor slow queries.';
|
|
388
|
+
show(el('dash-slow-empty'));
|
|
389
|
+
loaded.slow = true;
|
|
390
|
+
checkAllLoaded();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Top 5 expensive queries
|
|
394
|
+
ajaxGet(ROUTES.query_stats, { sort: 'total_time', limit: 5 }, function(data) {
|
|
395
|
+
if (data.error) {
|
|
396
|
+
el('dash-qstats-error').innerHTML = '<div class="mg-text-muted">' + escHtml(data.error) + '</div>';
|
|
397
|
+
show(el('dash-qstats-error'));
|
|
398
|
+
} else if (!data.length) {
|
|
399
|
+
el('dash-qstats-empty').textContent = 'No query statistics available.';
|
|
400
|
+
show(el('dash-qstats-empty'));
|
|
401
|
+
} else {
|
|
402
|
+
el('dash-qstats-tbody').innerHTML = data.map(function(q) {
|
|
403
|
+
return '<tr>' +
|
|
404
|
+
'<td>' + sqlBlock(q.sql, 120) + '</td>' +
|
|
405
|
+
'<td class="mg-num">' + Number(q.calls).toLocaleString() + '</td>' +
|
|
406
|
+
'<td class="mg-num">' + formatDurationStyled(q.total_time_ms) + '</td>' +
|
|
407
|
+
'<td class="mg-num">' + formatDurationStyled(q.avg_time_ms) + '</td></tr>';
|
|
408
|
+
}).join('');
|
|
409
|
+
show(el('dash-qstats-table'));
|
|
410
|
+
}
|
|
411
|
+
loaded.qstats = true;
|
|
412
|
+
checkAllLoaded();
|
|
413
|
+
}, function() {
|
|
414
|
+
el('dash-qstats-empty').textContent = 'Query statistics unavailable.';
|
|
415
|
+
show(el('dash-qstats-empty'));
|
|
416
|
+
loaded.qstats = true;
|
|
417
|
+
checkAllLoaded();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Duplicate indexes count
|
|
421
|
+
ajaxGet(ROUTES.duplicate_indexes, {}, function(data) {
|
|
422
|
+
var count = Array.isArray(data) ? data.length : 0;
|
|
423
|
+
var countEl = el('dash-dup-count');
|
|
424
|
+
if (count === 0) {
|
|
425
|
+
countEl.innerHTML = '<span style="color:#28a745;">✓ 0</span>';
|
|
426
|
+
} else {
|
|
427
|
+
countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
|
|
428
|
+
}
|
|
429
|
+
loaded.dup = true;
|
|
430
|
+
checkAllLoaded();
|
|
431
|
+
}, function() { el('dash-dup-count').textContent = '?'; loaded.dup = true; checkAllLoaded(); });
|
|
432
|
+
|
|
433
|
+
// Unused indexes count
|
|
434
|
+
ajaxGet(ROUTES.unused_indexes, {}, function(data) {
|
|
435
|
+
var count = Array.isArray(data) ? data.length : 0;
|
|
436
|
+
var countEl = el('dash-unused-count');
|
|
437
|
+
if (count === 0) {
|
|
438
|
+
countEl.innerHTML = '<span style="color:#28a745;">✓ 0</span>';
|
|
439
|
+
} else {
|
|
440
|
+
countEl.innerHTML = '<span style="color:#dc3545;">' + count + '</span>';
|
|
441
|
+
}
|
|
442
|
+
loaded.unused = true;
|
|
443
|
+
checkAllLoaded();
|
|
444
|
+
}, function() { el('dash-unused-count').textContent = '?'; loaded.unused = true; checkAllLoaded(); });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Dashboard tab-jump buttons
|
|
448
|
+
document.addEventListener('click', function(e) {
|
|
449
|
+
var jumpBtn = e.target.closest('.dash-jump-tab');
|
|
450
|
+
if (jumpBtn) { activateTab(jumpBtn.dataset.target); }
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Restore active tab from URL hash, or default to dashboard
|
|
454
|
+
var initialTab = (location.hash || '').replace('#', '') || 'dashboard';
|
|
455
|
+
activateTab(initialTab);
|
|
456
|
+
|
|
457
|
+
// --- Query Explorer Mode Toggle ---
|
|
458
|
+
|
|
459
|
+
qsa('.qe-mode').forEach(function(btn) {
|
|
460
|
+
btn.addEventListener('click', function() {
|
|
461
|
+
qsa('.qe-mode').forEach(function(b) {
|
|
462
|
+
b.classList.remove('active');
|
|
463
|
+
b.classList.add('mg-btn-outline');
|
|
464
|
+
});
|
|
465
|
+
btn.classList.add('active');
|
|
466
|
+
btn.classList.remove('mg-btn-outline');
|
|
467
|
+
if (btn.dataset.mode === 'visual') {
|
|
468
|
+
show(el('qe-visual'));
|
|
469
|
+
hide(el('qe-sql'));
|
|
470
|
+
} else {
|
|
471
|
+
hide(el('qe-visual'));
|
|
472
|
+
show(el('qe-sql'));
|
|
473
|
+
}
|
|
132
474
|
});
|
|
133
475
|
});
|
|
134
476
|
|
|
@@ -498,15 +840,14 @@
|
|
|
498
840
|
el('slow-tbody').innerHTML = data.map(function(q) {
|
|
499
841
|
var d = q.duration_ms;
|
|
500
842
|
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
843
|
return '<tr><td><span class="mg-badge ' + cls + '">' + d + ' ms</span></td>' +
|
|
504
844
|
'<td><small>' + escHtml(q.timestamp) + '</small></td>' +
|
|
505
|
-
'<td
|
|
506
|
-
'<td><button class="mg-btn mg-btn-outline mg-btn-sm slow-explain-btn" data-sql="' +
|
|
507
|
-
'<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' +
|
|
845
|
+
'<td>' + sqlBlock(q.sql, 200) + '</td>' +
|
|
846
|
+
'<td><button class="mg-btn mg-btn-outline mg-btn-sm slow-explain-btn" data-sql="' + escHtml(q.sql).replace(/"/g, '"') + '">Explain</button> ' +
|
|
847
|
+
'<button class="mg-btn mg-btn-outline-secondary mg-btn-sm slow-use-btn" data-sql="' + escHtml(q.sql).replace(/"/g, '"') + '">Use</button></td></tr>';
|
|
508
848
|
}).join('');
|
|
509
849
|
show(el('slow-table-wrapper'));
|
|
850
|
+
makeSortable(qs('#slow-table-wrapper .mg-table'));
|
|
510
851
|
}, function() {
|
|
511
852
|
hide(el('slow-loading'));
|
|
512
853
|
el('slow-empty').textContent = 'Failed to load slow queries.';
|
|
@@ -522,10 +863,13 @@
|
|
|
522
863
|
var useBtn = e.target.closest('.slow-use-btn');
|
|
523
864
|
if (useBtn) {
|
|
524
865
|
el('sql-input').value = useBtn.dataset.sql;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
866
|
+
activateTab('explorer');
|
|
867
|
+
// Switch to SQL mode within Query Explorer
|
|
868
|
+
qsa('.qe-mode').forEach(function(b) { b.classList.remove('active'); b.classList.add('mg-btn-outline'); });
|
|
869
|
+
qs('.qe-mode[data-mode="sql"]').classList.add('active');
|
|
870
|
+
qs('.qe-mode[data-mode="sql"]').classList.remove('mg-btn-outline');
|
|
871
|
+
hide(el('qe-visual'));
|
|
872
|
+
show(el('qe-sql'));
|
|
529
873
|
}
|
|
530
874
|
});
|
|
531
875
|
|
|
@@ -548,10 +892,22 @@
|
|
|
548
892
|
'<td>' + d.duplicate_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
549
893
|
'<td><code>' + escHtml(d.covered_by_index) + '</code></td>' +
|
|
550
894
|
'<td>' + d.covered_by_columns.map(function(c) { return '<code>' + escHtml(c) + '</code>'; }).join(', ') + '</td>' +
|
|
551
|
-
'<td
|
|
895
|
+
'<td>' + sqlBlock(dropSql, 0) + '</td>' +
|
|
552
896
|
'</tr>';
|
|
553
897
|
}).join('');
|
|
554
898
|
show(el('dup-table-wrapper'));
|
|
899
|
+
makeSortable(qs('#dup-table-wrapper .mg-table'));
|
|
900
|
+
|
|
901
|
+
// Generate migration
|
|
902
|
+
var ts = migrationTimestamp();
|
|
903
|
+
var migrationLines = ['# ' + ts + '_remove_duplicate_indexes.rb', '',
|
|
904
|
+
'class RemoveDuplicateIndexes < ActiveRecord::Migration[' + RAILS_MIGRATION_VERSION + ']', ' def change'];
|
|
905
|
+
data.forEach(function(d) {
|
|
906
|
+
migrationLines.push(' remove_index :' + d.table + ', name: :' + d.duplicate_index);
|
|
907
|
+
});
|
|
908
|
+
migrationLines.push(' end', 'end');
|
|
909
|
+
el('dup-migration-code').textContent = migrationLines.join('\n');
|
|
910
|
+
show(el('dup-migration'));
|
|
555
911
|
}, function() {
|
|
556
912
|
hide(el('dup-loading'));
|
|
557
913
|
el('dup-empty').textContent = 'Failed to scan indexes.';
|
|
@@ -560,6 +916,9 @@
|
|
|
560
916
|
}
|
|
561
917
|
|
|
562
918
|
el('dup-refresh').addEventListener('click', loadDuplicateIndexes);
|
|
919
|
+
el('dup-copy-migration').addEventListener('click', function() {
|
|
920
|
+
copyToClipboard(el('dup-migration-code').textContent, el('dup-copy-migration'));
|
|
921
|
+
});
|
|
563
922
|
|
|
564
923
|
// --- Table Sizes ---
|
|
565
924
|
|
|
@@ -571,7 +930,7 @@
|
|
|
571
930
|
|
|
572
931
|
function sizeBar(pct, color) {
|
|
573
932
|
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>';
|
|
933
|
+
'<div style="background:' + color + ';border-radius:3px;height:8px;width:' + Math.max(pct, 1) + '%;transition:width 0.3s;"></div></div>';
|
|
575
934
|
}
|
|
576
935
|
|
|
577
936
|
var PIE_COLORS = [
|
|
@@ -642,17 +1001,27 @@
|
|
|
642
1001
|
var pct = (t.total_mb / maxMb) * 100;
|
|
643
1002
|
var color = t.total_mb >= 100 ? '#dc3545' : t.total_mb >= 10 ? '#ffc107' : '#28a745';
|
|
644
1003
|
var rows = t.rows != null ? Number(t.rows).toLocaleString() : '?';
|
|
1004
|
+
var fragText = t.fragmented_mb > 0 ? formatMb(t.fragmented_mb) : '<span class="mg-text-muted">\u2014</span>';
|
|
1005
|
+
if (t.needs_optimize) fragText += ' <span class="mg-badge mg-badge-warning" title="Fragmentation >10% of total size">optimize</span>';
|
|
1006
|
+
var updated = t.updated_at ? new Date(t.updated_at).toLocaleDateString() : '<span class="mg-text-muted">\u2014</span>';
|
|
1007
|
+
var engineBadge = t.engine ? '<span class="mg-badge mg-badge-secondary">' + escHtml(t.engine) + '</span>' : '';
|
|
645
1008
|
return '<tr>' +
|
|
646
|
-
'<td><strong>' + escHtml(t.table) + '</strong
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
'
|
|
650
|
-
'<td
|
|
651
|
-
'<td
|
|
1009
|
+
'<td><strong>' + escHtml(t.table) + '</strong>' +
|
|
1010
|
+
(t.collation ? ' <span class="mg-text-muted" style="font-size:11px">' + escHtml(t.collation) + '</span>' : '') +
|
|
1011
|
+
(t.auto_increment ? ' <span class="mg-text-muted" style="font-size:11px">AI:' + Number(t.auto_increment).toLocaleString() + '</span>' : '') +
|
|
1012
|
+
'</td>' +
|
|
1013
|
+
'<td class="mg-num">' + rows + '</td>' +
|
|
1014
|
+
'<td>' + engineBadge + '</td>' +
|
|
1015
|
+
'<td class="mg-num">' + formatMb(t.data_mb) + '</td>' +
|
|
1016
|
+
'<td class="mg-num">' + formatMb(t.index_mb) + '</td>' +
|
|
1017
|
+
'<td class="mg-num"><strong>' + formatMb(t.total_mb) + '</strong></td>' +
|
|
1018
|
+
'<td class="mg-num">' + fragText + '</td>' +
|
|
1019
|
+
'<td>' + updated + '</td>' +
|
|
652
1020
|
'<td>' + sizeBar(pct, color) + '</td>' +
|
|
653
1021
|
'</tr>';
|
|
654
1022
|
}).join('');
|
|
655
1023
|
show(el('sizes-table-wrapper'));
|
|
1024
|
+
makeSortable(qs('#sizes-table-wrapper .mg-table'));
|
|
656
1025
|
}, function() {
|
|
657
1026
|
hide(el('sizes-loading'));
|
|
658
1027
|
el('sizes-total').textContent = 'Failed to load';
|
|
@@ -679,19 +1048,19 @@
|
|
|
679
1048
|
el('qstats-count').textContent = data.length + ' queries';
|
|
680
1049
|
el('qstats-tbody').innerHTML = data.map(function(q) {
|
|
681
1050
|
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
1051
|
return '<tr>' +
|
|
684
|
-
'<td
|
|
685
|
-
'<td
|
|
686
|
-
'<td
|
|
687
|
-
'<td
|
|
688
|
-
'<td
|
|
689
|
-
'<td
|
|
690
|
-
'<td
|
|
691
|
-
'<td
|
|
1052
|
+
'<td>' + sqlBlock(q.sql, 120) + '</td>' +
|
|
1053
|
+
'<td class="mg-num">' + Number(q.calls).toLocaleString() + '</td>' +
|
|
1054
|
+
'<td class="mg-num">' + formatDurationStyled(q.total_time_ms) + '</td>' +
|
|
1055
|
+
'<td class="mg-num">' + formatDurationStyled(q.avg_time_ms) + '</td>' +
|
|
1056
|
+
'<td class="mg-num">' + formatDurationStyled(q.max_time_ms) + '</td>' +
|
|
1057
|
+
'<td class="mg-num">' + Number(q.rows_examined).toLocaleString() + '</td>' +
|
|
1058
|
+
'<td class="mg-num">' + Number(q.rows_sent).toLocaleString() + '</td>' +
|
|
1059
|
+
'<td class="mg-num">' + (ratioClass ? '<span class="mg-badge ' + ratioClass + '">' + q.rows_ratio + 'x</span>' : q.rows_ratio + 'x') + '</td>' +
|
|
692
1060
|
'</tr>';
|
|
693
1061
|
}).join('');
|
|
694
1062
|
show(el('qstats-table-wrapper'));
|
|
1063
|
+
makeSortable(qs('#qstats-table-wrapper .mg-table'));
|
|
695
1064
|
}, function(json) {
|
|
696
1065
|
hide(el('qstats-loading'));
|
|
697
1066
|
el('qstats-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load query stats.') + '</div>';
|
|
@@ -727,13 +1096,25 @@
|
|
|
727
1096
|
return '<tr>' +
|
|
728
1097
|
'<td><strong>' + escHtml(d.table) + '</strong></td>' +
|
|
729
1098
|
'<td><code>' + escHtml(d.index_name) + '</code></td>' +
|
|
730
|
-
'<td
|
|
731
|
-
'<td
|
|
732
|
-
'<td
|
|
733
|
-
'<td
|
|
1099
|
+
'<td class="mg-num">' + d.reads + '</td>' +
|
|
1100
|
+
'<td class="mg-num">' + Number(d.writes).toLocaleString() + '</td>' +
|
|
1101
|
+
'<td class="mg-num">' + Number(d.table_rows).toLocaleString() + '</td>' +
|
|
1102
|
+
'<td>' + sqlBlock(d.drop_sql, 0) + '</td>' +
|
|
734
1103
|
'</tr>';
|
|
735
1104
|
}).join('');
|
|
736
1105
|
show(el('unused-table-wrapper'));
|
|
1106
|
+
makeSortable(qs('#unused-table-wrapper .mg-table'));
|
|
1107
|
+
|
|
1108
|
+
// Generate migration
|
|
1109
|
+
var ts = migrationTimestamp();
|
|
1110
|
+
var migrationLines = ['# ' + ts + '_remove_unused_indexes.rb', '',
|
|
1111
|
+
'class RemoveUnusedIndexes < ActiveRecord::Migration[' + RAILS_MIGRATION_VERSION + ']', ' def change'];
|
|
1112
|
+
data.forEach(function(d) {
|
|
1113
|
+
migrationLines.push(' remove_index :' + d.table + ', name: :' + d.index_name);
|
|
1114
|
+
});
|
|
1115
|
+
migrationLines.push(' end', 'end');
|
|
1116
|
+
el('unused-migration-code').textContent = migrationLines.join('\n');
|
|
1117
|
+
show(el('unused-migration'));
|
|
737
1118
|
}, function(json) {
|
|
738
1119
|
hide(el('unused-loading'));
|
|
739
1120
|
el('unused-error').innerHTML = '<div class="mg-alert mg-alert-warning">' + escHtml((json && json.error) || 'Failed to load unused indexes.') + '</div>';
|
|
@@ -742,6 +1123,9 @@
|
|
|
742
1123
|
}
|
|
743
1124
|
|
|
744
1125
|
el('unused-refresh').addEventListener('click', loadUnusedIndexes);
|
|
1126
|
+
el('unused-copy-migration').addEventListener('click', function() {
|
|
1127
|
+
copyToClipboard(el('unused-migration-code').textContent, el('unused-copy-migration'));
|
|
1128
|
+
});
|
|
745
1129
|
|
|
746
1130
|
// --- Server Overview ---
|
|
747
1131
|
|
data/bin/console
CHANGED
data/config/routes.rb
CHANGED
|
Binary file
|
|
Binary file
|