adonisjs-server-stats 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -9
- package/dist/src/dashboard/chart_aggregator.d.ts +21 -0
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -0
- package/dist/src/dashboard/chart_aggregator.js +89 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +147 -0
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_controller.js +1008 -0
- package/dist/src/dashboard/dashboard_routes.d.ts +16 -0
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_routes.js +88 -0
- package/dist/src/dashboard/dashboard_store.d.ts +158 -0
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_store.js +723 -0
- package/dist/src/dashboard/integrations/cache_inspector.d.ts +88 -0
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/cache_inspector.js +215 -0
- package/dist/src/dashboard/integrations/config_inspector.d.ts +33 -0
- package/dist/src/dashboard/integrations/config_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/config_inspector.js +155 -0
- package/dist/src/dashboard/integrations/index.d.ts +7 -0
- package/dist/src/dashboard/integrations/index.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/index.js +3 -0
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +106 -0
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/queue_inspector.js +182 -0
- package/dist/src/dashboard/migrator.d.ts +18 -0
- package/dist/src/dashboard/migrator.d.ts.map +1 -0
- package/dist/src/dashboard/migrator.js +144 -0
- package/dist/src/dashboard/models/stats_email.d.ts +19 -0
- package/dist/src/dashboard/models/stats_email.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_email.js +66 -0
- package/dist/src/dashboard/models/stats_event.d.ts +14 -0
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_event.js +43 -0
- package/dist/src/dashboard/models/stats_log.d.ts +12 -0
- package/dist/src/dashboard/models/stats_log.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_log.js +42 -0
- package/dist/src/dashboard/models/stats_metric.d.ts +15 -0
- package/dist/src/dashboard/models/stats_metric.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_metric.js +50 -0
- package/dist/src/dashboard/models/stats_query.d.ts +20 -0
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_query.js +67 -0
- package/dist/src/dashboard/models/stats_request.d.ts +21 -0
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_request.js +61 -0
- package/dist/src/dashboard/models/stats_saved_filter.d.ts +11 -0
- package/dist/src/dashboard/models/stats_saved_filter.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_saved_filter.js +38 -0
- package/dist/src/dashboard/models/stats_trace.d.ts +19 -0
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_trace.js +67 -0
- package/dist/src/debug/debug_store.d.ts +5 -0
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -0
- package/dist/src/debug/email_collector.d.ts +2 -0
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +4 -0
- package/dist/src/debug/event_collector.d.ts +2 -0
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +11 -2
- package/dist/src/debug/query_collector.d.ts +2 -0
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +11 -0
- package/dist/src/debug/ring_buffer.d.ts +3 -0
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/ring_buffer.js +6 -0
- package/dist/src/debug/trace_collector.d.ts +4 -2
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +7 -2
- package/dist/src/debug/types.d.ts +8 -0
- package/dist/src/debug/types.d.ts.map +1 -1
- package/dist/src/edge/client/dashboard.css +1504 -0
- package/dist/src/edge/client/dashboard.js +2378 -0
- package/dist/src/edge/client/debug-panel.css +530 -110
- package/dist/src/edge/client/debug-panel.js +663 -22
- package/dist/src/edge/client/stats-bar.css +115 -41
- package/dist/src/edge/client/stats-bar.js +37 -3
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +21 -0
- package/dist/src/edge/views/dashboard.edge +382 -0
- package/dist/src/edge/views/debug-panel.edge +60 -14
- package/dist/src/edge/views/stats-bar.edge +9 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/middleware/request_tracking_middleware.d.ts +20 -0
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +66 -2
- package/dist/src/provider/server_stats_provider.d.ts +13 -0
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +175 -1
- package/dist/src/types.d.ts +42 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +14 -1
|
@@ -16,9 +16,59 @@
|
|
|
16
16
|
|
|
17
17
|
if (!panel || !wrench) return;
|
|
18
18
|
|
|
19
|
+
// ── Theme detection & toggle ────────────────────────────────────
|
|
20
|
+
let themeOverride = localStorage.getItem('ss-dash-theme');
|
|
21
|
+
const themeBtn = document.getElementById('ss-dbg-theme-btn');
|
|
22
|
+
|
|
23
|
+
const applyPanelTheme = () => {
|
|
24
|
+
if (themeOverride) {
|
|
25
|
+
panel.setAttribute('data-ss-theme', themeOverride);
|
|
26
|
+
} else {
|
|
27
|
+
panel.removeAttribute('data-ss-theme');
|
|
28
|
+
}
|
|
29
|
+
if (themeBtn) {
|
|
30
|
+
const isDark = themeOverride === 'dark' || (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
31
|
+
themeBtn.textContent = isDark ? '\u2600' : '\u263D';
|
|
32
|
+
themeBtn.title = isDark ? 'Switch to light theme' : 'Switch to dark theme';
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (themeBtn) {
|
|
37
|
+
themeBtn.addEventListener('click', function () {
|
|
38
|
+
const isDark = themeOverride === 'dark' || (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
39
|
+
themeOverride = isDark ? 'light' : 'dark';
|
|
40
|
+
localStorage.setItem('ss-dash-theme', themeOverride);
|
|
41
|
+
applyPanelTheme();
|
|
42
|
+
// Sync stats bar if applyBarTheme exists globally
|
|
43
|
+
if (typeof window.__ssApplyBarTheme === 'function') window.__ssApplyBarTheme();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
applyPanelTheme();
|
|
48
|
+
|
|
49
|
+
// Listen for cross-tab theme changes
|
|
50
|
+
window.addEventListener('storage', function (e) {
|
|
51
|
+
if (e.key === 'ss-dash-theme') {
|
|
52
|
+
themeOverride = e.newValue;
|
|
53
|
+
applyPanelTheme();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
19
57
|
const LOGS_ENDPOINT = panel.dataset.logsEndpoint || (BASE + '/logs');
|
|
20
58
|
|
|
21
59
|
const tracingEnabled = panel.dataset.tracing === '1';
|
|
60
|
+
const dashboardPath = panel.dataset.dashboardPath || null;
|
|
61
|
+
const DASH_API = dashboardPath ? (dashboardPath.replace(/\/+$/, '') + '/api') : null;
|
|
62
|
+
|
|
63
|
+
/** Build an SVG external-link icon for deep links. */
|
|
64
|
+
const deepLinkSvg = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
|
|
65
|
+
|
|
66
|
+
/** Build a deep link anchor element HTML string. */
|
|
67
|
+
const deepLink = (section, id) => {
|
|
68
|
+
if (!dashboardPath) return '';
|
|
69
|
+
const href = dashboardPath + '#' + section + (id != null ? '?id=' + id : '');
|
|
70
|
+
return ' <a href="' + esc(href) + '" target="_blank" class="ss-dbg-deeplink" title="Open in dashboard" onclick="event.stopPropagation()">' + deepLinkSvg + '</a>';
|
|
71
|
+
};
|
|
22
72
|
|
|
23
73
|
let isOpen = false;
|
|
24
74
|
let activeTab = tracingEnabled ? 'timeline' : 'queries';
|
|
@@ -27,6 +77,8 @@
|
|
|
27
77
|
let logFilter = 'all';
|
|
28
78
|
let cachedLogs = [];
|
|
29
79
|
const currentPath = window.location.pathname;
|
|
80
|
+
let isLive = false;
|
|
81
|
+
let transmitSub = null;
|
|
30
82
|
|
|
31
83
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
32
84
|
const esc = (s) => {
|
|
@@ -92,7 +144,7 @@
|
|
|
92
144
|
|
|
93
145
|
// ── Custom pane cell formatter ────────────────────────────────────
|
|
94
146
|
const formatCell = (value, col) => {
|
|
95
|
-
if (value === null || value === undefined) return '<span
|
|
147
|
+
if (value === null || value === undefined) return '<span class="ss-dbg-c-dim">-</span>';
|
|
96
148
|
const fmt = col.format || 'text';
|
|
97
149
|
switch (fmt) {
|
|
98
150
|
case 'time':
|
|
@@ -177,6 +229,7 @@
|
|
|
177
229
|
panel.querySelectorAll('.ss-dbg-pane').forEach((p) => p.classList.remove('ss-dbg-active'));
|
|
178
230
|
|
|
179
231
|
tab.classList.add('ss-dbg-active');
|
|
232
|
+
tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
180
233
|
const pane = document.getElementById('ss-dbg-pane-' + name);
|
|
181
234
|
if (pane) pane.classList.add('ss-dbg-active');
|
|
182
235
|
|
|
@@ -193,6 +246,9 @@
|
|
|
193
246
|
else if (name === 'routes' && !fetched.routes) fetchRoutes();
|
|
194
247
|
else if (name === 'logs') fetchLogs();
|
|
195
248
|
else if (name === 'emails') fetchEmails();
|
|
249
|
+
else if (name === 'cache') fetchCache();
|
|
250
|
+
else if (name === 'jobs') fetchJobs();
|
|
251
|
+
else if (name === 'config' && !fetched.config) fetchConfig();
|
|
196
252
|
else {
|
|
197
253
|
const cp = customPanes.find((p) => p.id === name);
|
|
198
254
|
if (cp) {
|
|
@@ -264,7 +320,7 @@
|
|
|
264
320
|
}
|
|
265
321
|
|
|
266
322
|
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
267
|
-
+ '<th style="width:
|
|
323
|
+
+ '<th style="width:64px">#</th>'
|
|
268
324
|
+ '<th>SQL</th>'
|
|
269
325
|
+ '<th style="width:70px">Duration</th>'
|
|
270
326
|
+ '<th style="width:60px">Method</th>'
|
|
@@ -277,13 +333,13 @@
|
|
|
277
333
|
const durClass = durationClass(q.duration);
|
|
278
334
|
const dupCount = sqlCounts[q.sql] || 1;
|
|
279
335
|
html += '<tr>'
|
|
280
|
-
+ '<td style="
|
|
336
|
+
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + q.id + deepLink('queries', q.id) + '</td>'
|
|
281
337
|
+ '<td><span class="ss-dbg-sql" title="Click to expand" onclick="this.classList.toggle(\'ss-dbg-expanded\')">' + esc(q.sql) + '</span>'
|
|
282
338
|
+ (dupCount > 1 ? ' <span class="ss-dbg-dup">x' + dupCount + '</span>' : '')
|
|
283
339
|
+ '</td>'
|
|
284
340
|
+ '<td class="ss-dbg-duration ' + durClass + '">' + q.duration.toFixed(2) + 'ms</td>'
|
|
285
341
|
+ '<td><span class="' + methodClass(q.method) + '">' + esc(q.method) + '</span></td>'
|
|
286
|
-
+ '<td
|
|
342
|
+
+ '<td class="ss-dbg-c-muted">' + esc(q.model || '-') + '</td>'
|
|
287
343
|
+ '<td class="ss-dbg-event-time">' + timeAgo(q.timestamp) + '</td>'
|
|
288
344
|
+ '</tr>';
|
|
289
345
|
}
|
|
@@ -340,7 +396,7 @@
|
|
|
340
396
|
}
|
|
341
397
|
|
|
342
398
|
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
343
|
-
+ '<th style="width:
|
|
399
|
+
+ '<th style="width:64px">#</th>'
|
|
344
400
|
+ '<th>Event</th>'
|
|
345
401
|
+ '<th>Data</th>'
|
|
346
402
|
+ '<th style="width:100px">Time</th>'
|
|
@@ -351,14 +407,14 @@
|
|
|
351
407
|
const hasData = ev.data && ev.data !== '-';
|
|
352
408
|
const preview = hasData ? eventPreview(ev.data) : '-';
|
|
353
409
|
html += '<tr>'
|
|
354
|
-
+ '<td style="
|
|
410
|
+
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + ev.id + deepLink('events', ev.id) + '</td>'
|
|
355
411
|
+ '<td class="ss-dbg-event-name">' + esc(ev.event) + '</td>'
|
|
356
412
|
+ '<td class="ss-dbg-event-data">'
|
|
357
413
|
+ (hasData
|
|
358
414
|
? '<span class="ss-dbg-data-preview" data-ev-idx="' + i + '">' + esc(preview) + '</span>'
|
|
359
415
|
+ '<pre class="ss-dbg-data-full" id="ss-dbg-evdata-' + i + '" style="display:none">' + esc(ev.data) + '</pre>'
|
|
360
416
|
+ '<button type="button" class="ss-dbg-copy-btn" data-copy-idx="' + i + '" title="Copy JSON">⎘</button>'
|
|
361
|
-
: '<span
|
|
417
|
+
: '<span class="ss-dbg-c-dim">-</span>')
|
|
362
418
|
+ '</td>'
|
|
363
419
|
+ '<td class="ss-dbg-event-time">' + formatTime(ev.timestamp) + '</td>'
|
|
364
420
|
+ '</tr>';
|
|
@@ -468,9 +524,9 @@
|
|
|
468
524
|
html += '<tr' + (isCurrent ? ' class="ss-dbg-current-route"' : '') + '>'
|
|
469
525
|
+ '<td><span class="' + methodClass(r.method) + '">' + esc(r.method) + '</span></td>'
|
|
470
526
|
+ '<td>' + esc(r.pattern) + '</td>'
|
|
471
|
-
+ '<td
|
|
472
|
-
+ '<td
|
|
473
|
-
+ '<td style="
|
|
527
|
+
+ '<td class="ss-dbg-c-muted">' + esc(r.name || '-') + '</td>'
|
|
528
|
+
+ '<td class="ss-dbg-c-sql">' + esc(r.handler) + '</td>'
|
|
529
|
+
+ '<td class="ss-dbg-c-dim" style="font-size:10px">' + (r.middleware.length ? esc(r.middleware.join(', ')) : '-') + '</td>'
|
|
474
530
|
+ '</tr>';
|
|
475
531
|
}
|
|
476
532
|
|
|
@@ -631,7 +687,7 @@
|
|
|
631
687
|
}
|
|
632
688
|
|
|
633
689
|
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
634
|
-
+ '<th style="width:
|
|
690
|
+
+ '<th style="width:64px">#</th>'
|
|
635
691
|
+ '<th style="width:160px">From</th>'
|
|
636
692
|
+ '<th style="width:160px">To</th>'
|
|
637
693
|
+ '<th>Subject</th>'
|
|
@@ -644,13 +700,13 @@
|
|
|
644
700
|
for (let i = 0; i < filtered.length; i++) {
|
|
645
701
|
const e = filtered[i];
|
|
646
702
|
html += '<tr class="ss-dbg-email-row" data-email-id="' + e.id + '">'
|
|
647
|
-
+ '<td style="
|
|
648
|
-
+ '<td style="
|
|
649
|
-
+ '<td style="
|
|
650
|
-
+ '<td style="
|
|
703
|
+
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + e.id + deepLink('emails', e.id) + '</td>'
|
|
704
|
+
+ '<td class="ss-dbg-c-secondary" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(e.from) + '">' + esc(e.from) + '</td>'
|
|
705
|
+
+ '<td class="ss-dbg-c-secondary" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(e.to) + '">' + esc(e.to) + '</td>'
|
|
706
|
+
+ '<td class="ss-dbg-c-sql" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(e.subject) + '</td>'
|
|
651
707
|
+ '<td><span class="ss-dbg-email-status ss-dbg-email-status-' + esc(e.status) + '">' + esc(e.status) + '</span></td>'
|
|
652
|
-
+ '<td
|
|
653
|
-
+ '<td style="
|
|
708
|
+
+ '<td class="ss-dbg-c-muted">' + esc(e.mailer) + '</td>'
|
|
709
|
+
+ '<td class="ss-dbg-c-dim" style="text-align:center">' + (e.attachmentCount > 0 ? e.attachmentCount : '-') + '</td>'
|
|
654
710
|
+ '<td class="ss-dbg-event-time">' + timeAgo(e.timestamp) + '</td>'
|
|
655
711
|
+ '</tr>';
|
|
656
712
|
}
|
|
@@ -753,7 +809,7 @@
|
|
|
753
809
|
}
|
|
754
810
|
|
|
755
811
|
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
756
|
-
+ '<th style="width:
|
|
812
|
+
+ '<th style="width:64px">#</th>'
|
|
757
813
|
+ '<th style="width:60px">Method</th>'
|
|
758
814
|
+ '<th>URL</th>'
|
|
759
815
|
+ '<th style="width:55px">Status</th>'
|
|
@@ -766,13 +822,13 @@
|
|
|
766
822
|
for (let i = 0; i < filtered.length; i++) {
|
|
767
823
|
const t = filtered[i];
|
|
768
824
|
html += '<tr class="ss-dbg-email-row" data-trace-id="' + t.id + '">'
|
|
769
|
-
+ '<td style="
|
|
825
|
+
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + t.id + deepLink('traces', t.id) + '</td>'
|
|
770
826
|
+ '<td><span class="' + methodClass(t.method) + '">' + esc(t.method) + '</span></td>'
|
|
771
827
|
+ '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px" title="' + esc(t.url) + '">' + esc(t.url) + '</td>'
|
|
772
828
|
+ '<td><span class="ss-dbg-status ' + statusClass(t.statusCode) + '">' + t.statusCode + '</span></td>'
|
|
773
829
|
+ '<td class="ss-dbg-duration ' + durationClass(t.totalDuration) + '">' + t.totalDuration.toFixed(1) + 'ms</td>'
|
|
774
|
-
+ '<td style="
|
|
775
|
-
+ '<td style="text-align:center">' + (t.warningCount > 0 ? '<span
|
|
830
|
+
+ '<td class="ss-dbg-c-muted" style="text-align:center">' + t.spanCount + '</td>'
|
|
831
|
+
+ '<td style="text-align:center">' + (t.warningCount > 0 ? '<span class="ss-dbg-c-amber">' + t.warningCount + '</span>' : '<span class="ss-dbg-c-border">-</span>') + '</td>'
|
|
776
832
|
+ '<td class="ss-dbg-event-time">' + timeAgo(t.timestamp) + '</td>'
|
|
777
833
|
+ '</tr>';
|
|
778
834
|
}
|
|
@@ -888,6 +944,493 @@
|
|
|
888
944
|
|
|
889
945
|
if (tlSearchInput) tlSearchInput.addEventListener('input', renderTraces);
|
|
890
946
|
|
|
947
|
+
// ── Mini Stats Bar ─────────────────────────────────────────────
|
|
948
|
+
const miniStatsEl = document.getElementById('ss-dbg-mini-stats');
|
|
949
|
+
let miniStatsTimer = null;
|
|
950
|
+
|
|
951
|
+
const fetchMiniStats = () => {
|
|
952
|
+
if (!DASH_API || !miniStatsEl) return;
|
|
953
|
+
fetchJSON(DASH_API + '/overview?range=1h')
|
|
954
|
+
.then((data) => {
|
|
955
|
+
const avg = data.avgResponseTime || 0;
|
|
956
|
+
const err = data.errorRate || 0;
|
|
957
|
+
const rpm = data.requestsPerMinute || 0;
|
|
958
|
+
const hasData = (data.totalRequests || 0) > 0;
|
|
959
|
+
|
|
960
|
+
if (!hasData) {
|
|
961
|
+
miniStatsEl.innerHTML = '';
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const avgClass = avg > 500 ? 'ss-dbg-stat-red' : avg > 200 ? 'ss-dbg-stat-amber' : 'ss-dbg-stat-green';
|
|
966
|
+
const errClass = err > 5 ? 'ss-dbg-stat-red' : err > 1 ? 'ss-dbg-stat-amber' : 'ss-dbg-stat-green';
|
|
967
|
+
|
|
968
|
+
miniStatsEl.innerHTML =
|
|
969
|
+
'<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value ' + avgClass + '">' + avg.toFixed(1) + 'ms</span> avg</span>'
|
|
970
|
+
+ '<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value ' + errClass + '">' + err.toFixed(1) + '%</span> err</span>'
|
|
971
|
+
+ '<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value">' + Math.round(rpm) + '</span> req/m</span>';
|
|
972
|
+
})
|
|
973
|
+
.catch(() => {
|
|
974
|
+
miniStatsEl.innerHTML = '';
|
|
975
|
+
});
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// ── Cache Tab ─────────────────────────────────────────────────
|
|
979
|
+
const cacheSearchInput = document.getElementById('ss-dbg-search-cache');
|
|
980
|
+
const cacheSummaryEl = document.getElementById('ss-dbg-cache-summary');
|
|
981
|
+
const cacheBodyEl = document.getElementById('ss-dbg-cache-body');
|
|
982
|
+
const cacheStatsArea = document.getElementById('ss-dbg-cache-stats-area');
|
|
983
|
+
let cachedCacheData = { stats: {}, keys: [] };
|
|
984
|
+
|
|
985
|
+
const fetchCache = () => {
|
|
986
|
+
if (!DASH_API) return;
|
|
987
|
+
fetchJSON(DASH_API + '/cache')
|
|
988
|
+
.then((data) => {
|
|
989
|
+
cachedCacheData = data;
|
|
990
|
+
renderCache();
|
|
991
|
+
})
|
|
992
|
+
.catch(() => {
|
|
993
|
+
if (cacheBodyEl) cacheBodyEl.innerHTML = '<div class="ss-dbg-empty">Cache not available</div>';
|
|
994
|
+
if (cacheStatsArea) cacheStatsArea.innerHTML = '';
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const renderCache = () => {
|
|
999
|
+
if (!cacheBodyEl) return;
|
|
1000
|
+
const stats = cachedCacheData.stats || {};
|
|
1001
|
+
const keys = cachedCacheData.keys || cachedCacheData.data || [];
|
|
1002
|
+
const filter = (cacheSearchInput ? cacheSearchInput.value : '').toLowerCase();
|
|
1003
|
+
|
|
1004
|
+
// Stats area
|
|
1005
|
+
if (cacheStatsArea) {
|
|
1006
|
+
cacheStatsArea.innerHTML =
|
|
1007
|
+
'<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Hit Rate:</span><span class="ss-dbg-cache-stat-value">' + (stats.hitRate || 0).toFixed(1) + '%</span></div>'
|
|
1008
|
+
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Hits:</span><span class="ss-dbg-cache-stat-value">' + (stats.hits || 0) + '</span></div>'
|
|
1009
|
+
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Misses:</span><span class="ss-dbg-cache-stat-value">' + (stats.misses || 0) + '</span></div>'
|
|
1010
|
+
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Keys:</span><span class="ss-dbg-cache-stat-value">' + (stats.keyCount || keys.length || 0) + '</span></div>';
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (cacheSummaryEl) {
|
|
1014
|
+
cacheSummaryEl.textContent = (stats.keyCount || keys.length || 0) + ' keys';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
let filtered = keys;
|
|
1018
|
+
if (filter) {
|
|
1019
|
+
filtered = keys.filter((k) => (k.key || '').toLowerCase().indexOf(filter) !== -1);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (filtered.length === 0) {
|
|
1023
|
+
cacheBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching cache keys' : 'No cache keys found') + '</div>';
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
1028
|
+
+ '<th>Key</th>'
|
|
1029
|
+
+ '<th style="width:80px">Type</th>'
|
|
1030
|
+
+ '<th style="width:80px">TTL</th>'
|
|
1031
|
+
+ '<th style="width:80px">Size</th>'
|
|
1032
|
+
+ '</tr></thead><tbody>';
|
|
1033
|
+
|
|
1034
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
1035
|
+
const k = filtered[i];
|
|
1036
|
+
html += '<tr class="ss-dbg-email-row" data-cache-key="' + esc(k.key || '') + '">'
|
|
1037
|
+
+ '<td class="ss-dbg-c-sql">' + esc(k.key || '') + '</td>'
|
|
1038
|
+
+ '<td class="ss-dbg-c-muted">' + esc(k.type || '-') + '</td>'
|
|
1039
|
+
+ '<td class="ss-dbg-c-muted">' + (k.ttl != null ? k.ttl + 's' : '-') + '</td>'
|
|
1040
|
+
+ '<td class="ss-dbg-c-dim">' + (k.size != null ? k.size + 'B' : '-') + '</td>'
|
|
1041
|
+
+ '</tr>';
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
html += '</tbody></table>';
|
|
1045
|
+
cacheBodyEl.innerHTML = html;
|
|
1046
|
+
|
|
1047
|
+
// Click row to show cache detail
|
|
1048
|
+
cacheBodyEl.querySelectorAll('[data-cache-key]').forEach((row) => {
|
|
1049
|
+
row.addEventListener('click', () => {
|
|
1050
|
+
const key = row.getAttribute('data-cache-key');
|
|
1051
|
+
fetchJSON(DASH_API + '/cache/' + encodeURIComponent(key))
|
|
1052
|
+
.then((data) => {
|
|
1053
|
+
cacheBodyEl.innerHTML = '<div class="ss-dbg-cache-detail">'
|
|
1054
|
+
+ '<button type="button" class="ss-dbg-btn-clear" id="ss-dbg-cache-back">← Back</button>'
|
|
1055
|
+
+ ' <strong>' + esc(key) + '</strong>'
|
|
1056
|
+
+ '<pre>' + esc(JSON.stringify(data.value || data, null, 2)) + '</pre>'
|
|
1057
|
+
+ '</div>';
|
|
1058
|
+
const backBtn = document.getElementById('ss-dbg-cache-back');
|
|
1059
|
+
if (backBtn) backBtn.addEventListener('click', () => renderCache());
|
|
1060
|
+
})
|
|
1061
|
+
.catch(() => { /* ignore */ });
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
if (cacheSearchInput) cacheSearchInput.addEventListener('input', renderCache);
|
|
1067
|
+
|
|
1068
|
+
// ── Jobs Tab ──────────────────────────────────────────────────
|
|
1069
|
+
const jobsBodyEl = document.getElementById('ss-dbg-jobs-body');
|
|
1070
|
+
const jobsSummaryEl = document.getElementById('ss-dbg-jobs-summary');
|
|
1071
|
+
const jobsStatsArea = document.getElementById('ss-dbg-jobs-stats-area');
|
|
1072
|
+
const jobFilters = panel.querySelectorAll('[data-ss-dbg-job-status]');
|
|
1073
|
+
let jobStatusFilter = 'all';
|
|
1074
|
+
let cachedJobsData = { data: [], stats: {} };
|
|
1075
|
+
|
|
1076
|
+
const fetchJobs = () => {
|
|
1077
|
+
if (!DASH_API) return;
|
|
1078
|
+
let url = DASH_API + '/jobs?limit=100';
|
|
1079
|
+
if (jobStatusFilter && jobStatusFilter !== 'all') url += '&status=' + jobStatusFilter;
|
|
1080
|
+
|
|
1081
|
+
fetchJSON(url)
|
|
1082
|
+
.then((data) => {
|
|
1083
|
+
cachedJobsData = data;
|
|
1084
|
+
renderJobs();
|
|
1085
|
+
})
|
|
1086
|
+
.catch(() => {
|
|
1087
|
+
if (jobsBodyEl) jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">Jobs/Queue not available</div>';
|
|
1088
|
+
if (jobsStatsArea) jobsStatsArea.innerHTML = '';
|
|
1089
|
+
});
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const renderJobs = () => {
|
|
1093
|
+
if (!jobsBodyEl) return;
|
|
1094
|
+
const items = cachedJobsData.data || cachedJobsData.jobs || [];
|
|
1095
|
+
const stats = cachedJobsData.stats || {};
|
|
1096
|
+
|
|
1097
|
+
// Stats area
|
|
1098
|
+
if (jobsStatsArea) {
|
|
1099
|
+
jobsStatsArea.innerHTML = '<div class="ss-dbg-job-stats">'
|
|
1100
|
+
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Active:</span><span class="ss-dbg-job-stat-value">' + (stats.active || 0) + '</span></div>'
|
|
1101
|
+
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Waiting:</span><span class="ss-dbg-job-stat-value">' + (stats.waiting || 0) + '</span></div>'
|
|
1102
|
+
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Delayed:</span><span class="ss-dbg-job-stat-value">' + (stats.delayed || 0) + '</span></div>'
|
|
1103
|
+
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Completed:</span><span class="ss-dbg-job-stat-value">' + (stats.completed || 0) + '</span></div>'
|
|
1104
|
+
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Failed:</span><span class="ss-dbg-job-stat-value ss-dbg-c-red">' + (stats.failed || 0) + '</span></div>'
|
|
1105
|
+
+ '</div>';
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (jobsSummaryEl) {
|
|
1109
|
+
const total = (cachedJobsData.meta ? cachedJobsData.meta.total : null) || items.length;
|
|
1110
|
+
jobsSummaryEl.textContent = total + ' jobs';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (items.length === 0) {
|
|
1114
|
+
jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">No jobs found</div>';
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
1119
|
+
+ '<th style="width:50px">ID</th>'
|
|
1120
|
+
+ '<th>Name</th>'
|
|
1121
|
+
+ '<th style="width:80px">Status</th>'
|
|
1122
|
+
+ '<th style="width:60px">Attempts</th>'
|
|
1123
|
+
+ '<th style="width:80px">Duration</th>'
|
|
1124
|
+
+ '<th style="width:70px">Time</th>'
|
|
1125
|
+
+ '<th style="width:50px"></th>'
|
|
1126
|
+
+ '</tr></thead><tbody>';
|
|
1127
|
+
|
|
1128
|
+
for (let i = 0; i < items.length; i++) {
|
|
1129
|
+
const j = items[i];
|
|
1130
|
+
const statusBadge = j.status === 'failed' ? 'red' : j.status === 'completed' ? 'green' : j.status === 'active' ? 'blue' : 'amber';
|
|
1131
|
+
html += '<tr>'
|
|
1132
|
+
+ '<td class="ss-dbg-c-dim">' + j.id + '</td>'
|
|
1133
|
+
+ '<td class="ss-dbg-c-sql">' + esc(j.name || '') + '</td>'
|
|
1134
|
+
+ '<td><span class="ss-dbg-badge ss-dbg-badge-' + statusBadge + '">' + esc(j.status || '') + '</span></td>'
|
|
1135
|
+
+ '<td class="ss-dbg-c-muted" style="text-align:center">' + (j.attempts || j.attemptsMade || 0) + '</td>'
|
|
1136
|
+
+ '<td class="ss-dbg-duration">' + (j.duration != null ? j.duration.toFixed(0) + 'ms' : '-') + '</td>'
|
|
1137
|
+
+ '<td class="ss-dbg-event-time">' + timeAgo(j.timestamp || j.processedOn || j.created_at) + '</td>'
|
|
1138
|
+
+ '<td>' + (j.status === 'failed' ? '<button class="ss-dbg-retry-btn" data-retry-id="' + j.id + '">Retry</button>' : '') + '</td>'
|
|
1139
|
+
+ '</tr>';
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
html += '</tbody></table>';
|
|
1143
|
+
jobsBodyEl.innerHTML = html;
|
|
1144
|
+
|
|
1145
|
+
// Retry buttons
|
|
1146
|
+
jobsBodyEl.querySelectorAll('.ss-dbg-retry-btn').forEach((btn) => {
|
|
1147
|
+
btn.addEventListener('click', (e) => {
|
|
1148
|
+
e.stopPropagation();
|
|
1149
|
+
const id = btn.getAttribute('data-retry-id');
|
|
1150
|
+
btn.textContent = '...';
|
|
1151
|
+
btn.disabled = true;
|
|
1152
|
+
fetch(DASH_API + '/jobs/' + id + '/retry', { method: 'POST', credentials: 'same-origin' })
|
|
1153
|
+
.then(() => { btn.textContent = 'OK'; setTimeout(fetchJobs, 1000); })
|
|
1154
|
+
.catch(() => { btn.textContent = 'Retry'; btn.disabled = false; });
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
jobFilters.forEach((btn) => {
|
|
1160
|
+
btn.addEventListener('click', () => {
|
|
1161
|
+
jobFilters.forEach((b) => b.classList.remove('ss-dbg-active'));
|
|
1162
|
+
btn.classList.add('ss-dbg-active');
|
|
1163
|
+
jobStatusFilter = btn.getAttribute('data-ss-dbg-job-status');
|
|
1164
|
+
fetchJobs();
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// ── Config Tab ────────────────────────────────────────────────
|
|
1169
|
+
const configBodyEl = document.getElementById('ss-dbg-config-body');
|
|
1170
|
+
const configSummaryEl = document.getElementById('ss-dbg-config-summary');
|
|
1171
|
+
const configSearchInput = document.getElementById('ss-dbg-search-config');
|
|
1172
|
+
const configTabs = panel.querySelectorAll('[data-ss-dbg-config-tab]');
|
|
1173
|
+
let configRawData = null;
|
|
1174
|
+
let configActiveTab = 'config';
|
|
1175
|
+
let configSearchTerm = '';
|
|
1176
|
+
|
|
1177
|
+
const flattenConfig = (obj, prefix) => {
|
|
1178
|
+
const results = [];
|
|
1179
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
1180
|
+
results.push({ path: prefix, value: obj });
|
|
1181
|
+
return results;
|
|
1182
|
+
}
|
|
1183
|
+
const keys = Object.keys(obj);
|
|
1184
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1185
|
+
const fullPath = prefix ? prefix + '.' + keys[i] : keys[i];
|
|
1186
|
+
const val = obj[keys[i]];
|
|
1187
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val) && !val.__redacted) {
|
|
1188
|
+
const nested = flattenConfig(val, fullPath);
|
|
1189
|
+
for (let n = 0; n < nested.length; n++) results.push(nested[n]);
|
|
1190
|
+
} else {
|
|
1191
|
+
results.push({ path: fullPath, value: val });
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return results;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
const countLeaves = (obj) => {
|
|
1198
|
+
if (typeof obj !== 'object' || obj === null || obj.__redacted) return 1;
|
|
1199
|
+
let count = 0;
|
|
1200
|
+
const keys = Object.keys(obj);
|
|
1201
|
+
for (let i = 0; i < keys.length; i++) count += countLeaves(obj[keys[i]]);
|
|
1202
|
+
return count;
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const formatConfigValue = (val) => {
|
|
1206
|
+
if (val === null || val === undefined) return '<span class="ss-dbg-config-val-null">null</span>';
|
|
1207
|
+
if (val === true) return '<span class="ss-dbg-config-val-true">true</span>';
|
|
1208
|
+
if (val === false) return '<span class="ss-dbg-config-val-false">false</span>';
|
|
1209
|
+
if (typeof val === 'number') return '<span class="ss-dbg-config-val-number">' + val + '</span>';
|
|
1210
|
+
if (Array.isArray(val)) {
|
|
1211
|
+
const items = val.map((item) => {
|
|
1212
|
+
if (item === null || item === undefined) return 'null';
|
|
1213
|
+
if (typeof item === 'object') {
|
|
1214
|
+
try { return JSON.stringify(item); } catch { return String(item); }
|
|
1215
|
+
}
|
|
1216
|
+
return String(item);
|
|
1217
|
+
});
|
|
1218
|
+
return '<span class="ss-dbg-config-val-array">[' + esc(items.join(', ')) + ']</span>';
|
|
1219
|
+
}
|
|
1220
|
+
if (typeof val === 'object') {
|
|
1221
|
+
try { return '<span class="ss-dbg-config-val-null">' + esc(JSON.stringify(val, null, 2)) + '</span>'; } catch { /* fall through */ }
|
|
1222
|
+
}
|
|
1223
|
+
return esc(String(val));
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
const highlightMatch = (text, term) => {
|
|
1227
|
+
if (!term) return text;
|
|
1228
|
+
const idx = text.toLowerCase().indexOf(term.toLowerCase());
|
|
1229
|
+
if (idx === -1) return text;
|
|
1230
|
+
return text.slice(0, idx) + '<mark class="ss-dbg-config-match">' + text.slice(idx, idx + term.length) + '</mark>' + text.slice(idx + term.length);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
const isRedactedObj = (val) => val && typeof val === 'object' && val.__redacted === true;
|
|
1234
|
+
|
|
1235
|
+
const renderRedacted = (val, prefix) => {
|
|
1236
|
+
const cls = prefix + '-config-redacted';
|
|
1237
|
+
const realVal = esc(val.value || '');
|
|
1238
|
+
return '<span class="' + cls + ' ' + prefix + '-redacted-wrap" data-redacted-value="' + realVal + '">'
|
|
1239
|
+
+ '<span class="' + prefix + '-redacted-display">' + esc(val.display) + '</span>'
|
|
1240
|
+
+ '<span class="' + prefix + '-redacted-real" style="display:none">' + realVal + '</span>'
|
|
1241
|
+
+ '<button type="button" class="' + prefix + '-redacted-reveal" title="Reveal value">'
|
|
1242
|
+
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
|
1243
|
+
+ '</button>'
|
|
1244
|
+
+ '<button type="button" class="' + prefix + '-redacted-copy" title="Copy value">'
|
|
1245
|
+
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'
|
|
1246
|
+
+ '</button>'
|
|
1247
|
+
+ '</span>';
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const bindRedactedButtons = (container, prefix) => {
|
|
1251
|
+
container.querySelectorAll('.' + prefix + '-redacted-reveal').forEach((btn) => {
|
|
1252
|
+
btn.addEventListener('click', (e) => {
|
|
1253
|
+
e.stopPropagation();
|
|
1254
|
+
const wrap = btn.closest('.' + prefix + '-redacted-wrap');
|
|
1255
|
+
if (!wrap) return;
|
|
1256
|
+
const display = wrap.querySelector('.' + prefix + '-redacted-display');
|
|
1257
|
+
const real = wrap.querySelector('.' + prefix + '-redacted-real');
|
|
1258
|
+
if (!display || !real) return;
|
|
1259
|
+
const isHidden = real.style.display === 'none';
|
|
1260
|
+
display.style.display = isHidden ? 'none' : '';
|
|
1261
|
+
real.style.display = isHidden ? '' : 'none';
|
|
1262
|
+
btn.innerHTML = isHidden
|
|
1263
|
+
? '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>'
|
|
1264
|
+
: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
1265
|
+
btn.title = isHidden ? 'Hide value' : 'Reveal value';
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
container.querySelectorAll('.' + prefix + '-redacted-copy').forEach((btn) => {
|
|
1270
|
+
btn.addEventListener('click', (e) => {
|
|
1271
|
+
e.stopPropagation();
|
|
1272
|
+
const wrap = btn.closest('.' + prefix + '-redacted-wrap');
|
|
1273
|
+
if (!wrap) return;
|
|
1274
|
+
const val = wrap.getAttribute('data-redacted-value');
|
|
1275
|
+
if (!val) return;
|
|
1276
|
+
navigator.clipboard.writeText(val).then(() => {
|
|
1277
|
+
btn.innerHTML = '\u2713';
|
|
1278
|
+
setTimeout(() => {
|
|
1279
|
+
btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1280
|
+
}, 1200);
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const fetchConfig = () => {
|
|
1287
|
+
if (!DASH_API) return;
|
|
1288
|
+
fetchJSON(DASH_API + '/config')
|
|
1289
|
+
.then((data) => {
|
|
1290
|
+
configRawData = data;
|
|
1291
|
+
fetched.config = true;
|
|
1292
|
+
renderConfig();
|
|
1293
|
+
})
|
|
1294
|
+
.catch(() => {
|
|
1295
|
+
if (configBodyEl) configBodyEl.innerHTML = '<div class="ss-dbg-empty">Config not available</div>';
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
const renderConfigTable = (obj, prefix) => {
|
|
1300
|
+
const flat = flattenConfig(obj, prefix);
|
|
1301
|
+
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
1302
|
+
+ '<th style="width:320px">Key</th><th>Value</th>'
|
|
1303
|
+
+ '</tr></thead><tbody>';
|
|
1304
|
+
for (let i = 0; i < flat.length; i++) {
|
|
1305
|
+
const item = flat[i];
|
|
1306
|
+
const relPath = item.path.indexOf(prefix + '.') === 0 ? item.path.slice(prefix.length + 1) : item.path;
|
|
1307
|
+
const redacted = isRedactedObj(item.value);
|
|
1308
|
+
html += '<tr>'
|
|
1309
|
+
+ '<td><span class="ss-dbg-config-key">' + esc(relPath) + '</span></td>'
|
|
1310
|
+
+ '<td>' + (redacted ? renderRedacted(item.value, 'ss-dbg') : '<span class="ss-dbg-config-val">' + formatConfigValue(item.value) + '</span>') + '</td>'
|
|
1311
|
+
+ '</tr>';
|
|
1312
|
+
}
|
|
1313
|
+
html += '</tbody></table>';
|
|
1314
|
+
return html;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const renderConfig = () => {
|
|
1318
|
+
if (!configBodyEl || !configRawData) return;
|
|
1319
|
+
|
|
1320
|
+
const source = configActiveTab === 'env' ? (configRawData.env || {}) : (configRawData.config || configRawData);
|
|
1321
|
+
const flat = flattenConfig(source, '');
|
|
1322
|
+
const term = configSearchTerm.toLowerCase();
|
|
1323
|
+
let filtered = flat;
|
|
1324
|
+
if (term) {
|
|
1325
|
+
filtered = flat.filter((item) => {
|
|
1326
|
+
var valStr = isRedactedObj(item.value) ? item.value.display : String(item.value);
|
|
1327
|
+
return item.path.toLowerCase().indexOf(term) !== -1 || valStr.toLowerCase().indexOf(term) !== -1;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (configSummaryEl) {
|
|
1332
|
+
configSummaryEl.textContent = filtered.length + (term ? ' of ' + flat.length : '') + ' entries';
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
let html = '';
|
|
1336
|
+
|
|
1337
|
+
if (configActiveTab === 'env') {
|
|
1338
|
+
// Env vars: simple table
|
|
1339
|
+
html += '<div class="ss-dbg-config-table-wrap"><table class="ss-dbg-table"><thead><tr>'
|
|
1340
|
+
+ '<th>Variable</th><th>Value</th>'
|
|
1341
|
+
+ '</tr></thead><tbody>';
|
|
1342
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
1343
|
+
const item = filtered[i];
|
|
1344
|
+
const redacted = isRedactedObj(item.value);
|
|
1345
|
+
const displayVal = redacted ? item.value.display : String(item.value);
|
|
1346
|
+
html += '<tr>'
|
|
1347
|
+
+ '<td><span class="ss-dbg-config-key">' + highlightMatch(esc(item.path), term) + '</span></td>'
|
|
1348
|
+
+ '<td>' + (redacted ? renderRedacted(item.value, 'ss-dbg') : '<span class="ss-dbg-config-val">' + highlightMatch(esc(displayVal), term) + '</span>') + '</td>'
|
|
1349
|
+
+ '</tr>';
|
|
1350
|
+
}
|
|
1351
|
+
html += '</tbody></table></div>';
|
|
1352
|
+
} else {
|
|
1353
|
+
if (term) {
|
|
1354
|
+
// Search mode: flat list
|
|
1355
|
+
html += '<div class="ss-dbg-config-table-wrap"><table class="ss-dbg-table"><thead><tr>'
|
|
1356
|
+
+ '<th>Path</th><th>Value</th>'
|
|
1357
|
+
+ '</tr></thead><tbody>';
|
|
1358
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
1359
|
+
const item = filtered[i];
|
|
1360
|
+
const redacted = isRedactedObj(item.value);
|
|
1361
|
+
const displayVal = redacted ? item.value.display : String(item.value);
|
|
1362
|
+
html += '<tr>'
|
|
1363
|
+
+ '<td><span class="ss-dbg-config-key" style="white-space:nowrap">' + highlightMatch(esc(item.path), term) + '</span></td>'
|
|
1364
|
+
+ '<td>' + (redacted ? renderRedacted(item.value, 'ss-dbg') : '<span class="ss-dbg-config-val" style="word-break:break-all">' + highlightMatch(esc(displayVal), term) + '</span>') + '</td>'
|
|
1365
|
+
+ '</tr>';
|
|
1366
|
+
}
|
|
1367
|
+
html += '</tbody></table></div>';
|
|
1368
|
+
} else {
|
|
1369
|
+
// Browse mode: collapsible sections
|
|
1370
|
+
const topKeys = Object.keys(source);
|
|
1371
|
+
html += '<div class="ss-dbg-config-sections">';
|
|
1372
|
+
for (let t = 0; t < topKeys.length; t++) {
|
|
1373
|
+
const sectionKey = topKeys[t];
|
|
1374
|
+
const sectionVal = source[sectionKey];
|
|
1375
|
+
const childCount = countLeaves(sectionVal);
|
|
1376
|
+
const isObj = typeof sectionVal === 'object' && sectionVal !== null && !sectionVal.__redacted;
|
|
1377
|
+
|
|
1378
|
+
html += '<div class="ss-dbg-config-section">';
|
|
1379
|
+
if (isObj) {
|
|
1380
|
+
html += '<div class="ss-dbg-config-section-header" data-config-section="' + esc(sectionKey) + '">'
|
|
1381
|
+
+ '<span class="ss-dbg-config-toggle">\u25B6</span>'
|
|
1382
|
+
+ '<span class="ss-dbg-config-key">' + esc(sectionKey) + '</span>'
|
|
1383
|
+
+ '<span class="ss-dbg-config-count">' + childCount + ' entries</span>'
|
|
1384
|
+
+ '</div>';
|
|
1385
|
+
html += '<div class="ss-dbg-config-section-body" style="display:none">';
|
|
1386
|
+
html += renderConfigTable(sectionVal, sectionKey);
|
|
1387
|
+
html += '</div>';
|
|
1388
|
+
} else {
|
|
1389
|
+
html += '<div class="ss-dbg-config-section-header ss-dbg-config-leaf">'
|
|
1390
|
+
+ '<span class="ss-dbg-config-key">' + esc(sectionKey) + '</span>'
|
|
1391
|
+
+ '<span class="ss-dbg-config-val" style="margin-left:8px">' + formatConfigValue(sectionVal) + '</span>'
|
|
1392
|
+
+ '</div>';
|
|
1393
|
+
}
|
|
1394
|
+
html += '</div>';
|
|
1395
|
+
}
|
|
1396
|
+
html += '</div>';
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
configBodyEl.innerHTML = html;
|
|
1401
|
+
|
|
1402
|
+
// Bind section toggles
|
|
1403
|
+
configBodyEl.querySelectorAll('[data-config-section]').forEach((header) => {
|
|
1404
|
+
header.addEventListener('click', () => {
|
|
1405
|
+
const sectionBody = header.nextElementSibling;
|
|
1406
|
+
if (!sectionBody) return;
|
|
1407
|
+
const isHidden = sectionBody.style.display === 'none';
|
|
1408
|
+
sectionBody.style.display = isHidden ? '' : 'none';
|
|
1409
|
+
const toggle = header.querySelector('.ss-dbg-config-toggle');
|
|
1410
|
+
if (toggle) toggle.textContent = isHidden ? '\u25BC' : '\u25B6';
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// Bind redacted reveal/copy buttons
|
|
1415
|
+
bindRedactedButtons(configBodyEl, 'ss-dbg');
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
configTabs.forEach((btn) => {
|
|
1419
|
+
btn.addEventListener('click', () => {
|
|
1420
|
+
configTabs.forEach((b) => b.classList.remove('ss-dbg-active'));
|
|
1421
|
+
btn.classList.add('ss-dbg-active');
|
|
1422
|
+
configActiveTab = btn.getAttribute('data-ss-dbg-config-tab');
|
|
1423
|
+
renderConfig();
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
if (configSearchInput) {
|
|
1428
|
+
configSearchInput.addEventListener('input', () => {
|
|
1429
|
+
configSearchTerm = configSearchInput.value.trim();
|
|
1430
|
+
renderConfig();
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
891
1434
|
// ── Custom panes: fetch, render, bind ───────────────────────────
|
|
892
1435
|
const getNestedValue = (obj, path) => {
|
|
893
1436
|
const parts = path.split('.');
|
|
@@ -1008,13 +1551,34 @@
|
|
|
1008
1551
|
}
|
|
1009
1552
|
}
|
|
1010
1553
|
|
|
1554
|
+
// ── Connection mode indicator ──────────────────────────────────
|
|
1555
|
+
const POLL_INTERVAL_NORMAL = REFRESH_INTERVAL;
|
|
1556
|
+
const POLL_INTERVAL_LIVE = 15000; // slow polling as fallback when live
|
|
1557
|
+
|
|
1558
|
+
const updateConnectionIndicator = () => {
|
|
1559
|
+
const el = document.getElementById('ss-dbg-conn-mode');
|
|
1560
|
+
if (!el) return;
|
|
1561
|
+
if (isLive) {
|
|
1562
|
+
el.textContent = 'live';
|
|
1563
|
+
el.className = 'ss-dbg-conn-mode ss-dbg-conn-live';
|
|
1564
|
+
el.title = 'Connected via Transmit (SSE) — real-time updates';
|
|
1565
|
+
} else {
|
|
1566
|
+
el.textContent = 'polling';
|
|
1567
|
+
el.className = 'ss-dbg-conn-mode ss-dbg-conn-polling';
|
|
1568
|
+
el.title = 'Polling every ' + (POLL_INTERVAL_NORMAL / 1000) + 's';
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1011
1572
|
// ── Auto-refresh ────────────────────────────────────────────────
|
|
1012
1573
|
const startRefresh = () => {
|
|
1013
1574
|
stopRefresh();
|
|
1575
|
+
fetchMiniStats();
|
|
1576
|
+
const interval = isLive ? POLL_INTERVAL_LIVE : POLL_INTERVAL_NORMAL;
|
|
1014
1577
|
refreshTimer = setInterval(() => {
|
|
1015
1578
|
if (!isOpen) return;
|
|
1016
1579
|
loadTab(activeTab);
|
|
1017
|
-
|
|
1580
|
+
fetchMiniStats();
|
|
1581
|
+
}, interval);
|
|
1018
1582
|
};
|
|
1019
1583
|
|
|
1020
1584
|
const stopRefresh = () => {
|
|
@@ -1024,6 +1588,83 @@
|
|
|
1024
1588
|
}
|
|
1025
1589
|
};
|
|
1026
1590
|
|
|
1591
|
+
// ── Transmit (SSE) support ─────────────────────────────────────
|
|
1592
|
+
const initTransmit = () => {
|
|
1593
|
+
// window.Transmit is set by the inline IIFE injected before this module
|
|
1594
|
+
const TransmitClass = (typeof window !== 'undefined' && window.Transmit) ? window.Transmit : null;
|
|
1595
|
+
|
|
1596
|
+
if (!TransmitClass) return; // Transmit client not available
|
|
1597
|
+
|
|
1598
|
+
try {
|
|
1599
|
+
const transmit = new TransmitClass({
|
|
1600
|
+
baseUrl: window.location.origin,
|
|
1601
|
+
onSubscription: () => {
|
|
1602
|
+
isLive = true;
|
|
1603
|
+
updateConnectionIndicator();
|
|
1604
|
+
// Restart refresh with slower interval now that we have live updates
|
|
1605
|
+
if (isOpen) startRefresh();
|
|
1606
|
+
},
|
|
1607
|
+
onReconnectFailed: () => {
|
|
1608
|
+
isLive = false;
|
|
1609
|
+
updateConnectionIndicator();
|
|
1610
|
+
if (isOpen) startRefresh();
|
|
1611
|
+
},
|
|
1612
|
+
onSubscribeFailed: () => {
|
|
1613
|
+
isLive = false;
|
|
1614
|
+
updateConnectionIndicator();
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
transmitSub = transmit.subscription('server-stats/debug');
|
|
1619
|
+
|
|
1620
|
+
transmitSub.onMessage((message) => {
|
|
1621
|
+
try {
|
|
1622
|
+
const event = typeof message === 'string' ? JSON.parse(message) : message;
|
|
1623
|
+
handleLiveEvent(event);
|
|
1624
|
+
} catch { /* ignore */ }
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
transmitSub.create().catch(() => {
|
|
1628
|
+
isLive = false;
|
|
1629
|
+
updateConnectionIndicator();
|
|
1630
|
+
});
|
|
1631
|
+
} catch {
|
|
1632
|
+
// Transmit init failed — stay on polling
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
const handleLiveEvent = (event) => {
|
|
1637
|
+
if (!isOpen) return;
|
|
1638
|
+
|
|
1639
|
+
// Backend sends { types: ['query', 'event', ...] }
|
|
1640
|
+
const types = event.types || (event.type ? [event.type] : []);
|
|
1641
|
+
const tabMap = {
|
|
1642
|
+
'query': 'queries',
|
|
1643
|
+
'event': 'events',
|
|
1644
|
+
'email': 'emails',
|
|
1645
|
+
'trace': 'timeline'
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
let shouldRefresh = false;
|
|
1649
|
+
for (let i = 0; i < types.length; i++) {
|
|
1650
|
+
const targetTab = tabMap[types[i]];
|
|
1651
|
+
if (targetTab && targetTab === activeTab) {
|
|
1652
|
+
shouldRefresh = true;
|
|
1653
|
+
}
|
|
1654
|
+
if (types[i] === 'query') {
|
|
1655
|
+
updateBarQueryBadge();
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (shouldRefresh) {
|
|
1660
|
+
loadTab(activeTab);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// Initialize Transmit after a short delay to let the page fully load
|
|
1665
|
+
setTimeout(initTransmit, 500);
|
|
1666
|
+
updateConnectionIndicator();
|
|
1667
|
+
|
|
1027
1668
|
// ── Stats bar query badge (always visible) ──────────────────────
|
|
1028
1669
|
const updateBarQueryBadge = () => {
|
|
1029
1670
|
const el = document.getElementById('ss-b-dbg-queries');
|