Skip to content

Commit bdb3970

Browse files
Andrew Bakerclaude
andcommitted
v1.8.6 — Copy to clipboard (all tabs), Escape closes Explain, entity fixes, help panel improvements
- Add Copy button (electric pink) to panel header — copies active tab as plain text - Summary copy captures everything: env, DB/HTTP/log/cache/asset cards, plugin leaderboard, slowest queries, N+1 patterns, slowest HTTP, dupe queries, slowest hooks - Escape key closes help panel or collapses open EXPLAIN/detail rows - Fix build.sh sed dot-escaping bug that was corrupting HTML entities on version bump - Restore corrupted &#128308; and &#128200; entities in Issues/DB titles - Add overscroll-behavior:contain to help panel (scroll bleed fix) - Add Template and Transients cards to help panel - Electric pink accent on Copy and ? buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3791c14 commit bdb3970

File tree

4 files changed

+316
-3
lines changed

4 files changed

+316
-3
lines changed

assets/cs-perf-monitor.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
max-width: 520px;
166166
max-height: calc(100svh - 80px);
167167
overflow-y: auto;
168+
overscroll-behavior: contain;
168169
-webkit-overflow-scrolling: touch;
169170
background: #252526;
170171
border: 1px solid #007acc;
@@ -266,6 +267,17 @@
266267
}
267268
.cs-perf-btn:hover { background: #3c3c3c; color: #fff; border-color: #777; }
268269

270+
/* Copy + Help buttons — electric pink accent */
271+
#cs-perf-copy,
272+
.cs-perf-help-btn {
273+
background: #ff1493;
274+
border-color: #ff1493;
275+
color: #fff;
276+
font-weight: 700;
277+
}
278+
#cs-perf-copy:hover,
279+
.cs-perf-help-btn:hover { background: #ff45a8; border-color: #ff45a8; color: #fff; }
280+
269281
/* ── Panel body ──────────────────────────────────────────────────────────── */
270282
#cs-perf-body {
271283
display: flex;

assets/cs-perf-monitor.js

Lines changed: 292 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Features: call-chain trace, EXPLAIN on demand, N+1 detection,
66
* multi-column sort, colour-coded severity, export JSON.
77
*
8-
* @since 1.8.0
8+
* @since 1.8.6
99
*/
1010
(function () {
1111
'use strict';
@@ -1232,6 +1232,273 @@
12321232
}
12331233
}
12341234

1235+
// ── Copy current tab to clipboard ─────────────────────────────────────────
1236+
function copyCurrentTab() {
1237+
var tab = activeTab;
1238+
var lines = ['=== CS Monitor: ' + tab.toUpperCase() + ' ===', 'URL: ' + (meta.url || window.location.href), ''];
1239+
1240+
switch (tab) {
1241+
case 'issues':
1242+
if (issuesList.length === 0) {
1243+
lines.push('No issues detected.');
1244+
} else {
1245+
issuesList.forEach(function (issue) {
1246+
lines.push('[' + issue.sev.toUpperCase() + '] ' + issue.title
1247+
+ (issue.detail ? ' — ' + issue.detail : '')
1248+
+ ' (\u2192 ' + issue.tab + ')');
1249+
});
1250+
}
1251+
break;
1252+
case 'db':
1253+
lines.push('Queries: ' + filteredDB.length + ' / ' + data.queries.length);
1254+
lines.push('');
1255+
filteredDB.forEach(function (q, i) {
1256+
lines.push((i + 1) + '. [' + q.keyword + '] ' + q.sql.replace(/\s+/g, ' ').trim());
1257+
lines.push(' Plugin: ' + q.plugin + ' | Rows: ' + (q.rows >= 0 ? q.rows : '\u2013') + ' | Time: ' + fmtMs(q.time_ms));
1258+
if (q.is_dupe) lines.push(' [DUPLICATE]');
1259+
if (isN1(q.sql)) lines.push(' [N+1 PATTERN]');
1260+
});
1261+
break;
1262+
case 'http':
1263+
lines.push('HTTP calls: ' + filteredHTTP.length);
1264+
lines.push('');
1265+
filteredHTTP.forEach(function (h, i) {
1266+
lines.push((i + 1) + '. [' + (h.method || 'GET') + '] ' + h.url);
1267+
lines.push(' Plugin: ' + h.plugin + ' | Status: ' + (h.status || 'ERR') + ' | Time: ' + fmtMs(h.time_ms));
1268+
if (h.error) lines.push(' Error: ' + h.error);
1269+
});
1270+
break;
1271+
case 'logs':
1272+
var logs = data.logs || [];
1273+
lines.push('Log entries: ' + logs.length);
1274+
lines.push('');
1275+
logs.forEach(function (l) {
1276+
lines.push('[' + (l.level || 'info').toUpperCase() + '] ' + (l.message || '')
1277+
+ (l.file ? ' (' + l.file + (l.line ? ':' + l.line : '') + ')' : ''));
1278+
});
1279+
break;
1280+
case 'assets':
1281+
var assets = data.assets || {};
1282+
var scripts = assets.scripts || [], styles = assets.styles || [];
1283+
lines.push('Scripts: ' + scripts.length + ' | Styles: ' + styles.length);
1284+
lines.push('');
1285+
lines.push('--- Scripts ---');
1286+
scripts.forEach(function (a) { lines.push(a.handle + ' | ' + a.plugin + ' | ' + (a.src || '')); });
1287+
lines.push('');
1288+
lines.push('--- Styles ---');
1289+
styles.forEach(function (a) { lines.push(a.handle + ' | ' + a.plugin + ' | ' + (a.src || '')); });
1290+
break;
1291+
case 'hooks':
1292+
var hooks = data.hooks || [];
1293+
lines.push('Hooks: ' + hooks.length);
1294+
lines.push('');
1295+
hooks.slice(0, 50).forEach(function (h) {
1296+
lines.push(h.hook + ' | ' + h.count + 'x | ' + fmtMs(h.total_ms) + ' total | max ' + fmtMs(h.max_ms));
1297+
});
1298+
break;
1299+
case 'request':
1300+
var req = data.request || {};
1301+
if (req.method) lines.push('Method: ' + req.method);
1302+
if (req.url) lines.push('Request URL: ' + req.url);
1303+
if (req.matched_rule) lines.push('Rewrite rule: ' + req.matched_rule);
1304+
if (req.query_vars && Object.keys(req.query_vars).length) {
1305+
lines.push('Query vars:');
1306+
Object.keys(req.query_vars).forEach(function (k) { lines.push(' ' + k + ': ' + req.query_vars[k]); });
1307+
}
1308+
if (req.get && Object.keys(req.get).length) {
1309+
lines.push('GET:');
1310+
Object.keys(req.get).forEach(function (k) { lines.push(' ' + k + ': ' + req.get[k]); });
1311+
}
1312+
if (req.post && Object.keys(req.post).length) {
1313+
lines.push('POST:');
1314+
Object.keys(req.post).forEach(function (k) { lines.push(' ' + k + ': ' + req.post[k]); });
1315+
}
1316+
if (req.user_roles && req.user_roles.length) lines.push('Roles: ' + req.user_roles.join(', '));
1317+
break;
1318+
case 'template':
1319+
var tmpl = data.template || {};
1320+
lines.push('Active template: ' + (tmpl.final || '(unknown)'));
1321+
lines.push('');
1322+
(tmpl.hierarchy || []).forEach(function (f) {
1323+
lines.push((f.exists ? '[x] ' : '[ ] ') + f.file);
1324+
});
1325+
break;
1326+
case 'transients':
1327+
var trans = data.transients || [];
1328+
lines.push('Transients: ' + trans.length);
1329+
lines.push('');
1330+
trans.forEach(function (t) {
1331+
lines.push(t.key + ' | ' + (t.hit ? 'HIT' : 'MISS')
1332+
+ ' | gets: ' + t.gets + ' | sets: ' + t.sets + ' | deletes: ' + t.deletes);
1333+
});
1334+
break;
1335+
case 'summary':
1336+
var cQueries = data.queries || [], cHttp = data.http || [], cLogs = data.logs || [];
1337+
1338+
// Environment
1339+
if (meta.php_version) {
1340+
lines.push('Environment');
1341+
lines.push(' PHP: ' + meta.php_version + ' | WP: ' + (meta.wp_version || '?') + (meta.mysql_version ? ' | MySQL: ' + meta.mysql_version : ''));
1342+
if (meta.memory_peak_mb) lines.push(' Memory peak: ' + meta.memory_peak_mb + 'MB / ' + (meta.memory_limit || '?'));
1343+
if (meta.active_theme) lines.push(' Theme: ' + meta.active_theme);
1344+
if (meta.is_multisite) lines.push(' Multisite: yes');
1345+
lines.push('');
1346+
}
1347+
1348+
// DB card
1349+
var cSlowQ = cQueries.filter(function (q) { return q.time_ms >= T_SLOW; }).length;
1350+
var cCritQ = cQueries.filter(function (q) { return q.time_ms >= T_CRITICAL; }).length;
1351+
var cDupeQ = cQueries.filter(function (q) { return q.is_dupe; }).length;
1352+
var cN1Cnt = Object.keys(n1Patterns).length;
1353+
lines.push('DB Queries: ' + meta.query_count + ' | ' + fmtMs(meta.query_total_ms) + ' total'
1354+
+ (cCritQ ? ' | ' + cCritQ + ' critical' : cSlowQ ? ' | ' + cSlowQ + ' slow' : ' | no slow queries')
1355+
+ (cDupeQ ? ' | ' + cDupeQ + ' dupes' : '')
1356+
+ (cN1Cnt ? ' | ' + cN1Cnt + ' N+1 pattern' + (cN1Cnt > 1 ? 's' : '') : ''));
1357+
1358+
// HTTP card
1359+
var cSlowH = cHttp.filter(function (h) { return h.time_ms >= T_SLOW; }).length;
1360+
var cCacH = cHttp.filter(function (h) { return h.cached; }).length;
1361+
var cErrH = cHttp.filter(function (h) { return !!h.error; }).length;
1362+
lines.push('HTTP / REST: ' + meta.http_count + ' calls | ' + fmtMs(meta.http_total_ms) + ' total'
1363+
+ (cSlowH ? ' | ' + cSlowH + ' slow' : '')
1364+
+ (cCacH ? ' | ' + cCacH + ' cached' : '')
1365+
+ (cErrH ? ' | ' + cErrH + ' errors' : '')
1366+
+ (cHttp.length === 0 ? ' | no outbound calls' : ''));
1367+
1368+
// Logs card
1369+
var cErrL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('error') !== -1; }).length;
1370+
var cWarnL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('warn') !== -1; }).length;
1371+
var cDepL = cLogs.filter(function (e) { return (e.level || '').toLowerCase().indexOf('dep') !== -1; }).length;
1372+
lines.push('Logs: ' + cLogs.length
1373+
+ (cErrL ? ' | ' + cErrL + ' errors' : '')
1374+
+ (cWarnL ? ' | ' + cWarnL + ' warnings' : '')
1375+
+ (cDepL ? ' | ' + cDepL + ' deprecated' : '')
1376+
+ (cLogs.length === 0 ? ' | none' : ''));
1377+
1378+
// Cache card
1379+
var cCache = data.cache || {};
1380+
if (cCache.available) {
1381+
var cHitStr = cCache.hit_rate !== null ? cCache.hit_rate + '%' : 'n/a';
1382+
lines.push('Object Cache: ' + cHitStr + ' hit rate | ' + (cCache.hits || 0) + ' hits, ' + (cCache.misses || 0) + ' misses'
1383+
+ (cCache.persistent ? ' | persistent' : ' | non-persistent'));
1384+
}
1385+
1386+
// Assets card
1387+
var cAssets = data.assets || {};
1388+
lines.push('Assets: ' + ((cAssets.scripts || []).length + (cAssets.styles || []).length)
1389+
+ ' | ' + (cAssets.scripts || []).length + ' JS, ' + (cAssets.styles || []).length + ' CSS');
1390+
1391+
lines.push('');
1392+
1393+
// Plugin leaderboard
1394+
var cByP = {};
1395+
cQueries.forEach(function (q) {
1396+
if (!cByP[q.plugin]) cByP[q.plugin] = { count: 0, total_ms: 0, slow: 0, n1: 0 };
1397+
cByP[q.plugin].count++; cByP[q.plugin].total_ms += q.time_ms;
1398+
if (q.time_ms >= T_SLOW) cByP[q.plugin].slow++;
1399+
if (isN1(q.sql)) cByP[q.plugin].n1++;
1400+
});
1401+
var cPluginList = Object.keys(cByP).map(function (p) {
1402+
return { plugin: p, count: cByP[p].count, total_ms: cByP[p].total_ms, slow: cByP[p].slow, n1: cByP[p].n1 };
1403+
}).sort(function (a, b) { return b.total_ms - a.total_ms; });
1404+
if (cPluginList.length > 0) {
1405+
lines.push('Plugin Leaderboard — DB query time');
1406+
cPluginList.slice(0, 8).forEach(function (p, i) {
1407+
lines.push(' ' + (i + 1) + '. ' + p.plugin + ' \u2014 ' + p.count + ' queries, ' + fmtMs(p.total_ms)
1408+
+ (p.slow ? ', ' + p.slow + ' slow' : '')
1409+
+ (p.n1 ? ', ' + p.n1 + ' N+1' : ''));
1410+
});
1411+
lines.push('');
1412+
}
1413+
1414+
// Slowest queries (top 5)
1415+
var cTop5Q = cQueries.slice().sort(function (a, b) { return b.time_ms - a.time_ms; }).slice(0, 5);
1416+
if (cTop5Q.length > 0) {
1417+
lines.push('Slowest Queries');
1418+
cTop5Q.forEach(function (q) {
1419+
lines.push(' ' + fmtMs(q.time_ms) + ' ' + q.sql.replace(/\s+/g, ' ').trim().slice(0, 100) + ' [' + q.plugin + ']');
1420+
});
1421+
lines.push('');
1422+
}
1423+
1424+
// N+1 patterns
1425+
var cN1List = Object.values(n1Patterns).sort(function (a, b) { return b.count - a.count; });
1426+
if (cN1List.length > 0) {
1427+
lines.push('N+1 Query Patterns');
1428+
cN1List.forEach(function (p) {
1429+
lines.push(' x' + p.count + ' ' + normalisePattern(p.example).slice(0, 100) + ' [' + p.plugin + ']');
1430+
});
1431+
lines.push('');
1432+
}
1433+
1434+
// Slowest HTTP (top 5)
1435+
var cTop5H = cHttp.slice().sort(function (a, b) { return b.time_ms - a.time_ms; }).slice(0, 5);
1436+
if (cTop5H.length > 0) {
1437+
lines.push('Slowest HTTP Calls');
1438+
cTop5H.forEach(function (h) {
1439+
lines.push(' ' + fmtMs(h.time_ms) + ' [' + (h.method || 'GET') + '] ' + (h.url || '').slice(0, 100) + ' [' + h.plugin + ']');
1440+
});
1441+
lines.push('');
1442+
}
1443+
1444+
// Duplicate queries
1445+
var cDupeGroups = {};
1446+
cQueries.forEach(function (q) {
1447+
var fp = q.sql.replace(/\s+/g, ' ').toLowerCase().trim();
1448+
if (!cDupeGroups[fp]) cDupeGroups[fp] = { sql: q.sql, count: 0, total_ms: 0 };
1449+
cDupeGroups[fp].count++; cDupeGroups[fp].total_ms += q.time_ms;
1450+
});
1451+
var cDupeList = Object.values(cDupeGroups).filter(function (g) { return g.count > 1; })
1452+
.sort(function (a, b) { return b.count - a.count; }).slice(0, 8);
1453+
if (cDupeList.length > 0) {
1454+
lines.push('Exact Duplicate Queries (' + cDupeList.length + ' groups)');
1455+
cDupeList.forEach(function (g) {
1456+
lines.push(' x' + g.count + ' ' + g.sql.replace(/\s+/g, ' ').trim().slice(0, 100)
1457+
+ ' (' + fmtMs(g.count > 0 ? g.total_ms / g.count : 0) + ' avg)');
1458+
});
1459+
lines.push('');
1460+
}
1461+
1462+
// Slowest hooks (top 8)
1463+
var cTopHooks = (data.hooks || []).slice(0, 8);
1464+
if (cTopHooks.length > 0) {
1465+
lines.push('Slowest Hooks');
1466+
cTopHooks.forEach(function (h) {
1467+
lines.push(' ' + h.hook + ' | ' + h.count + 'x | ' + fmtMs(h.total_ms) + ' total | max ' + fmtMs(h.max_ms));
1468+
});
1469+
}
1470+
break;
1471+
}
1472+
1473+
var text = lines.join('\n');
1474+
var copyBtn = document.getElementById('cs-perf-copy');
1475+
if (navigator.clipboard && navigator.clipboard.writeText) {
1476+
navigator.clipboard.writeText(text).then(function () {
1477+
flashCopyBtn(copyBtn, 'Copied!');
1478+
}).catch(function () { fallbackCopy(text, copyBtn); });
1479+
} else {
1480+
fallbackCopy(text, copyBtn);
1481+
}
1482+
}
1483+
1484+
function fallbackCopy(text, btn) {
1485+
var ta = document.createElement('textarea');
1486+
ta.value = text;
1487+
ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0';
1488+
document.body.appendChild(ta);
1489+
ta.focus(); ta.select();
1490+
try { document.execCommand('copy'); flashCopyBtn(btn, 'Copied!'); }
1491+
catch (e) { flashCopyBtn(btn, 'Failed'); }
1492+
document.body.removeChild(ta);
1493+
}
1494+
1495+
function flashCopyBtn(btn, msg) {
1496+
if (!btn) return;
1497+
var orig = btn.textContent;
1498+
btn.textContent = msg;
1499+
setTimeout(function () { btn.textContent = orig; }, 1500);
1500+
}
1501+
12351502
// ── Resize ────────────────────────────────────────────────────────────────
12361503
function bindResizeHandle() {
12371504
var startY, startH;
@@ -1287,6 +1554,8 @@
12871554
togglePanel();
12881555
});
12891556
if (exportBtn) exportBtn.addEventListener('click', function (e) { e.stopPropagation(); exportJSON(); });
1557+
var copyBtn = document.getElementById('cs-perf-copy');
1558+
if (copyBtn) copyBtn.addEventListener('click', function (e) { e.stopPropagation(); copyCurrentTab(); });
12901559

12911560
var helpBtn = document.getElementById('cs-perf-help-btn');
12921561
var helpPanel = document.getElementById('cs-perf-help');
@@ -1409,6 +1678,28 @@
14091678
if (e.ctrlKey && e.shiftKey && (e.key === 'm' || e.key === 'M')) {
14101679
e.preventDefault(); togglePanel();
14111680
}
1681+
if (e.key === 'Escape') {
1682+
// Close help panel first if open
1683+
var helpPanelEl = document.getElementById('cs-perf-help');
1684+
if (helpPanelEl && helpPanelEl.style.display !== 'none') {
1685+
helpPanelEl.style.display = 'none';
1686+
return;
1687+
}
1688+
// Collapse any open EXPLAIN result divs and detail rows
1689+
var hadOpen = false;
1690+
Array.prototype.forEach.call(document.querySelectorAll('.cs-explain-result'), function (r) {
1691+
if (r.innerHTML) { r.innerHTML = ''; hadOpen = true; }
1692+
});
1693+
Array.prototype.forEach.call(document.querySelectorAll('.cs-row-detail'), function (d) {
1694+
if (d.style.display !== 'none') { d.style.display = 'none'; hadOpen = true; }
1695+
});
1696+
// Reset any disabled EXPLAIN buttons
1697+
if (hadOpen) {
1698+
Array.prototype.forEach.call(document.querySelectorAll('.cs-explain-btn'), function (btn) {
1699+
btn.disabled = false; btn.textContent = 'EXPLAIN';
1700+
});
1701+
}
1702+
}
14121703
});
14131704
}
14141705

0 commit comments

Comments
 (0)