adonisjs-server-stats 1.4.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +272 -142
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/src/controller/debug_controller.d.ts +2 -2
  4. package/dist/src/controller/debug_controller.d.ts.map +1 -1
  5. package/dist/src/controller/server_stats_controller.d.ts +1 -1
  6. package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
  7. package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
  8. package/dist/src/dashboard/chart_aggregator.js +8 -8
  9. package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
  10. package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
  11. package/dist/src/dashboard/dashboard_controller.js +244 -522
  12. package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
  13. package/dist/src/dashboard/dashboard_routes.js +7 -2
  14. package/dist/src/dashboard/dashboard_store.d.ts +6 -3
  15. package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
  16. package/dist/src/dashboard/dashboard_store.js +54 -78
  17. package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
  18. package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
  19. package/dist/src/dashboard/migrator.d.ts.map +1 -1
  20. package/dist/src/dashboard/migrator.js +3 -1
  21. package/dist/src/dashboard/models/stats_event.d.ts +1 -1
  22. package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
  23. package/dist/src/dashboard/models/stats_query.d.ts +1 -1
  24. package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
  25. package/dist/src/dashboard/models/stats_request.d.ts +2 -2
  26. package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
  27. package/dist/src/dashboard/models/stats_request.js +1 -1
  28. package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
  29. package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
  30. package/dist/src/debug/debug_store.d.ts +6 -6
  31. package/dist/src/debug/debug_store.d.ts.map +1 -1
  32. package/dist/src/debug/debug_store.js +10 -10
  33. package/dist/src/debug/email_collector.d.ts +0 -9
  34. package/dist/src/debug/email_collector.d.ts.map +1 -1
  35. package/dist/src/debug/email_collector.js +6 -28
  36. package/dist/src/debug/event_collector.d.ts +1 -1
  37. package/dist/src/debug/event_collector.d.ts.map +1 -1
  38. package/dist/src/debug/event_collector.js +17 -17
  39. package/dist/src/debug/query_collector.d.ts +1 -1
  40. package/dist/src/debug/query_collector.d.ts.map +1 -1
  41. package/dist/src/debug/query_collector.js +13 -14
  42. package/dist/src/debug/ring_buffer.d.ts.map +1 -1
  43. package/dist/src/debug/route_inspector.d.ts +1 -1
  44. package/dist/src/debug/route_inspector.d.ts.map +1 -1
  45. package/dist/src/debug/route_inspector.js +12 -12
  46. package/dist/src/debug/trace_collector.d.ts.map +1 -1
  47. package/dist/src/debug/trace_collector.js +6 -5
  48. package/dist/src/edge/client/dashboard.css +516 -171
  49. package/dist/src/edge/client/dashboard.js +2756 -1662
  50. package/dist/src/edge/client/debug-panel.css +476 -133
  51. package/dist/src/edge/client/debug-panel.js +1496 -1043
  52. package/dist/src/edge/client/stats-bar.css +64 -30
  53. package/dist/src/edge/client/stats-bar.js +598 -319
  54. package/dist/src/edge/plugin.d.ts +1 -1
  55. package/dist/src/edge/plugin.d.ts.map +1 -1
  56. package/dist/src/edge/plugin.js +41 -59
  57. package/dist/src/edge/views/stats-bar.edge +1 -1
  58. package/dist/src/index.d.ts +1 -1
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
  61. package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
  62. package/dist/src/middleware/request_tracking_middleware.js +7 -6
  63. package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
  64. package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
  65. package/dist/src/provider/server_stats_provider.d.ts +1 -1
  66. package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
  67. package/dist/src/provider/server_stats_provider.js +33 -32
  68. package/dist/src/types.d.ts +2 -2
  69. package/dist/src/utils/json_helpers.d.ts +8 -0
  70. package/dist/src/utils/json_helpers.d.ts.map +1 -0
  71. package/dist/src/utils/json_helpers.js +21 -0
  72. package/dist/src/utils/mail_helpers.d.ts +13 -0
  73. package/dist/src/utils/mail_helpers.d.ts.map +1 -0
  74. package/dist/src/utils/mail_helpers.js +26 -0
  75. package/dist/src/utils/math_helpers.d.ts +8 -0
  76. package/dist/src/utils/math_helpers.d.ts.map +1 -0
  77. package/dist/src/utils/math_helpers.js +11 -0
  78. package/dist/src/utils/time_helpers.d.ts +12 -0
  79. package/dist/src/utils/time_helpers.d.ts.map +1 -0
  80. package/dist/src/utils/time_helpers.js +32 -0
  81. package/dist/src/utils/transmit_client.d.ts +9 -0
  82. package/dist/src/utils/transmit_client.d.ts.map +1 -0
  83. package/dist/src/utils/transmit_client.js +20 -0
  84. package/package.json +35 -29
@@ -7,1681 +7,2134 @@
7
7
  * Config is read from data-* attributes on #ss-dbg-panel:
8
8
  * data-logs-endpoint — logs API URL
9
9
  */
10
- (function () {
11
- const BASE = '/admin/api/debug';
12
- const REFRESH_INTERVAL = 3000;
13
- const panel = document.getElementById('ss-dbg-panel');
14
- const wrench = document.getElementById('ss-dbg-wrench');
15
- const closeBtn = document.getElementById('ss-dbg-close');
10
+ ;(function () {
11
+ const BASE = '/admin/api/debug'
12
+ const REFRESH_INTERVAL = 3000
13
+ const panel = document.getElementById('ss-dbg-panel')
14
+ const wrench = document.getElementById('ss-dbg-wrench')
15
+ const closeBtn = document.getElementById('ss-dbg-close')
16
16
 
17
- if (!panel || !wrench) return;
17
+ if (!panel || !wrench) return
18
18
 
19
19
  // ── Theme detection & toggle ────────────────────────────────────
20
- let themeOverride = localStorage.getItem('ss-dash-theme');
21
- const themeBtn = document.getElementById('ss-dbg-theme-btn');
20
+ let themeOverride = localStorage.getItem('ss-dash-theme')
21
+ const themeBtn = document.getElementById('ss-dbg-theme-btn')
22
22
 
23
23
  const applyPanelTheme = () => {
24
24
  if (themeOverride) {
25
- panel.setAttribute('data-ss-theme', themeOverride);
25
+ panel.setAttribute('data-ss-theme', themeOverride)
26
26
  } else {
27
- panel.removeAttribute('data-ss-theme');
27
+ panel.removeAttribute('data-ss-theme')
28
28
  }
29
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';
30
+ const isDark =
31
+ themeOverride === 'dark' ||
32
+ (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches)
33
+ themeBtn.textContent = isDark ? '\u2600' : '\u263D'
34
+ themeBtn.title = isDark ? 'Switch to light theme' : 'Switch to dark theme'
33
35
  }
34
- };
36
+ }
35
37
 
36
38
  if (themeBtn) {
37
39
  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();
40
+ const isDark =
41
+ themeOverride === 'dark' ||
42
+ (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches)
43
+ themeOverride = isDark ? 'light' : 'dark'
44
+ localStorage.setItem('ss-dash-theme', themeOverride)
45
+ applyPanelTheme()
42
46
  // Sync stats bar if applyBarTheme exists globally
43
- if (typeof window.__ssApplyBarTheme === 'function') window.__ssApplyBarTheme();
44
- });
47
+ if (typeof window.__ssApplyBarTheme === 'function') window.__ssApplyBarTheme()
48
+ })
45
49
  }
46
50
 
47
- applyPanelTheme();
51
+ applyPanelTheme()
48
52
 
49
53
  // Listen for cross-tab theme changes
50
54
  window.addEventListener('storage', function (e) {
51
55
  if (e.key === 'ss-dash-theme') {
52
- themeOverride = e.newValue;
53
- applyPanelTheme();
56
+ themeOverride = e.newValue
57
+ applyPanelTheme()
54
58
  }
55
- });
59
+ })
56
60
 
57
- const LOGS_ENDPOINT = panel.dataset.logsEndpoint || (BASE + '/logs');
61
+ const LOGS_ENDPOINT = panel.dataset.logsEndpoint || BASE + '/logs'
58
62
 
59
- const tracingEnabled = panel.dataset.tracing === '1';
60
- const dashboardPath = panel.dataset.dashboardPath || null;
61
- const DASH_API = dashboardPath ? (dashboardPath.replace(/\/+$/, '') + '/api') : null;
63
+ const tracingEnabled = panel.dataset.tracing === '1'
64
+ const dashboardPath = panel.dataset.dashboardPath || null
65
+ const DASH_API = dashboardPath ? dashboardPath.replace(/\/+$/, '') + '/api' : null
62
66
 
63
67
  /** 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>';
68
+ const deepLinkSvg =
69
+ '<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
70
 
66
71
  /** Build a deep link anchor element HTML string. */
67
72
  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
- };
72
-
73
- let isOpen = false;
74
- let activeTab = tracingEnabled ? 'timeline' : 'queries';
75
- const fetched = {};
76
- let refreshTimer = null;
77
- let logFilter = 'all';
78
- let cachedLogs = [];
79
- const currentPath = window.location.pathname;
80
- let isLive = false;
81
- let transmitSub = null;
73
+ if (!dashboardPath) return ''
74
+ const href = dashboardPath + '#' + section + (id != null ? '?id=' + id : '')
75
+ return (
76
+ ' <a href="' +
77
+ esc(href) +
78
+ '" target="_blank" class="ss-dbg-deeplink" title="Open in dashboard" onclick="event.stopPropagation()">' +
79
+ deepLinkSvg +
80
+ '</a>'
81
+ )
82
+ }
83
+
84
+ let isOpen = false
85
+ let activeTab = tracingEnabled ? 'timeline' : 'queries'
86
+ const fetched = {}
87
+ let refreshTimer = null
88
+ let logFilter = 'all'
89
+ let cachedLogs = []
90
+ const currentPath = window.location.pathname
91
+ let isLive = false
92
+ let transmitSub = null
82
93
 
83
94
  // ── Helpers ──────────────────────────────────────────────────────
84
95
  const esc = (s) => {
85
- if (typeof s !== 'string') s = '' + s;
86
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
87
- };
96
+ if (typeof s !== 'string') s = '' + s
97
+ return s
98
+ .replace(/&/g, '&amp;')
99
+ .replace(/</g, '&lt;')
100
+ .replace(/>/g, '&gt;')
101
+ .replace(/"/g, '&quot;')
102
+ }
88
103
 
89
104
  const timeAgo = (ts) => {
90
- const diff = Math.floor((Date.now() - ts) / 1000);
91
- if (diff < 60) return diff + 's ago';
92
- if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
93
- return Math.floor(diff / 3600) + 'h ago';
94
- };
105
+ const diff = Math.floor((Date.now() - ts) / 1000)
106
+ if (diff < 60) return diff + 's ago'
107
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago'
108
+ return Math.floor(diff / 3600) + 'h ago'
109
+ }
95
110
 
96
111
  const formatTime = (ts) => {
97
- const d = new Date(ts);
98
- return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })
99
- + '.' + String(d.getMilliseconds()).padStart(3, '0');
100
- };
112
+ const d = new Date(ts)
113
+ return (
114
+ d.toLocaleTimeString('en-US', {
115
+ hour12: false,
116
+ hour: '2-digit',
117
+ minute: '2-digit',
118
+ second: '2-digit',
119
+ }) +
120
+ '.' +
121
+ String(d.getMilliseconds()).padStart(3, '0')
122
+ )
123
+ }
101
124
 
102
125
  const eventPreview = (data) => {
103
- if (!data) return '-';
126
+ if (!data) return '-'
104
127
  try {
105
- const parsed = JSON.parse(data);
106
- return compactPreview(parsed, 100);
128
+ const parsed = JSON.parse(data)
129
+ return compactPreview(parsed, 100)
107
130
  } catch {
108
- return data.length > 100 ? data.slice(0, 100) + '...' : data;
131
+ return data.length > 100 ? data.slice(0, 100) + '...' : data
109
132
  }
110
- };
133
+ }
111
134
 
112
135
  const compactPreview = (val, maxLen) => {
113
- if (val === null) return 'null';
114
- if (typeof val === 'string') return '"' + (val.length > 40 ? val.slice(0, 40) + '...' : val) + '"';
115
- if (typeof val === 'number' || typeof val === 'boolean') return String(val);
136
+ if (val === null) return 'null'
137
+ if (typeof val === 'string')
138
+ return '"' + (val.length > 40 ? val.slice(0, 40) + '...' : val) + '"'
139
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val)
116
140
  if (Array.isArray(val)) {
117
- if (val.length === 0) return '[]';
118
- const items = val.slice(0, 3).map((v) => compactPreview(v, 30));
119
- const s = '[' + items.join(', ') + (val.length > 3 ? ', ...' + val.length + ' items' : '') + ']';
120
- return s.length > maxLen ? '[' + val.length + ' items]' : s;
141
+ if (val.length === 0) return '[]'
142
+ const items = val.slice(0, 3).map((v) => compactPreview(v, 30))
143
+ const s =
144
+ '[' + items.join(', ') + (val.length > 3 ? ', ...' + val.length + ' items' : '') + ']'
145
+ return s.length > maxLen ? '[' + val.length + ' items]' : s
121
146
  }
122
147
  if (typeof val === 'object') {
123
- const keys = Object.keys(val);
124
- if (keys.length === 0) return '{}';
125
- const pairs = [];
148
+ const keys = Object.keys(val)
149
+ if (keys.length === 0) return '{}'
150
+ const pairs = []
126
151
  for (let i = 0; i < Math.min(keys.length, 4); i++) {
127
- const k = keys[i];
128
- const v = compactPreview(val[k], 30);
129
- pairs.push(k + ': ' + v);
152
+ const k = keys[i]
153
+ const v = compactPreview(val[k], 30)
154
+ pairs.push(k + ': ' + v)
130
155
  }
131
- const s = '{ ' + pairs.join(', ') + (keys.length > 4 ? ', ...+' + (keys.length - 4) : '') + ' }';
132
- return s.length > maxLen ? '{ ' + keys.slice(0, 6).join(', ') + (keys.length > 6 ? ', ...' : '') + ' }' : s;
156
+ const s =
157
+ '{ ' + pairs.join(', ') + (keys.length > 4 ? ', ...+' + (keys.length - 4) : '') + ' }'
158
+ return s.length > maxLen
159
+ ? '{ ' + keys.slice(0, 6).join(', ') + (keys.length > 6 ? ', ...' : '') + ' }'
160
+ : s
133
161
  }
134
- return String(val);
135
- };
162
+ return String(val)
163
+ }
136
164
 
137
- const methodClass = (m) => 'ss-dbg-method ss-dbg-method-' + (typeof m === 'string' ? m.toLowerCase() : '');
165
+ const methodClass = (m) =>
166
+ 'ss-dbg-method ss-dbg-method-' + (typeof m === 'string' ? m.toLowerCase() : '')
138
167
 
139
168
  const durationClass = (ms) => {
140
- if (ms > 500) return 'ss-dbg-very-slow';
141
- if (ms > 100) return 'ss-dbg-slow';
142
- return '';
143
- };
169
+ if (ms > 500) return 'ss-dbg-very-slow'
170
+ if (ms > 100) return 'ss-dbg-slow'
171
+ return ''
172
+ }
144
173
 
145
174
  // ── Custom pane cell formatter ────────────────────────────────────
146
175
  const formatCell = (value, col) => {
147
- if (value === null || value === undefined) return '<span class="ss-dbg-c-dim">-</span>';
148
- const fmt = col.format || 'text';
176
+ if (value === null || value === undefined) return '<span class="ss-dbg-c-dim">-</span>'
177
+ const fmt = col.format || 'text'
149
178
  switch (fmt) {
150
179
  case 'time':
151
- return typeof value === 'number' ? formatTime(value) : esc(value);
180
+ return typeof value === 'number' ? formatTime(value) : esc(value)
152
181
  case 'timeAgo':
153
- return '<span class="ss-dbg-event-time">' + (typeof value === 'number' ? timeAgo(value) : esc(value)) + '</span>';
182
+ return (
183
+ '<span class="ss-dbg-event-time">' +
184
+ (typeof value === 'number' ? timeAgo(value) : esc(value)) +
185
+ '</span>'
186
+ )
154
187
  case 'duration': {
155
- const ms = typeof value === 'number' ? value : parseFloat(value);
156
- if (isNaN(ms)) return esc(value);
157
- return '<span class="ss-dbg-duration ' + durationClass(ms) + '">' + ms.toFixed(2) + 'ms</span>';
188
+ const ms = typeof value === 'number' ? value : parseFloat(value)
189
+ if (isNaN(ms)) return esc(value)
190
+ return (
191
+ '<span class="ss-dbg-duration ' + durationClass(ms) + '">' + ms.toFixed(2) + 'ms</span>'
192
+ )
158
193
  }
159
194
  case 'method':
160
- return '<span class="' + methodClass(value) + '">' + esc(value) + '</span>';
195
+ return '<span class="' + methodClass(value) + '">' + esc(value) + '</span>'
161
196
  case 'json': {
162
197
  if (typeof value === 'string') {
163
- try { value = JSON.parse(value); } catch { /* use as-is */ }
198
+ try {
199
+ value = JSON.parse(value)
200
+ } catch {
201
+ /* use as-is */
202
+ }
164
203
  }
165
- const preview = typeof value === 'object' ? compactPreview(value, 100) : String(value);
166
- return '<span class="ss-dbg-data-preview" style="cursor:default">' + esc(preview) + '</span>';
204
+ const preview = typeof value === 'object' ? compactPreview(value, 100) : String(value)
205
+ return (
206
+ '<span class="ss-dbg-data-preview" style="cursor:default">' + esc(preview) + '</span>'
207
+ )
167
208
  }
168
209
  case 'badge': {
169
- const sv = String(value).toLowerCase();
170
- const colorMap = col.badgeColorMap || {};
171
- const color = colorMap[sv] || 'muted';
172
- return '<span class="ss-dbg-badge ss-dbg-badge-' + esc(color) + '">' + esc(value) + '</span>';
210
+ const sv = String(value).toLowerCase()
211
+ const colorMap = col.badgeColorMap || {}
212
+ const color = colorMap[sv] || 'muted'
213
+ return (
214
+ '<span class="ss-dbg-badge ss-dbg-badge-' + esc(color) + '">' + esc(value) + '</span>'
215
+ )
173
216
  }
174
217
  default:
175
- return esc(value);
218
+ return esc(value)
176
219
  }
177
- };
220
+ }
178
221
 
179
222
  // ── Toggle panel ────────────────────────────────────────────────
180
223
  const togglePanel = () => {
181
- isOpen = !isOpen;
182
- panel.classList.toggle('ss-dbg-open', isOpen);
183
- wrench.classList.toggle('ss-dbg-active', isOpen);
224
+ isOpen = !isOpen
225
+ panel.classList.toggle('ss-dbg-open', isOpen)
226
+ wrench.classList.toggle('ss-dbg-active', isOpen)
184
227
 
185
228
  if (isOpen) {
186
- loadTab(activeTab);
187
- startRefresh();
229
+ loadTab(activeTab)
230
+ startRefresh()
188
231
  } else {
189
- stopRefresh();
232
+ stopRefresh()
190
233
  }
191
- };
234
+ }
192
235
 
193
236
  wrench.addEventListener('click', (e) => {
194
- e.stopPropagation();
195
- togglePanel();
196
- });
237
+ e.stopPropagation()
238
+ togglePanel()
239
+ })
197
240
 
198
241
  if (closeBtn) {
199
242
  closeBtn.addEventListener('click', () => {
200
- if (isOpen) togglePanel();
201
- });
243
+ if (isOpen) togglePanel()
244
+ })
202
245
  }
203
246
 
204
247
  document.addEventListener('keydown', (e) => {
205
- if (e.key === 'Escape' && isOpen) togglePanel();
206
- });
248
+ if (e.key === 'Escape' && isOpen) togglePanel()
249
+ })
207
250
 
208
251
  // ── Custom panes config ─────────────────────────────────────────
209
- let customPanes = [];
210
- const customPaneState = {};
252
+ let customPanes = []
253
+ const customPaneState = {}
211
254
  try {
212
- const cfgEl = document.getElementById('ss-dbg-custom-panes-config');
213
- if (cfgEl) customPanes = JSON.parse(cfgEl.textContent || '[]');
214
- } catch { /* ignore */ }
255
+ const cfgEl = document.getElementById('ss-dbg-custom-panes-config')
256
+ if (cfgEl) customPanes = JSON.parse(cfgEl.textContent || '[]')
257
+ } catch {
258
+ /* ignore */
259
+ }
215
260
 
216
261
  for (let i = 0; i < customPanes.length; i++) {
217
- const cp = customPanes[i];
218
- customPaneState[cp.id] = { data: [], fetched: false, filter: '' };
262
+ const cp = customPanes[i]
263
+ customPaneState[cp.id] = { data: [], fetched: false, filter: '' }
219
264
  }
220
265
 
221
266
  // ── Tab switching ───────────────────────────────────────────────
222
- const tabs = panel.querySelectorAll('[data-ss-dbg-tab]');
267
+ const tabs = panel.querySelectorAll('[data-ss-dbg-tab]')
223
268
  tabs.forEach((tab) => {
224
269
  tab.addEventListener('click', () => {
225
- const name = tab.getAttribute('data-ss-dbg-tab');
226
- if (name === activeTab) return;
270
+ const name = tab.getAttribute('data-ss-dbg-tab')
271
+ if (name === activeTab) return
227
272
 
228
- tabs.forEach((t) => t.classList.remove('ss-dbg-active'));
229
- panel.querySelectorAll('.ss-dbg-pane').forEach((p) => p.classList.remove('ss-dbg-active'));
273
+ tabs.forEach((t) => t.classList.remove('ss-dbg-active'))
274
+ panel.querySelectorAll('.ss-dbg-pane').forEach((p) => p.classList.remove('ss-dbg-active'))
230
275
 
231
- tab.classList.add('ss-dbg-active');
232
- tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
233
- const pane = document.getElementById('ss-dbg-pane-' + name);
234
- if (pane) pane.classList.add('ss-dbg-active');
276
+ tab.classList.add('ss-dbg-active')
277
+ tab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
278
+ const pane = document.getElementById('ss-dbg-pane-' + name)
279
+ if (pane) pane.classList.add('ss-dbg-active')
235
280
 
236
- activeTab = name;
237
- loadTab(name);
238
- });
239
- });
281
+ activeTab = name
282
+ loadTab(name)
283
+ })
284
+ })
240
285
 
241
286
  // ── Data loading ────────────────────────────────────────────────
242
287
  const loadTab = (name) => {
243
- if (name === 'timeline') fetchTraces();
244
- else if (name === 'queries') fetchQueries();
245
- else if (name === 'events') fetchEvents();
246
- else if (name === 'routes' && !fetched.routes) fetchRoutes();
247
- else if (name === 'logs') fetchLogs();
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();
288
+ if (name === 'timeline') fetchTraces()
289
+ else if (name === 'queries') fetchQueries()
290
+ else if (name === 'events') fetchEvents()
291
+ else if (name === 'routes' && !fetched.routes) fetchRoutes()
292
+ else if (name === 'logs') fetchLogs()
293
+ else if (name === 'emails') fetchEmails()
294
+ else if (name === 'cache') fetchCache()
295
+ else if (name === 'jobs') fetchJobs()
296
+ else if (name === 'config' && !fetched.config) fetchConfig()
252
297
  else {
253
- const cp = customPanes.find((p) => p.id === name);
298
+ const cp = customPanes.find((p) => p.id === name)
254
299
  if (cp) {
255
- if (cp.fetchOnce && customPaneState[cp.id].fetched) return;
256
- fetchCustomPane(cp);
300
+ if (cp.fetchOnce && customPaneState[cp.id].fetched) return
301
+ fetchCustomPane(cp)
257
302
  }
258
303
  }
259
- };
304
+ }
260
305
 
261
306
  const fetchJSON = (url) =>
262
- fetch(url, { credentials: 'same-origin' })
263
- .then((r) => {
264
- if (!r.ok) throw new Error(r.status);
265
- return r.json();
266
- });
307
+ fetch(url, { credentials: 'same-origin' }).then((r) => {
308
+ if (!r.ok) throw new Error(r.status)
309
+ return r.json()
310
+ })
267
311
 
268
312
  // ── Queries Tab ─────────────────────────────────────────────────
269
- const querySearchInput = document.getElementById('ss-dbg-search-queries');
270
- const querySummaryEl = document.getElementById('ss-dbg-queries-summary');
271
- const queryBodyEl = document.getElementById('ss-dbg-queries-body');
272
- const queryClearBtn = document.getElementById('ss-dbg-queries-clear');
273
- let cachedQueries = { queries: [], summary: {} };
313
+ const querySearchInput = document.getElementById('ss-dbg-search-queries')
314
+ const querySummaryEl = document.getElementById('ss-dbg-queries-summary')
315
+ const queryBodyEl = document.getElementById('ss-dbg-queries-body')
316
+ const queryClearBtn = document.getElementById('ss-dbg-queries-clear')
317
+ let cachedQueries = { queries: [], summary: {} }
274
318
 
275
319
  const fetchQueries = () => {
276
320
  fetchJSON(BASE + '/queries')
277
321
  .then((data) => {
278
- cachedQueries = data;
279
- renderQueries();
322
+ cachedQueries = data
323
+ renderQueries()
280
324
  })
281
325
  .catch(() => {
282
- queryBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load queries</div>';
283
- });
284
- };
326
+ queryBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load queries</div>'
327
+ })
328
+ }
285
329
 
286
330
  const renderQueries = () => {
287
- const filter = (querySearchInput ? querySearchInput.value : '').toLowerCase();
288
- const queries = cachedQueries.queries || [];
289
- const summary = cachedQueries.summary || {};
331
+ const filter = (querySearchInput ? querySearchInput.value : '').toLowerCase()
332
+ const queries = cachedQueries.queries || []
333
+ const summary = cachedQueries.summary || {}
290
334
 
291
335
  if (querySummaryEl) {
292
- querySummaryEl.textContent = summary.total + ' queries'
293
- + (summary.slow > 0 ? ', ' + summary.slow + ' slow' : '')
294
- + (summary.duplicates > 0 ? ', ' + summary.duplicates + ' dup' : '')
295
- + ', avg ' + (summary.avgDuration || 0).toFixed(1) + 'ms';
336
+ querySummaryEl.textContent =
337
+ summary.total +
338
+ ' queries' +
339
+ (summary.slow > 0 ? ', ' + summary.slow + ' slow' : '') +
340
+ (summary.duplicates > 0 ? ', ' + summary.duplicates + ' dup' : '') +
341
+ ', avg ' +
342
+ (summary.avgDuration || 0).toFixed(1) +
343
+ 'ms'
296
344
  }
297
345
 
298
- const badge = document.getElementById('ss-dbg-query-badge');
346
+ const badge = document.getElementById('ss-dbg-query-badge')
299
347
  if (badge && activeTab === 'queries') {
300
- badge.textContent = summary.total + ' queries, avg ' + (summary.avgDuration || 0).toFixed(1) + 'ms';
348
+ badge.textContent =
349
+ summary.total + ' queries, avg ' + (summary.avgDuration || 0).toFixed(1) + 'ms'
301
350
  }
302
351
 
303
- let filtered = queries;
352
+ let filtered = queries
304
353
  if (filter) {
305
- filtered = queries.filter((q) =>
306
- q.sql.toLowerCase().indexOf(filter) !== -1
307
- || (q.model || '').toLowerCase().indexOf(filter) !== -1
308
- || q.method.toLowerCase().indexOf(filter) !== -1
309
- );
354
+ filtered = queries.filter(
355
+ (q) =>
356
+ q.sql.toLowerCase().indexOf(filter) !== -1 ||
357
+ (q.model || '').toLowerCase().indexOf(filter) !== -1 ||
358
+ q.method.toLowerCase().indexOf(filter) !== -1
359
+ )
310
360
  }
311
361
 
312
362
  if (filtered.length === 0) {
313
- queryBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching queries' : 'No queries recorded yet') + '</div>';
314
- return;
363
+ queryBodyEl.innerHTML =
364
+ '<div class="ss-dbg-empty">' +
365
+ (filter ? 'No matching queries' : 'No queries recorded yet') +
366
+ '</div>'
367
+ return
315
368
  }
316
369
 
317
- const sqlCounts = {};
370
+ const sqlCounts = {}
318
371
  for (let i = 0; i < queries.length; i++) {
319
- sqlCounts[queries[i].sql] = (sqlCounts[queries[i].sql] || 0) + 1;
372
+ sqlCounts[queries[i].sql] = (sqlCounts[queries[i].sql] || 0) + 1
320
373
  }
321
374
 
322
- let html = '<table class="ss-dbg-table"><thead><tr>'
323
- + '<th style="width:64px">#</th>'
324
- + '<th>SQL</th>'
325
- + '<th style="width:70px">Duration</th>'
326
- + '<th style="width:60px">Method</th>'
327
- + '<th style="width:100px">Model</th>'
328
- + '<th style="width:60px">Time</th>'
329
- + '</tr></thead><tbody>';
375
+ let html =
376
+ '<table class="ss-dbg-table"><thead><tr>' +
377
+ '<th style="width:64px">#</th>' +
378
+ '<th>SQL</th>' +
379
+ '<th style="width:70px">Duration</th>' +
380
+ '<th style="width:60px">Method</th>' +
381
+ '<th style="width:100px">Model</th>' +
382
+ '<th style="width:60px">Time</th>' +
383
+ '</tr></thead><tbody>'
330
384
 
331
385
  for (let j = 0; j < filtered.length; j++) {
332
- const q = filtered[j];
333
- const durClass = durationClass(q.duration);
334
- const dupCount = sqlCounts[q.sql] || 1;
335
- html += '<tr>'
336
- + '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + q.id + deepLink('queries', q.id) + '</td>'
337
- + '<td><span class="ss-dbg-sql" title="Click to expand" onclick="this.classList.toggle(\'ss-dbg-expanded\')">' + esc(q.sql) + '</span>'
338
- + (dupCount > 1 ? ' <span class="ss-dbg-dup">x' + dupCount + '</span>' : '')
339
- + '</td>'
340
- + '<td class="ss-dbg-duration ' + durClass + '">' + q.duration.toFixed(2) + 'ms</td>'
341
- + '<td><span class="' + methodClass(q.method) + '">' + esc(q.method) + '</span></td>'
342
- + '<td class="ss-dbg-c-muted">' + esc(q.model || '-') + '</td>'
343
- + '<td class="ss-dbg-event-time">' + timeAgo(q.timestamp) + '</td>'
344
- + '</tr>';
345
- }
346
-
347
- html += '</tbody></table>';
348
- queryBodyEl.innerHTML = html;
349
- };
350
-
351
- if (querySearchInput) querySearchInput.addEventListener('input', renderQueries);
386
+ const q = filtered[j]
387
+ const durClass = durationClass(q.duration)
388
+ const dupCount = sqlCounts[q.sql] || 1
389
+ html +=
390
+ '<tr>' +
391
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' +
392
+ q.id +
393
+ deepLink('queries', q.id) +
394
+ '</td>' +
395
+ '<td><span class="ss-dbg-sql" title="Click to expand" onclick="this.classList.toggle(\'ss-dbg-expanded\')">' +
396
+ esc(q.sql) +
397
+ '</span>' +
398
+ (dupCount > 1 ? ' <span class="ss-dbg-dup">x' + dupCount + '</span>' : '') +
399
+ '</td>' +
400
+ '<td class="ss-dbg-duration ' +
401
+ durClass +
402
+ '">' +
403
+ q.duration.toFixed(2) +
404
+ 'ms</td>' +
405
+ '<td><span class="' +
406
+ methodClass(q.method) +
407
+ '">' +
408
+ esc(q.method) +
409
+ '</span></td>' +
410
+ '<td class="ss-dbg-c-muted">' +
411
+ esc(q.model || '-') +
412
+ '</td>' +
413
+ '<td class="ss-dbg-event-time">' +
414
+ timeAgo(q.timestamp) +
415
+ '</td>' +
416
+ '</tr>'
417
+ }
418
+
419
+ html += '</tbody></table>'
420
+ queryBodyEl.innerHTML = html
421
+ }
422
+
423
+ if (querySearchInput) querySearchInput.addEventListener('input', renderQueries)
352
424
  if (queryClearBtn) {
353
425
  queryClearBtn.addEventListener('click', () => {
354
- cachedQueries = { queries: [], summary: { total: 0, slow: 0, duplicates: 0, avgDuration: 0 } };
355
- renderQueries();
356
- });
426
+ cachedQueries = { queries: [], summary: { total: 0, slow: 0, duplicates: 0, avgDuration: 0 } }
427
+ renderQueries()
428
+ })
357
429
  }
358
430
 
359
431
  // ── Events Tab ──────────────────────────────────────────────────
360
- const eventSearchInput = document.getElementById('ss-dbg-search-events');
361
- const eventSummaryEl = document.getElementById('ss-dbg-events-summary');
362
- const eventBodyEl = document.getElementById('ss-dbg-events-body');
363
- const eventClearBtn = document.getElementById('ss-dbg-events-clear');
364
- let cachedEvents = { events: [], total: 0 };
432
+ const eventSearchInput = document.getElementById('ss-dbg-search-events')
433
+ const eventSummaryEl = document.getElementById('ss-dbg-events-summary')
434
+ const eventBodyEl = document.getElementById('ss-dbg-events-body')
435
+ const eventClearBtn = document.getElementById('ss-dbg-events-clear')
436
+ let cachedEvents = { events: [], total: 0 }
365
437
 
366
438
  const fetchEvents = () => {
367
439
  fetchJSON(BASE + '/events')
368
440
  .then((data) => {
369
- cachedEvents = data;
370
- renderEvents();
441
+ cachedEvents = data
442
+ renderEvents()
371
443
  })
372
444
  .catch(() => {
373
- eventBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load events</div>';
374
- });
375
- };
445
+ eventBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load events</div>'
446
+ })
447
+ }
376
448
 
377
449
  const renderEvents = () => {
378
- const filter = (eventSearchInput ? eventSearchInput.value : '').toLowerCase();
379
- const events = cachedEvents.events || [];
450
+ const filter = (eventSearchInput ? eventSearchInput.value : '').toLowerCase()
451
+ const events = cachedEvents.events || []
380
452
 
381
453
  if (eventSummaryEl) {
382
- eventSummaryEl.textContent = cachedEvents.total + ' events';
454
+ eventSummaryEl.textContent = cachedEvents.total + ' events'
383
455
  }
384
456
 
385
- let filtered = events;
457
+ let filtered = events
386
458
  if (filter) {
387
- filtered = events.filter((e) =>
388
- e.event.toLowerCase().indexOf(filter) !== -1
389
- || (e.data || '').toLowerCase().indexOf(filter) !== -1
390
- );
459
+ filtered = events.filter(
460
+ (e) =>
461
+ e.event.toLowerCase().indexOf(filter) !== -1 ||
462
+ (e.data || '').toLowerCase().indexOf(filter) !== -1
463
+ )
391
464
  }
392
465
 
393
466
  if (filtered.length === 0) {
394
- eventBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching events' : 'No events recorded yet') + '</div>';
395
- return;
467
+ eventBodyEl.innerHTML =
468
+ '<div class="ss-dbg-empty">' +
469
+ (filter ? 'No matching events' : 'No events recorded yet') +
470
+ '</div>'
471
+ return
396
472
  }
397
473
 
398
- let html = '<table class="ss-dbg-table"><thead><tr>'
399
- + '<th style="width:64px">#</th>'
400
- + '<th>Event</th>'
401
- + '<th>Data</th>'
402
- + '<th style="width:100px">Time</th>'
403
- + '</tr></thead><tbody>';
474
+ let html =
475
+ '<table class="ss-dbg-table"><thead><tr>' +
476
+ '<th style="width:64px">#</th>' +
477
+ '<th>Event</th>' +
478
+ '<th>Data</th>' +
479
+ '<th style="width:100px">Time</th>' +
480
+ '</tr></thead><tbody>'
404
481
 
405
482
  for (let i = 0; i < filtered.length; i++) {
406
- const ev = filtered[i];
407
- const hasData = ev.data && ev.data !== '-';
408
- const preview = hasData ? eventPreview(ev.data) : '-';
409
- html += '<tr>'
410
- + '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + ev.id + deepLink('events', ev.id) + '</td>'
411
- + '<td class="ss-dbg-event-name">' + esc(ev.event) + '</td>'
412
- + '<td class="ss-dbg-event-data">'
413
- + (hasData
414
- ? '<span class="ss-dbg-data-preview" data-ev-idx="' + i + '">' + esc(preview) + '</span>'
415
- + '<pre class="ss-dbg-data-full" id="ss-dbg-evdata-' + i + '" style="display:none">' + esc(ev.data) + '</pre>'
416
- + '<button type="button" class="ss-dbg-copy-btn" data-copy-idx="' + i + '" title="Copy JSON">&#x2398;</button>'
417
- : '<span class="ss-dbg-c-dim">-</span>')
418
- + '</td>'
419
- + '<td class="ss-dbg-event-time">' + formatTime(ev.timestamp) + '</td>'
420
- + '</tr>';
421
- }
422
-
423
- html += '</tbody></table>';
424
- eventBodyEl.innerHTML = html;
483
+ const ev = filtered[i]
484
+ const hasData = ev.data && ev.data !== '-'
485
+ const preview = hasData ? eventPreview(ev.data) : '-'
486
+ html +=
487
+ '<tr>' +
488
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' +
489
+ ev.id +
490
+ deepLink('events', ev.id) +
491
+ '</td>' +
492
+ '<td class="ss-dbg-event-name">' +
493
+ esc(ev.event) +
494
+ '</td>' +
495
+ '<td class="ss-dbg-event-data">' +
496
+ (hasData
497
+ ? '<span class="ss-dbg-data-preview" data-ev-idx="' +
498
+ i +
499
+ '">' +
500
+ esc(preview) +
501
+ '</span>' +
502
+ '<pre class="ss-dbg-data-full" id="ss-dbg-evdata-' +
503
+ i +
504
+ '" style="display:none">' +
505
+ esc(ev.data) +
506
+ '</pre>' +
507
+ '<button type="button" class="ss-dbg-copy-btn" data-copy-idx="' +
508
+ i +
509
+ '" title="Copy JSON">&#x2398;</button>'
510
+ : '<span class="ss-dbg-c-dim">-</span>') +
511
+ '</td>' +
512
+ '<td class="ss-dbg-event-time">' +
513
+ formatTime(ev.timestamp) +
514
+ '</td>' +
515
+ '</tr>'
516
+ }
517
+
518
+ html += '</tbody></table>'
519
+ eventBodyEl.innerHTML = html
425
520
 
426
521
  // Toggle expand on preview click
427
522
  eventBodyEl.querySelectorAll('.ss-dbg-data-preview').forEach((el) => {
428
523
  el.addEventListener('click', () => {
429
- const idx = el.getAttribute('data-ev-idx');
430
- const pre = document.getElementById('ss-dbg-evdata-' + idx);
524
+ const idx = el.getAttribute('data-ev-idx')
525
+ const pre = document.getElementById('ss-dbg-evdata-' + idx)
431
526
  if (pre) {
432
- const open = pre.style.display !== 'none';
433
- pre.style.display = open ? 'none' : 'block';
434
- el.style.display = open ? '' : 'none';
527
+ const open = pre.style.display !== 'none'
528
+ pre.style.display = open ? 'none' : 'block'
529
+ el.style.display = open ? '' : 'none'
435
530
  }
436
- });
437
- });
531
+ })
532
+ })
438
533
 
439
534
  // Collapse on full-data click
440
535
  eventBodyEl.querySelectorAll('.ss-dbg-data-full').forEach((el) => {
441
536
  el.addEventListener('click', () => {
442
- el.style.display = 'none';
443
- const idx = el.id.replace('ss-dbg-evdata-', '');
444
- const preview = eventBodyEl.querySelector('[data-ev-idx="' + idx + '"]');
445
- if (preview) preview.style.display = '';
446
- });
447
- });
537
+ el.style.display = 'none'
538
+ const idx = el.id.replace('ss-dbg-evdata-', '')
539
+ const preview = eventBodyEl.querySelector('[data-ev-idx="' + idx + '"]')
540
+ if (preview) preview.style.display = ''
541
+ })
542
+ })
448
543
 
449
544
  // Copy button
450
545
  eventBodyEl.querySelectorAll('.ss-dbg-copy-btn').forEach((btn) => {
451
546
  btn.addEventListener('click', (e) => {
452
- e.stopPropagation();
453
- const idx = btn.getAttribute('data-copy-idx');
454
- const data = filtered[idx]?.data || '';
547
+ e.stopPropagation()
548
+ const idx = btn.getAttribute('data-copy-idx')
549
+ const data = filtered[idx]?.data || ''
455
550
  navigator.clipboard.writeText(data).then(() => {
456
- btn.textContent = '\u2713';
457
- setTimeout(() => { btn.innerHTML = '&#x2398;'; }, 1200);
458
- });
459
- });
460
- });
461
- };
462
-
463
- if (eventSearchInput) eventSearchInput.addEventListener('input', renderEvents);
551
+ btn.textContent = '\u2713'
552
+ setTimeout(() => {
553
+ btn.innerHTML = '&#x2398;'
554
+ }, 1200)
555
+ })
556
+ })
557
+ })
558
+ }
559
+
560
+ if (eventSearchInput) eventSearchInput.addEventListener('input', renderEvents)
464
561
  if (eventClearBtn) {
465
562
  eventClearBtn.addEventListener('click', () => {
466
- cachedEvents = { events: [], total: 0 };
467
- renderEvents();
468
- });
563
+ cachedEvents = { events: [], total: 0 }
564
+ renderEvents()
565
+ })
469
566
  }
470
567
 
471
568
  // ── Routes Tab ──────────────────────────────────────────────────
472
- const routeSearchInput = document.getElementById('ss-dbg-search-routes');
473
- const routeSummaryEl = document.getElementById('ss-dbg-routes-summary');
474
- const routeBodyEl = document.getElementById('ss-dbg-routes-body');
475
- let cachedRoutes = { routes: [], total: 0 };
569
+ const routeSearchInput = document.getElementById('ss-dbg-search-routes')
570
+ const routeSummaryEl = document.getElementById('ss-dbg-routes-summary')
571
+ const routeBodyEl = document.getElementById('ss-dbg-routes-body')
572
+ let cachedRoutes = { routes: [], total: 0 }
476
573
 
477
574
  const fetchRoutes = () => {
478
575
  fetchJSON(BASE + '/routes')
479
576
  .then((data) => {
480
- cachedRoutes = data;
481
- fetched.routes = true;
482
- renderRoutes();
577
+ cachedRoutes = data
578
+ fetched.routes = true
579
+ renderRoutes()
483
580
  })
484
581
  .catch(() => {
485
- routeBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load routes</div>';
486
- });
487
- };
582
+ routeBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load routes</div>'
583
+ })
584
+ }
488
585
 
489
586
  const renderRoutes = () => {
490
- const filter = (routeSearchInput ? routeSearchInput.value : '').toLowerCase();
491
- const routes = cachedRoutes.routes || [];
587
+ const filter = (routeSearchInput ? routeSearchInput.value : '').toLowerCase()
588
+ const routes = cachedRoutes.routes || []
492
589
 
493
590
  if (routeSummaryEl) {
494
- routeSummaryEl.textContent = cachedRoutes.total + ' routes';
591
+ routeSummaryEl.textContent = cachedRoutes.total + ' routes'
495
592
  }
496
593
 
497
- let filtered = routes;
594
+ let filtered = routes
498
595
  if (filter) {
499
- filtered = routes.filter((r) =>
500
- r.pattern.toLowerCase().indexOf(filter) !== -1
501
- || r.method.toLowerCase().indexOf(filter) !== -1
502
- || (r.name || '').toLowerCase().indexOf(filter) !== -1
503
- || r.handler.toLowerCase().indexOf(filter) !== -1
504
- || r.middleware.join(' ').toLowerCase().indexOf(filter) !== -1
505
- );
596
+ filtered = routes.filter(
597
+ (r) =>
598
+ r.pattern.toLowerCase().indexOf(filter) !== -1 ||
599
+ r.method.toLowerCase().indexOf(filter) !== -1 ||
600
+ (r.name || '').toLowerCase().indexOf(filter) !== -1 ||
601
+ r.handler.toLowerCase().indexOf(filter) !== -1 ||
602
+ r.middleware.join(' ').toLowerCase().indexOf(filter) !== -1
603
+ )
506
604
  }
507
605
 
508
606
  if (filtered.length === 0) {
509
- routeBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching routes' : 'No routes available') + '</div>';
510
- return;
511
- }
512
-
513
- let html = '<table class="ss-dbg-table"><thead><tr>'
514
- + '<th style="width:60px">Method</th>'
515
- + '<th>Pattern</th>'
516
- + '<th style="width:140px">Name</th>'
517
- + '<th>Handler</th>'
518
- + '<th>Middleware</th>'
519
- + '</tr></thead><tbody>';
607
+ routeBodyEl.innerHTML =
608
+ '<div class="ss-dbg-empty">' +
609
+ (filter ? 'No matching routes' : 'No routes available') +
610
+ '</div>'
611
+ return
612
+ }
613
+
614
+ let html =
615
+ '<table class="ss-dbg-table"><thead><tr>' +
616
+ '<th style="width:60px">Method</th>' +
617
+ '<th>Pattern</th>' +
618
+ '<th style="width:140px">Name</th>' +
619
+ '<th>Handler</th>' +
620
+ '<th>Middleware</th>' +
621
+ '</tr></thead><tbody>'
520
622
 
521
623
  for (let i = 0; i < filtered.length; i++) {
522
- const r = filtered[i];
523
- const isCurrent = currentPath === r.pattern || currentPath.match(new RegExp('^' + r.pattern.replace(/:[^/]+/g, '[^/]+') + '$'));
524
- html += '<tr' + (isCurrent ? ' class="ss-dbg-current-route"' : '') + '>'
525
- + '<td><span class="' + methodClass(r.method) + '">' + esc(r.method) + '</span></td>'
526
- + '<td>' + esc(r.pattern) + '</td>'
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>'
530
- + '</tr>';
531
- }
532
-
533
- html += '</tbody></table>';
534
- routeBodyEl.innerHTML = html;
535
- };
624
+ const r = filtered[i]
625
+ const isCurrent =
626
+ currentPath === r.pattern ||
627
+ currentPath.match(new RegExp('^' + r.pattern.replace(/:[^/]+/g, '[^/]+') + '$'))
628
+ html +=
629
+ '<tr' +
630
+ (isCurrent ? ' class="ss-dbg-current-route"' : '') +
631
+ '>' +
632
+ '<td><span class="' +
633
+ methodClass(r.method) +
634
+ '">' +
635
+ esc(r.method) +
636
+ '</span></td>' +
637
+ '<td>' +
638
+ esc(r.pattern) +
639
+ '</td>' +
640
+ '<td class="ss-dbg-c-muted">' +
641
+ esc(r.name || '-') +
642
+ '</td>' +
643
+ '<td class="ss-dbg-c-sql">' +
644
+ esc(r.handler) +
645
+ '</td>' +
646
+ '<td class="ss-dbg-c-dim" style="font-size:10px">' +
647
+ (r.middleware.length ? esc(r.middleware.join(', ')) : '-') +
648
+ '</td>' +
649
+ '</tr>'
650
+ }
651
+
652
+ html += '</tbody></table>'
653
+ routeBodyEl.innerHTML = html
654
+ }
536
655
 
537
- if (routeSearchInput) routeSearchInput.addEventListener('input', renderRoutes);
656
+ if (routeSearchInput) routeSearchInput.addEventListener('input', renderRoutes)
538
657
 
539
658
  // ── Logs Tab ────────────────────────────────────────────────────
540
- const logBodyEl = document.getElementById('ss-dbg-logs-body');
541
- const logFilters = panel.querySelectorAll('[data-ss-dbg-level]');
542
- const logReqIdInput = document.getElementById('ss-dbg-log-reqid');
543
- const logReqIdClear = document.getElementById('ss-dbg-log-reqid-clear');
544
- let logReqIdFilter = '';
659
+ const logBodyEl = document.getElementById('ss-dbg-logs-body')
660
+ const logFilters = panel.querySelectorAll('[data-ss-dbg-level]')
661
+ const logReqIdInput = document.getElementById('ss-dbg-log-reqid')
662
+ const logReqIdClear = document.getElementById('ss-dbg-log-reqid-clear')
663
+ let logReqIdFilter = ''
545
664
 
546
665
  const setReqIdFilter = (id) => {
547
- logReqIdFilter = id || '';
548
- if (logReqIdInput) logReqIdInput.value = logReqIdFilter;
549
- if (logReqIdClear) logReqIdClear.style.display = logReqIdFilter ? '' : 'none';
550
- renderLogs();
551
- };
666
+ logReqIdFilter = id || ''
667
+ if (logReqIdInput) logReqIdInput.value = logReqIdFilter
668
+ if (logReqIdClear) logReqIdClear.style.display = logReqIdFilter ? '' : 'none'
669
+ renderLogs()
670
+ }
552
671
 
553
672
  if (logReqIdInput) {
554
673
  logReqIdInput.addEventListener('input', () => {
555
- logReqIdFilter = logReqIdInput.value.trim();
556
- if (logReqIdClear) logReqIdClear.style.display = logReqIdFilter ? '' : 'none';
557
- renderLogs();
558
- });
674
+ logReqIdFilter = logReqIdInput.value.trim()
675
+ if (logReqIdClear) logReqIdClear.style.display = logReqIdFilter ? '' : 'none'
676
+ renderLogs()
677
+ })
559
678
  }
560
679
  if (logReqIdClear) {
561
- logReqIdClear.addEventListener('click', () => setReqIdFilter(''));
680
+ logReqIdClear.addEventListener('click', () => setReqIdFilter(''))
562
681
  }
563
682
 
564
683
  const fetchLogs = () => {
565
684
  fetchJSON(LOGS_ENDPOINT)
566
685
  .then((data) => {
567
- cachedLogs = Array.isArray(data) ? data : (data.logs || data.entries || []);
568
- renderLogs();
686
+ cachedLogs = Array.isArray(data) ? data : data.logs || data.entries || []
687
+ renderLogs()
569
688
  })
570
689
  .catch(() => {
571
- logBodyEl.innerHTML = '<div class="ss-dbg-empty">No log endpoint available</div>';
572
- });
573
- };
690
+ logBodyEl.innerHTML = '<div class="ss-dbg-empty">No log endpoint available</div>'
691
+ })
692
+ }
574
693
 
575
- const shortReqId = (id) => id ? id.slice(0, 8) : '';
694
+ const shortReqId = (id) => (id ? id.slice(0, 8) : '')
576
695
 
577
696
  const renderLogs = () => {
578
- let entries = cachedLogs;
697
+ let entries = cachedLogs
579
698
 
580
699
  if (logFilter !== 'all') {
581
700
  entries = entries.filter((e) => {
582
- const level = (e.levelName || e.level_name || '').toLowerCase();
583
- if (logFilter === 'error') return level === 'error' || level === 'fatal';
584
- return level === logFilter;
585
- });
701
+ const level = (e.levelName || e.level_name || '').toLowerCase()
702
+ if (logFilter === 'error') return level === 'error' || level === 'fatal'
703
+ return level === logFilter
704
+ })
586
705
  }
587
706
 
588
707
  if (logReqIdFilter) {
589
- const f = logReqIdFilter.toLowerCase();
708
+ const f = logReqIdFilter.toLowerCase()
590
709
  entries = entries.filter((e) => {
591
- const rid = (e.request_id || e['x-request-id'] || '').toLowerCase();
592
- return rid.indexOf(f) !== -1;
593
- });
710
+ const rid = (e.request_id || e['x-request-id'] || '').toLowerCase()
711
+ return rid.indexOf(f) !== -1
712
+ })
594
713
  }
595
714
 
596
715
  if (entries.length === 0) {
597
- let hint = '';
598
- if (logReqIdFilter) hint = ' matching request ' + logReqIdFilter;
599
- else if (logFilter !== 'all') hint = ' for ' + logFilter;
600
- logBodyEl.innerHTML = '<div class="ss-dbg-empty">No log entries' + hint + '</div>';
601
- return;
716
+ let hint = ''
717
+ if (logReqIdFilter) hint = ' matching request ' + logReqIdFilter
718
+ else if (logFilter !== 'all') hint = ' for ' + logFilter
719
+ logBodyEl.innerHTML = '<div class="ss-dbg-empty">No log entries' + hint + '</div>'
720
+ return
602
721
  }
603
722
 
604
- const shown = entries.slice(-200).reverse();
605
- let html = '';
723
+ const shown = entries.slice(-200).reverse()
724
+ let html = ''
606
725
 
607
726
  for (let i = 0; i < shown.length; i++) {
608
- const e = shown[i];
609
- const level = (e.levelName || e.level_name || 'info').toLowerCase();
610
- const msg = e.msg || e.message || JSON.stringify(e);
611
- const ts = e.time || e.timestamp || 0;
612
- const reqId = e.request_id || e['x-request-id'] || '';
613
-
614
- html += '<div class="ss-dbg-log-entry">'
615
- + '<span class="ss-dbg-log-level ss-dbg-log-level-' + esc(level) + '">' + esc(level.toUpperCase()) + '</span>'
616
- + '<span class="ss-dbg-log-time">' + (ts ? formatTime(ts) : '-') + '</span>'
617
- + (reqId
618
- ? '<span class="ss-dbg-log-reqid" data-reqid="' + esc(reqId) + '" title="' + esc(reqId) + '">' + esc(shortReqId(reqId)) + '</span>'
619
- : '<span class="ss-dbg-log-reqid-empty">-</span>')
620
- + '<span class="ss-dbg-log-msg">' + esc(msg) + '</span>'
621
- + '</div>';
622
- }
623
-
624
- logBodyEl.innerHTML = html;
727
+ const e = shown[i]
728
+ const level = (e.levelName || e.level_name || 'info').toLowerCase()
729
+ const msg = e.msg || e.message || JSON.stringify(e)
730
+ const ts = e.time || e.timestamp || 0
731
+ const reqId = e.request_id || e['x-request-id'] || ''
732
+
733
+ html +=
734
+ '<div class="ss-dbg-log-entry">' +
735
+ '<span class="ss-dbg-log-level ss-dbg-log-level-' +
736
+ esc(level) +
737
+ '">' +
738
+ esc(level.toUpperCase()) +
739
+ '</span>' +
740
+ '<span class="ss-dbg-log-time">' +
741
+ (ts ? formatTime(ts) : '-') +
742
+ '</span>' +
743
+ (reqId
744
+ ? '<span class="ss-dbg-log-reqid" data-reqid="' +
745
+ esc(reqId) +
746
+ '" title="' +
747
+ esc(reqId) +
748
+ '">' +
749
+ esc(shortReqId(reqId)) +
750
+ '</span>'
751
+ : '<span class="ss-dbg-log-reqid-empty">-</span>') +
752
+ '<span class="ss-dbg-log-msg">' +
753
+ esc(msg) +
754
+ '</span>' +
755
+ '</div>'
756
+ }
757
+
758
+ logBodyEl.innerHTML = html
625
759
 
626
760
  // Click request ID to filter
627
761
  logBodyEl.querySelectorAll('.ss-dbg-log-reqid').forEach((el) => {
628
762
  el.addEventListener('click', () => {
629
- setReqIdFilter(el.getAttribute('data-reqid'));
630
- });
631
- });
632
- };
763
+ setReqIdFilter(el.getAttribute('data-reqid'))
764
+ })
765
+ })
766
+ }
633
767
 
634
768
  logFilters.forEach((btn) => {
635
769
  btn.addEventListener('click', () => {
636
- logFilters.forEach((b) => b.classList.remove('ss-dbg-active'));
637
- btn.classList.add('ss-dbg-active');
638
- logFilter = btn.getAttribute('data-ss-dbg-level');
639
- renderLogs();
640
- });
641
- });
770
+ logFilters.forEach((b) => b.classList.remove('ss-dbg-active'))
771
+ btn.classList.add('ss-dbg-active')
772
+ logFilter = btn.getAttribute('data-ss-dbg-level')
773
+ renderLogs()
774
+ })
775
+ })
642
776
 
643
777
  // ── Emails Tab ─────────────────────────────────────────────────
644
- const emailSearchInput = document.getElementById('ss-dbg-search-emails');
645
- const emailSummaryEl = document.getElementById('ss-dbg-emails-summary');
646
- const emailBodyEl = document.getElementById('ss-dbg-emails-body');
647
- const emailClearBtn = document.getElementById('ss-dbg-emails-clear');
648
- const emailPreviewEl = document.getElementById('ss-dbg-email-preview');
649
- const emailPreviewMeta = document.getElementById('ss-dbg-email-preview-meta');
650
- const emailPreviewClose = document.getElementById('ss-dbg-email-preview-close');
651
- const emailIframe = document.getElementById('ss-dbg-email-iframe');
652
- let cachedEmails = { emails: [], total: 0 };
778
+ const emailSearchInput = document.getElementById('ss-dbg-search-emails')
779
+ const emailSummaryEl = document.getElementById('ss-dbg-emails-summary')
780
+ const emailBodyEl = document.getElementById('ss-dbg-emails-body')
781
+ const emailClearBtn = document.getElementById('ss-dbg-emails-clear')
782
+ const emailPreviewEl = document.getElementById('ss-dbg-email-preview')
783
+ const emailPreviewMeta = document.getElementById('ss-dbg-email-preview-meta')
784
+ const emailPreviewClose = document.getElementById('ss-dbg-email-preview-close')
785
+ const emailIframe = document.getElementById('ss-dbg-email-iframe')
786
+ let cachedEmails = { emails: [], total: 0 }
653
787
 
654
788
  const fetchEmails = () => {
655
789
  fetchJSON(BASE + '/emails')
656
790
  .then((data) => {
657
- cachedEmails = data;
658
- renderEmails();
791
+ cachedEmails = data
792
+ renderEmails()
659
793
  })
660
794
  .catch(() => {
661
- if (emailBodyEl) emailBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load emails</div>';
662
- });
663
- };
795
+ if (emailBodyEl)
796
+ emailBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load emails</div>'
797
+ })
798
+ }
664
799
 
665
800
  const renderEmails = () => {
666
- if (!emailBodyEl) return;
667
- const filter = (emailSearchInput ? emailSearchInput.value : '').toLowerCase();
668
- const emails = cachedEmails.emails || [];
801
+ if (!emailBodyEl) return
802
+ const filter = (emailSearchInput ? emailSearchInput.value : '').toLowerCase()
803
+ const emails = cachedEmails.emails || []
669
804
 
670
805
  if (emailSummaryEl) {
671
- emailSummaryEl.textContent = cachedEmails.total + ' emails';
806
+ emailSummaryEl.textContent = cachedEmails.total + ' emails'
672
807
  }
673
808
 
674
- let filtered = emails;
809
+ let filtered = emails
675
810
  if (filter) {
676
- filtered = emails.filter((e) =>
677
- (e.from || '').toLowerCase().indexOf(filter) !== -1
678
- || (e.to || '').toLowerCase().indexOf(filter) !== -1
679
- || (e.subject || '').toLowerCase().indexOf(filter) !== -1
680
- || (e.mailer || '').toLowerCase().indexOf(filter) !== -1
681
- );
811
+ filtered = emails.filter(
812
+ (e) =>
813
+ (e.from || '').toLowerCase().indexOf(filter) !== -1 ||
814
+ (e.to || '').toLowerCase().indexOf(filter) !== -1 ||
815
+ (e.subject || '').toLowerCase().indexOf(filter) !== -1 ||
816
+ (e.mailer || '').toLowerCase().indexOf(filter) !== -1
817
+ )
682
818
  }
683
819
 
684
820
  if (filtered.length === 0) {
685
- emailBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching emails' : 'No emails captured yet') + '</div>';
686
- return;
687
- }
688
-
689
- let html = '<table class="ss-dbg-table"><thead><tr>'
690
- + '<th style="width:64px">#</th>'
691
- + '<th style="width:160px">From</th>'
692
- + '<th style="width:160px">To</th>'
693
- + '<th>Subject</th>'
694
- + '<th style="width:60px">Status</th>'
695
- + '<th style="width:60px">Mailer</th>'
696
- + '<th style="width:30px" title="Attachments">&#x1F4CE;</th>'
697
- + '<th style="width:70px">Time</th>'
698
- + '</tr></thead><tbody>';
821
+ emailBodyEl.innerHTML =
822
+ '<div class="ss-dbg-empty">' +
823
+ (filter ? 'No matching emails' : 'No emails captured yet') +
824
+ '</div>'
825
+ return
826
+ }
827
+
828
+ let html =
829
+ '<table class="ss-dbg-table"><thead><tr>' +
830
+ '<th style="width:64px">#</th>' +
831
+ '<th style="width:160px">From</th>' +
832
+ '<th style="width:160px">To</th>' +
833
+ '<th>Subject</th>' +
834
+ '<th style="width:60px">Status</th>' +
835
+ '<th style="width:60px">Mailer</th>' +
836
+ '<th style="width:30px" title="Attachments">&#x1F4CE;</th>' +
837
+ '<th style="width:70px">Time</th>' +
838
+ '</tr></thead><tbody>'
699
839
 
700
840
  for (let i = 0; i < filtered.length; i++) {
701
- const e = filtered[i];
702
- html += '<tr class="ss-dbg-email-row" data-email-id="' + e.id + '">'
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>'
707
- + '<td><span class="ss-dbg-email-status ss-dbg-email-status-' + esc(e.status) + '">' + esc(e.status) + '</span></td>'
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>'
710
- + '<td class="ss-dbg-event-time">' + timeAgo(e.timestamp) + '</td>'
711
- + '</tr>';
712
- }
713
-
714
- html += '</tbody></table>';
715
- emailBodyEl.innerHTML = html;
841
+ const e = filtered[i]
842
+ html +=
843
+ '<tr class="ss-dbg-email-row" data-email-id="' +
844
+ e.id +
845
+ '">' +
846
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' +
847
+ e.id +
848
+ deepLink('emails', e.id) +
849
+ '</td>' +
850
+ '<td class="ss-dbg-c-secondary" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' +
851
+ esc(e.from) +
852
+ '">' +
853
+ esc(e.from) +
854
+ '</td>' +
855
+ '<td class="ss-dbg-c-secondary" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' +
856
+ esc(e.to) +
857
+ '">' +
858
+ esc(e.to) +
859
+ '</td>' +
860
+ '<td class="ss-dbg-c-sql" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
861
+ esc(e.subject) +
862
+ '</td>' +
863
+ '<td><span class="ss-dbg-email-status ss-dbg-email-status-' +
864
+ esc(e.status) +
865
+ '">' +
866
+ esc(e.status) +
867
+ '</span></td>' +
868
+ '<td class="ss-dbg-c-muted">' +
869
+ esc(e.mailer) +
870
+ '</td>' +
871
+ '<td class="ss-dbg-c-dim" style="text-align:center">' +
872
+ (e.attachmentCount > 0 ? e.attachmentCount : '-') +
873
+ '</td>' +
874
+ '<td class="ss-dbg-event-time">' +
875
+ timeAgo(e.timestamp) +
876
+ '</td>' +
877
+ '</tr>'
878
+ }
879
+
880
+ html += '</tbody></table>'
881
+ emailBodyEl.innerHTML = html
716
882
 
717
883
  // Click row to open preview
718
884
  emailBodyEl.querySelectorAll('.ss-dbg-email-row').forEach((row) => {
719
885
  row.addEventListener('click', () => {
720
- const id = row.getAttribute('data-email-id');
721
- showEmailPreview(id, filtered);
722
- });
723
- });
724
- };
886
+ const id = row.getAttribute('data-email-id')
887
+ showEmailPreview(id, filtered)
888
+ })
889
+ })
890
+ }
725
891
 
726
892
  const showEmailPreview = (id, emails) => {
727
- if (!emailPreviewEl || !emailIframe || !emailPreviewMeta) return;
728
- const email = emails.find((e) => String(e.id) === String(id));
893
+ if (!emailPreviewEl || !emailIframe || !emailPreviewMeta) return
894
+ const email = emails.find((e) => String(e.id) === String(id))
729
895
 
730
896
  if (emailPreviewMeta && email) {
731
897
  emailPreviewMeta.innerHTML =
732
- '<strong>Subject:</strong> ' + esc(email.subject)
733
- + '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>From:</strong> ' + esc(email.from)
734
- + '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>To:</strong> ' + esc(email.to)
735
- + (email.cc ? '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>CC:</strong> ' + esc(email.cc) : '')
736
- + '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>Status:</strong> <span class="ss-dbg-email-status ss-dbg-email-status-' + esc(email.status) + '">' + esc(email.status) + '</span>'
737
- + '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>Mailer:</strong> ' + esc(email.mailer);
738
- }
739
-
740
- emailIframe.src = BASE + '/emails/' + id + '/preview';
741
- emailPreviewEl.style.display = 'flex';
742
- };
898
+ '<strong>Subject:</strong> ' +
899
+ esc(email.subject) +
900
+ '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>From:</strong> ' +
901
+ esc(email.from) +
902
+ '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>To:</strong> ' +
903
+ esc(email.to) +
904
+ (email.cc ? '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>CC:</strong> ' + esc(email.cc) : '') +
905
+ '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>Status:</strong> <span class="ss-dbg-email-status ss-dbg-email-status-' +
906
+ esc(email.status) +
907
+ '">' +
908
+ esc(email.status) +
909
+ '</span>' +
910
+ '&nbsp;&nbsp;|&nbsp;&nbsp;<strong>Mailer:</strong> ' +
911
+ esc(email.mailer)
912
+ }
913
+
914
+ emailIframe.src = BASE + '/emails/' + id + '/preview'
915
+ emailPreviewEl.style.display = 'flex'
916
+ }
743
917
 
744
918
  if (emailPreviewClose) {
745
919
  emailPreviewClose.addEventListener('click', () => {
746
- if (emailPreviewEl) emailPreviewEl.style.display = 'none';
747
- if (emailIframe) emailIframe.src = 'about:blank';
748
- });
920
+ if (emailPreviewEl) emailPreviewEl.style.display = 'none'
921
+ if (emailIframe) emailIframe.src = 'about:blank'
922
+ })
749
923
  }
750
924
 
751
- if (emailSearchInput) emailSearchInput.addEventListener('input', renderEmails);
925
+ if (emailSearchInput) emailSearchInput.addEventListener('input', renderEmails)
752
926
  if (emailClearBtn) {
753
927
  emailClearBtn.addEventListener('click', () => {
754
- cachedEmails = { emails: [], total: 0 };
755
- renderEmails();
756
- });
928
+ cachedEmails = { emails: [], total: 0 }
929
+ renderEmails()
930
+ })
757
931
  }
758
932
 
759
933
  // ── Timeline Tab ────────────────────────────────────────────────
760
- const tlSearchInput = document.getElementById('ss-dbg-search-timeline');
761
- const tlSummaryEl = document.getElementById('ss-dbg-timeline-summary');
762
- const tlBodyEl = document.getElementById('ss-dbg-timeline-body');
763
- const tlListEl = document.getElementById('ss-dbg-timeline-list');
764
- const tlDetailEl = document.getElementById('ss-dbg-timeline-detail');
765
- const tlBackBtn = document.getElementById('ss-dbg-tl-back');
766
- const tlDetailTitle = document.getElementById('ss-dbg-tl-detail-title');
767
- const tlWaterfall = document.getElementById('ss-dbg-tl-waterfall');
768
- let cachedTraces = { traces: [], total: 0 };
934
+ const tlSearchInput = document.getElementById('ss-dbg-search-timeline')
935
+ const tlSummaryEl = document.getElementById('ss-dbg-timeline-summary')
936
+ const tlBodyEl = document.getElementById('ss-dbg-timeline-body')
937
+ const tlListEl = document.getElementById('ss-dbg-timeline-list')
938
+ const tlDetailEl = document.getElementById('ss-dbg-timeline-detail')
939
+ const tlBackBtn = document.getElementById('ss-dbg-tl-back')
940
+ const tlDetailTitle = document.getElementById('ss-dbg-tl-detail-title')
941
+ const tlWaterfall = document.getElementById('ss-dbg-tl-waterfall')
942
+ let cachedTraces = { traces: [], total: 0 }
769
943
 
770
944
  const statusClass = (code) => {
771
- if (code >= 500) return 'ss-dbg-status-5xx';
772
- if (code >= 400) return 'ss-dbg-status-4xx';
773
- if (code >= 300) return 'ss-dbg-status-3xx';
774
- return 'ss-dbg-status-2xx';
775
- };
945
+ if (code >= 500) return 'ss-dbg-status-5xx'
946
+ if (code >= 400) return 'ss-dbg-status-4xx'
947
+ if (code >= 300) return 'ss-dbg-status-3xx'
948
+ return 'ss-dbg-status-2xx'
949
+ }
776
950
 
777
951
  const fetchTraces = () => {
778
- if (!tracingEnabled) return;
952
+ if (!tracingEnabled) return
779
953
  fetchJSON(BASE + '/traces')
780
954
  .then((data) => {
781
- cachedTraces = data;
782
- renderTraces();
955
+ cachedTraces = data
956
+ renderTraces()
783
957
  })
784
958
  .catch(() => {
785
- if (tlBodyEl) tlBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load traces</div>';
786
- });
787
- };
959
+ if (tlBodyEl) tlBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load traces</div>'
960
+ })
961
+ }
788
962
 
789
963
  const renderTraces = () => {
790
- if (!tlBodyEl) return;
791
- const filter = (tlSearchInput ? tlSearchInput.value : '').toLowerCase();
792
- const traces = cachedTraces.traces || [];
964
+ if (!tlBodyEl) return
965
+ const filter = (tlSearchInput ? tlSearchInput.value : '').toLowerCase()
966
+ const traces = cachedTraces.traces || []
793
967
 
794
968
  if (tlSummaryEl) {
795
- tlSummaryEl.textContent = cachedTraces.total + ' requests';
969
+ tlSummaryEl.textContent = cachedTraces.total + ' requests'
796
970
  }
797
971
 
798
- let filtered = traces;
972
+ let filtered = traces
799
973
  if (filter) {
800
- filtered = traces.filter((t) =>
801
- t.url.toLowerCase().indexOf(filter) !== -1
802
- || t.method.toLowerCase().indexOf(filter) !== -1
803
- );
974
+ filtered = traces.filter(
975
+ (t) =>
976
+ t.url.toLowerCase().indexOf(filter) !== -1 ||
977
+ t.method.toLowerCase().indexOf(filter) !== -1
978
+ )
804
979
  }
805
980
 
806
981
  if (filtered.length === 0) {
807
- tlBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching requests' : 'No requests traced yet') + '</div>';
808
- return;
809
- }
810
-
811
- let html = '<table class="ss-dbg-table"><thead><tr>'
812
- + '<th style="width:64px">#</th>'
813
- + '<th style="width:60px">Method</th>'
814
- + '<th>URL</th>'
815
- + '<th style="width:55px">Status</th>'
816
- + '<th style="width:70px">Duration</th>'
817
- + '<th style="width:50px">Spans</th>'
818
- + '<th style="width:30px" title="Warnings">&#x26A0;</th>'
819
- + '<th style="width:70px">Time</th>'
820
- + '</tr></thead><tbody>';
982
+ tlBodyEl.innerHTML =
983
+ '<div class="ss-dbg-empty">' +
984
+ (filter ? 'No matching requests' : 'No requests traced yet') +
985
+ '</div>'
986
+ return
987
+ }
988
+
989
+ let html =
990
+ '<table class="ss-dbg-table"><thead><tr>' +
991
+ '<th style="width:64px">#</th>' +
992
+ '<th style="width:60px">Method</th>' +
993
+ '<th>URL</th>' +
994
+ '<th style="width:55px">Status</th>' +
995
+ '<th style="width:70px">Duration</th>' +
996
+ '<th style="width:50px">Spans</th>' +
997
+ '<th style="width:30px" title="Warnings">&#x26A0;</th>' +
998
+ '<th style="width:70px">Time</th>' +
999
+ '</tr></thead><tbody>'
821
1000
 
822
1001
  for (let i = 0; i < filtered.length; i++) {
823
- const t = filtered[i];
824
- html += '<tr class="ss-dbg-email-row" data-trace-id="' + t.id + '">'
825
- + '<td class="ss-dbg-c-dim" style="white-space:nowrap">' + t.id + deepLink('traces', t.id) + '</td>'
826
- + '<td><span class="' + methodClass(t.method) + '">' + esc(t.method) + '</span></td>'
827
- + '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px" title="' + esc(t.url) + '">' + esc(t.url) + '</td>'
828
- + '<td><span class="ss-dbg-status ' + statusClass(t.statusCode) + '">' + t.statusCode + '</span></td>'
829
- + '<td class="ss-dbg-duration ' + durationClass(t.totalDuration) + '">' + t.totalDuration.toFixed(1) + 'ms</td>'
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>'
832
- + '<td class="ss-dbg-event-time">' + timeAgo(t.timestamp) + '</td>'
833
- + '</tr>';
834
- }
835
-
836
- html += '</tbody></table>';
837
- tlBodyEl.innerHTML = html;
1002
+ const t = filtered[i]
1003
+ html +=
1004
+ '<tr class="ss-dbg-email-row" data-trace-id="' +
1005
+ t.id +
1006
+ '">' +
1007
+ '<td class="ss-dbg-c-dim" style="white-space:nowrap">' +
1008
+ t.id +
1009
+ deepLink('traces', t.id) +
1010
+ '</td>' +
1011
+ '<td><span class="' +
1012
+ methodClass(t.method) +
1013
+ '">' +
1014
+ esc(t.method) +
1015
+ '</span></td>' +
1016
+ '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px" title="' +
1017
+ esc(t.url) +
1018
+ '">' +
1019
+ esc(t.url) +
1020
+ '</td>' +
1021
+ '<td><span class="ss-dbg-status ' +
1022
+ statusClass(t.statusCode) +
1023
+ '">' +
1024
+ t.statusCode +
1025
+ '</span></td>' +
1026
+ '<td class="ss-dbg-duration ' +
1027
+ durationClass(t.totalDuration) +
1028
+ '">' +
1029
+ t.totalDuration.toFixed(1) +
1030
+ 'ms</td>' +
1031
+ '<td class="ss-dbg-c-muted" style="text-align:center">' +
1032
+ t.spanCount +
1033
+ '</td>' +
1034
+ '<td style="text-align:center">' +
1035
+ (t.warningCount > 0
1036
+ ? '<span class="ss-dbg-c-amber">' + t.warningCount + '</span>'
1037
+ : '<span class="ss-dbg-c-border">-</span>') +
1038
+ '</td>' +
1039
+ '<td class="ss-dbg-event-time">' +
1040
+ timeAgo(t.timestamp) +
1041
+ '</td>' +
1042
+ '</tr>'
1043
+ }
1044
+
1045
+ html += '</tbody></table>'
1046
+ tlBodyEl.innerHTML = html
838
1047
 
839
1048
  // Click row to open detail
840
1049
  tlBodyEl.querySelectorAll('[data-trace-id]').forEach((row) => {
841
1050
  row.addEventListener('click', () => {
842
- const id = row.getAttribute('data-trace-id');
843
- fetchTraceDetail(id);
844
- });
845
- });
846
- };
1051
+ const id = row.getAttribute('data-trace-id')
1052
+ fetchTraceDetail(id)
1053
+ })
1054
+ })
1055
+ }
847
1056
 
848
1057
  const fetchTraceDetail = (id) => {
849
1058
  fetchJSON(BASE + '/traces/' + id)
850
1059
  .then((trace) => {
851
- showTimeline(trace);
1060
+ showTimeline(trace)
852
1061
  })
853
1062
  .catch(() => {
854
- if (tlWaterfall) tlWaterfall.innerHTML = '<div class="ss-dbg-empty">Failed to load trace</div>';
855
- });
856
- };
1063
+ if (tlWaterfall)
1064
+ tlWaterfall.innerHTML = '<div class="ss-dbg-empty">Failed to load trace</div>'
1065
+ })
1066
+ }
857
1067
 
858
1068
  const showTimeline = (trace) => {
859
- if (!tlListEl || !tlDetailEl || !tlDetailTitle || !tlWaterfall) return;
1069
+ if (!tlListEl || !tlDetailEl || !tlDetailTitle || !tlWaterfall) return
860
1070
 
861
- tlListEl.style.display = 'none';
862
- tlDetailEl.style.display = '';
1071
+ tlListEl.style.display = 'none'
1072
+ tlDetailEl.style.display = ''
863
1073
 
864
1074
  tlDetailTitle.innerHTML =
865
- '<span class="' + methodClass(trace.method) + '">' + esc(trace.method) + '</span> '
866
- + esc(trace.url) + ' '
867
- + '<span class="ss-dbg-status ' + statusClass(trace.statusCode) + '">' + trace.statusCode + '</span>'
868
- + '<span class="ss-dbg-tl-meta">' + trace.totalDuration.toFixed(1) + 'ms &middot; '
869
- + trace.spanCount + ' spans &middot; '
870
- + formatTime(trace.timestamp) + '</span>';
871
-
872
- const spans = trace.spans || [];
873
- const total = trace.totalDuration || 1;
1075
+ '<span class="' +
1076
+ methodClass(trace.method) +
1077
+ '">' +
1078
+ esc(trace.method) +
1079
+ '</span> ' +
1080
+ esc(trace.url) +
1081
+ ' ' +
1082
+ '<span class="ss-dbg-status ' +
1083
+ statusClass(trace.statusCode) +
1084
+ '">' +
1085
+ trace.statusCode +
1086
+ '</span>' +
1087
+ '<span class="ss-dbg-tl-meta">' +
1088
+ trace.totalDuration.toFixed(1) +
1089
+ 'ms &middot; ' +
1090
+ trace.spanCount +
1091
+ ' spans &middot; ' +
1092
+ formatTime(trace.timestamp) +
1093
+ '</span>'
1094
+
1095
+ const spans = trace.spans || []
1096
+ const total = trace.totalDuration || 1
874
1097
 
875
1098
  // Legend
876
- let html = '<div class="ss-dbg-tl-legend">'
877
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#6d28d9"></span>DB</div>'
878
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#1e3a5f"></span>Request</div>'
879
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#059669"></span>Mail</div>'
880
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#b45309"></span>Event</div>'
881
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#0e7490"></span>View</div>'
882
- + '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#525252"></span>Custom</div>'
883
- + '</div>';
1099
+ let html =
1100
+ '<div class="ss-dbg-tl-legend">' +
1101
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#6d28d9"></span>DB</div>' +
1102
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#1e3a5f"></span>Request</div>' +
1103
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#059669"></span>Mail</div>' +
1104
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#b45309"></span>Event</div>' +
1105
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#0e7490"></span>View</div>' +
1106
+ '<div class="ss-dbg-tl-legend-item"><span class="ss-dbg-tl-legend-dot" style="background:#525252"></span>Custom</div>' +
1107
+ '</div>'
884
1108
 
885
1109
  if (spans.length === 0) {
886
- html += '<div class="ss-dbg-empty">No spans captured for this request</div>';
1110
+ html += '<div class="ss-dbg-empty">No spans captured for this request</div>'
887
1111
  } else {
888
1112
  // Build nesting depth from parentId
889
- const depthMap = {};
1113
+ const depthMap = {}
890
1114
  for (let i = 0; i < spans.length; i++) {
891
- const s = spans[i];
1115
+ const s = spans[i]
892
1116
  if (!s.parentId) {
893
- depthMap[s.id] = 0;
1117
+ depthMap[s.id] = 0
894
1118
  } else {
895
- depthMap[s.id] = (depthMap[s.parentId] || 0) + 1;
1119
+ depthMap[s.id] = (depthMap[s.parentId] || 0) + 1
896
1120
  }
897
1121
  }
898
1122
 
899
1123
  // Sort by startOffset
900
- const sorted = spans.slice().sort((a, b) => a.startOffset - b.startOffset);
1124
+ const sorted = spans.slice().sort((a, b) => a.startOffset - b.startOffset)
901
1125
 
902
1126
  for (let i = 0; i < sorted.length; i++) {
903
- const s = sorted[i];
904
- const depth = depthMap[s.id] || 0;
905
- const leftPct = (s.startOffset / total * 100).toFixed(2);
906
- const widthPct = Math.max(s.duration / total * 100, 0.5).toFixed(2);
907
- const indent = depth * 16;
908
- const catLabel = s.category === 'db' ? 'DB' : s.category;
909
- const metaStr = s.metadata ? Object.entries(s.metadata).filter(([,v]) => v != null).map(([k,v]) => k + '=' + v).join(', ') : '';
910
- const tooltip = s.label + ' (' + s.duration.toFixed(2) + 'ms)' + (metaStr ? '\n' + metaStr : '');
911
-
912
- html += '<div class="ss-dbg-tl-row">'
913
- + '<div class="ss-dbg-tl-label" style="padding-left:' + (8 + indent) + 'px" title="' + esc(tooltip) + '">'
914
- + '<span class="ss-dbg-badge ss-dbg-badge-' + (s.category === 'db' ? 'purple' : s.category === 'mail' ? 'green' : s.category === 'event' ? 'amber' : s.category === 'view' ? 'blue' : 'muted') + '" style="font-size:9px;margin-right:4px">' + esc(catLabel) + '</span>'
915
- + esc(s.label.length > 40 ? s.label.slice(0, 40) + '...' : s.label)
916
- + '</div>'
917
- + '<div class="ss-dbg-tl-track">'
918
- + '<div class="ss-dbg-tl-bar ss-dbg-tl-bar-' + esc(s.category) + '" style="left:' + leftPct + '%;width:' + widthPct + '%" title="' + esc(tooltip) + '"></div>'
919
- + '</div>'
920
- + '<span class="ss-dbg-tl-dur">' + s.duration.toFixed(2) + 'ms</span>'
921
- + '</div>';
1127
+ const s = sorted[i]
1128
+ const depth = depthMap[s.id] || 0
1129
+ const leftPct = ((s.startOffset / total) * 100).toFixed(2)
1130
+ const widthPct = Math.max((s.duration / total) * 100, 0.5).toFixed(2)
1131
+ const indent = depth * 16
1132
+ const catLabel = s.category === 'db' ? 'DB' : s.category
1133
+ const metaStr = s.metadata
1134
+ ? Object.entries(s.metadata)
1135
+ .filter(([, v]) => v != null)
1136
+ .map(([k, v]) => k + '=' + v)
1137
+ .join(', ')
1138
+ : ''
1139
+ const tooltip =
1140
+ s.label + ' (' + s.duration.toFixed(2) + 'ms)' + (metaStr ? '\n' + metaStr : '')
1141
+
1142
+ html +=
1143
+ '<div class="ss-dbg-tl-row">' +
1144
+ '<div class="ss-dbg-tl-label" style="padding-left:' +
1145
+ (8 + indent) +
1146
+ 'px" title="' +
1147
+ esc(tooltip) +
1148
+ '">' +
1149
+ '<span class="ss-dbg-badge ss-dbg-badge-' +
1150
+ (s.category === 'db'
1151
+ ? 'purple'
1152
+ : s.category === 'mail'
1153
+ ? 'green'
1154
+ : s.category === 'event'
1155
+ ? 'amber'
1156
+ : s.category === 'view'
1157
+ ? 'blue'
1158
+ : 'muted') +
1159
+ '" style="font-size:9px;margin-right:4px">' +
1160
+ esc(catLabel) +
1161
+ '</span>' +
1162
+ esc(s.label.length > 40 ? s.label.slice(0, 40) + '...' : s.label) +
1163
+ '</div>' +
1164
+ '<div class="ss-dbg-tl-track">' +
1165
+ '<div class="ss-dbg-tl-bar ss-dbg-tl-bar-' +
1166
+ esc(s.category) +
1167
+ '" style="left:' +
1168
+ leftPct +
1169
+ '%;width:' +
1170
+ widthPct +
1171
+ '%" title="' +
1172
+ esc(tooltip) +
1173
+ '"></div>' +
1174
+ '</div>' +
1175
+ '<span class="ss-dbg-tl-dur">' +
1176
+ s.duration.toFixed(2) +
1177
+ 'ms</span>' +
1178
+ '</div>'
922
1179
  }
923
1180
  }
924
1181
 
925
1182
  // Warnings
926
1183
  if (trace.warnings && trace.warnings.length > 0) {
927
- html += '<div class="ss-dbg-tl-warnings">'
928
- + '<div class="ss-dbg-tl-warnings-title">Warnings (' + trace.warnings.length + ')</div>';
1184
+ html +=
1185
+ '<div class="ss-dbg-tl-warnings">' +
1186
+ '<div class="ss-dbg-tl-warnings-title">Warnings (' +
1187
+ trace.warnings.length +
1188
+ ')</div>'
929
1189
  for (let w = 0; w < trace.warnings.length; w++) {
930
- html += '<div class="ss-dbg-tl-warning">' + esc(trace.warnings[w]) + '</div>';
1190
+ html += '<div class="ss-dbg-tl-warning">' + esc(trace.warnings[w]) + '</div>'
931
1191
  }
932
- html += '</div>';
1192
+ html += '</div>'
933
1193
  }
934
1194
 
935
- tlWaterfall.innerHTML = html;
936
- };
1195
+ tlWaterfall.innerHTML = html
1196
+ }
937
1197
 
938
1198
  if (tlBackBtn) {
939
1199
  tlBackBtn.addEventListener('click', () => {
940
- if (tlListEl) tlListEl.style.display = '';
941
- if (tlDetailEl) tlDetailEl.style.display = 'none';
942
- });
1200
+ if (tlListEl) tlListEl.style.display = ''
1201
+ if (tlDetailEl) tlDetailEl.style.display = 'none'
1202
+ })
943
1203
  }
944
1204
 
945
- if (tlSearchInput) tlSearchInput.addEventListener('input', renderTraces);
1205
+ if (tlSearchInput) tlSearchInput.addEventListener('input', renderTraces)
946
1206
 
947
1207
  // ── Mini Stats Bar ─────────────────────────────────────────────
948
- const miniStatsEl = document.getElementById('ss-dbg-mini-stats');
949
- let miniStatsTimer = null;
1208
+ const miniStatsEl = document.getElementById('ss-dbg-mini-stats')
1209
+ let miniStatsTimer = null
950
1210
 
951
1211
  const fetchMiniStats = () => {
952
- if (!DASH_API || !miniStatsEl) return;
1212
+ if (!DASH_API || !miniStatsEl) return
953
1213
  fetchJSON(DASH_API + '/overview?range=1h')
954
1214
  .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;
1215
+ const avg = data.avgResponseTime || 0
1216
+ const err = data.errorRate || 0
1217
+ const rpm = data.requestsPerMinute || 0
1218
+ const hasData = (data.totalRequests || 0) > 0
959
1219
 
960
1220
  if (!hasData) {
961
- miniStatsEl.innerHTML = '';
962
- return;
1221
+ miniStatsEl.innerHTML = ''
1222
+ return
963
1223
  }
964
1224
 
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';
1225
+ const avgClass =
1226
+ avg > 500 ? 'ss-dbg-stat-red' : avg > 200 ? 'ss-dbg-stat-amber' : 'ss-dbg-stat-green'
1227
+ const errClass =
1228
+ err > 5 ? 'ss-dbg-stat-red' : err > 1 ? 'ss-dbg-stat-amber' : 'ss-dbg-stat-green'
967
1229
 
968
1230
  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>';
1231
+ '<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value ' +
1232
+ avgClass +
1233
+ '">' +
1234
+ avg.toFixed(1) +
1235
+ 'ms</span> avg</span>' +
1236
+ '<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value ' +
1237
+ errClass +
1238
+ '">' +
1239
+ err.toFixed(1) +
1240
+ '%</span> err</span>' +
1241
+ '<span class="ss-dbg-mini-stat"><span class="ss-dbg-mini-stat-value">' +
1242
+ Math.round(rpm) +
1243
+ '</span> req/m</span>'
972
1244
  })
973
1245
  .catch(() => {
974
- miniStatsEl.innerHTML = '';
975
- });
976
- };
1246
+ miniStatsEl.innerHTML = ''
1247
+ })
1248
+ }
977
1249
 
978
1250
  // ── 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: [] };
1251
+ const cacheSearchInput = document.getElementById('ss-dbg-search-cache')
1252
+ const cacheSummaryEl = document.getElementById('ss-dbg-cache-summary')
1253
+ const cacheBodyEl = document.getElementById('ss-dbg-cache-body')
1254
+ const cacheStatsArea = document.getElementById('ss-dbg-cache-stats-area')
1255
+ let cachedCacheData = { stats: {}, keys: [] }
984
1256
 
985
1257
  const fetchCache = () => {
986
- if (!DASH_API) return;
1258
+ if (!DASH_API) return
987
1259
  fetchJSON(DASH_API + '/cache')
988
1260
  .then((data) => {
989
- cachedCacheData = data;
990
- renderCache();
1261
+ cachedCacheData = data
1262
+ renderCache()
991
1263
  })
992
1264
  .catch(() => {
993
- if (cacheBodyEl) cacheBodyEl.innerHTML = '<div class="ss-dbg-empty">Cache not available</div>';
994
- if (cacheStatsArea) cacheStatsArea.innerHTML = '';
995
- });
996
- };
1265
+ if (cacheBodyEl)
1266
+ cacheBodyEl.innerHTML = '<div class="ss-dbg-empty">Cache not available</div>'
1267
+ if (cacheStatsArea) cacheStatsArea.innerHTML = ''
1268
+ })
1269
+ }
997
1270
 
998
1271
  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();
1272
+ if (!cacheBodyEl) return
1273
+ const stats = cachedCacheData.stats || {}
1274
+ const keys = cachedCacheData.keys || cachedCacheData.data || []
1275
+ const filter = (cacheSearchInput ? cacheSearchInput.value : '').toLowerCase()
1003
1276
 
1004
1277
  // Stats area
1005
1278
  if (cacheStatsArea) {
1006
1279
  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>';
1280
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Hit Rate:</span><span class="ss-dbg-cache-stat-value">' +
1281
+ (stats.hitRate || 0).toFixed(1) +
1282
+ '%</span></div>' +
1283
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Hits:</span><span class="ss-dbg-cache-stat-value">' +
1284
+ (stats.hits || 0) +
1285
+ '</span></div>' +
1286
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Misses:</span><span class="ss-dbg-cache-stat-value">' +
1287
+ (stats.misses || 0) +
1288
+ '</span></div>' +
1289
+ '<div class="ss-dbg-cache-stat"><span class="ss-dbg-cache-stat-label">Keys:</span><span class="ss-dbg-cache-stat-value">' +
1290
+ (stats.keyCount || keys.length || 0) +
1291
+ '</span></div>'
1011
1292
  }
1012
1293
 
1013
1294
  if (cacheSummaryEl) {
1014
- cacheSummaryEl.textContent = (stats.keyCount || keys.length || 0) + ' keys';
1295
+ cacheSummaryEl.textContent = (stats.keyCount || keys.length || 0) + ' keys'
1015
1296
  }
1016
1297
 
1017
- let filtered = keys;
1298
+ let filtered = keys
1018
1299
  if (filter) {
1019
- filtered = keys.filter((k) => (k.key || '').toLowerCase().indexOf(filter) !== -1);
1300
+ filtered = keys.filter((k) => (k.key || '').toLowerCase().indexOf(filter) !== -1)
1020
1301
  }
1021
1302
 
1022
1303
  if (filtered.length === 0) {
1023
- cacheBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching cache keys' : 'No cache keys found') + '</div>';
1024
- return;
1304
+ cacheBodyEl.innerHTML =
1305
+ '<div class="ss-dbg-empty">' +
1306
+ (filter ? 'No matching cache keys' : 'No cache keys found') +
1307
+ '</div>'
1308
+ return
1025
1309
  }
1026
1310
 
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>';
1311
+ let html =
1312
+ '<table class="ss-dbg-table"><thead><tr>' +
1313
+ '<th>Key</th>' +
1314
+ '<th style="width:80px">Type</th>' +
1315
+ '<th style="width:80px">TTL</th>' +
1316
+ '<th style="width:80px">Size</th>' +
1317
+ '</tr></thead><tbody>'
1033
1318
 
1034
1319
  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;
1320
+ const k = filtered[i]
1321
+ html +=
1322
+ '<tr class="ss-dbg-email-row" data-cache-key="' +
1323
+ esc(k.key || '') +
1324
+ '">' +
1325
+ '<td class="ss-dbg-c-sql">' +
1326
+ esc(k.key || '') +
1327
+ '</td>' +
1328
+ '<td class="ss-dbg-c-muted">' +
1329
+ esc(k.type || '-') +
1330
+ '</td>' +
1331
+ '<td class="ss-dbg-c-muted">' +
1332
+ (k.ttl != null ? k.ttl + 's' : '-') +
1333
+ '</td>' +
1334
+ '<td class="ss-dbg-c-dim">' +
1335
+ (k.size != null ? k.size + 'B' : '-') +
1336
+ '</td>' +
1337
+ '</tr>'
1338
+ }
1339
+
1340
+ html += '</tbody></table>'
1341
+ cacheBodyEl.innerHTML = html
1046
1342
 
1047
1343
  // Click row to show cache detail
1048
1344
  cacheBodyEl.querySelectorAll('[data-cache-key]').forEach((row) => {
1049
1345
  row.addEventListener('click', () => {
1050
- const key = row.getAttribute('data-cache-key');
1346
+ const key = row.getAttribute('data-cache-key')
1051
1347
  fetchJSON(DASH_API + '/cache/' + encodeURIComponent(key))
1052
1348
  .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">&larr; Back</button>'
1055
- + '&nbsp;&nbsp;<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());
1349
+ cacheBodyEl.innerHTML =
1350
+ '<div class="ss-dbg-cache-detail">' +
1351
+ '<button type="button" class="ss-dbg-btn-clear" id="ss-dbg-cache-back">&larr; Back</button>' +
1352
+ '&nbsp;&nbsp;<strong>' +
1353
+ esc(key) +
1354
+ '</strong>' +
1355
+ '<pre>' +
1356
+ esc(JSON.stringify(data.value || data, null, 2)) +
1357
+ '</pre>' +
1358
+ '</div>'
1359
+ const backBtn = document.getElementById('ss-dbg-cache-back')
1360
+ if (backBtn) backBtn.addEventListener('click', () => renderCache())
1060
1361
  })
1061
- .catch(() => { /* ignore */ });
1062
- });
1063
- });
1064
- };
1362
+ .catch(() => {
1363
+ /* ignore */
1364
+ })
1365
+ })
1366
+ })
1367
+ }
1065
1368
 
1066
- if (cacheSearchInput) cacheSearchInput.addEventListener('input', renderCache);
1369
+ if (cacheSearchInput) cacheSearchInput.addEventListener('input', renderCache)
1067
1370
 
1068
1371
  // ── 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: {} };
1372
+ const jobsBodyEl = document.getElementById('ss-dbg-jobs-body')
1373
+ const jobsSummaryEl = document.getElementById('ss-dbg-jobs-summary')
1374
+ const jobsStatsArea = document.getElementById('ss-dbg-jobs-stats-area')
1375
+ const jobFilters = panel.querySelectorAll('[data-ss-dbg-job-status]')
1376
+ let jobStatusFilter = 'all'
1377
+ let cachedJobsData = { data: [], stats: {} }
1075
1378
 
1076
1379
  const fetchJobs = () => {
1077
- if (!DASH_API) return;
1078
- let url = DASH_API + '/jobs?limit=100';
1079
- if (jobStatusFilter && jobStatusFilter !== 'all') url += '&status=' + jobStatusFilter;
1380
+ if (!DASH_API) return
1381
+ let url = DASH_API + '/jobs?limit=100'
1382
+ if (jobStatusFilter && jobStatusFilter !== 'all') url += '&status=' + jobStatusFilter
1080
1383
 
1081
1384
  fetchJSON(url)
1082
1385
  .then((data) => {
1083
- cachedJobsData = data;
1084
- renderJobs();
1386
+ cachedJobsData = data
1387
+ renderJobs()
1085
1388
  })
1086
1389
  .catch(() => {
1087
- if (jobsBodyEl) jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">Jobs/Queue not available</div>';
1088
- if (jobsStatsArea) jobsStatsArea.innerHTML = '';
1089
- });
1090
- };
1390
+ if (jobsBodyEl)
1391
+ jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">Jobs/Queue not available</div>'
1392
+ if (jobsStatsArea) jobsStatsArea.innerHTML = ''
1393
+ })
1394
+ }
1091
1395
 
1092
1396
  const renderJobs = () => {
1093
- if (!jobsBodyEl) return;
1094
- const items = cachedJobsData.data || cachedJobsData.jobs || [];
1095
- const stats = cachedJobsData.stats || {};
1397
+ if (!jobsBodyEl) return
1398
+ const items = cachedJobsData.data || cachedJobsData.jobs || []
1399
+ const stats = cachedJobsData.stats || {}
1096
1400
 
1097
1401
  // Stats area
1098
1402
  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>';
1403
+ jobsStatsArea.innerHTML =
1404
+ '<div class="ss-dbg-job-stats">' +
1405
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Active:</span><span class="ss-dbg-job-stat-value">' +
1406
+ (stats.active || 0) +
1407
+ '</span></div>' +
1408
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Waiting:</span><span class="ss-dbg-job-stat-value">' +
1409
+ (stats.waiting || 0) +
1410
+ '</span></div>' +
1411
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Delayed:</span><span class="ss-dbg-job-stat-value">' +
1412
+ (stats.delayed || 0) +
1413
+ '</span></div>' +
1414
+ '<div class="ss-dbg-job-stat"><span class="ss-dbg-job-stat-label">Completed:</span><span class="ss-dbg-job-stat-value">' +
1415
+ (stats.completed || 0) +
1416
+ '</span></div>' +
1417
+ '<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">' +
1418
+ (stats.failed || 0) +
1419
+ '</span></div>' +
1420
+ '</div>'
1106
1421
  }
1107
1422
 
1108
1423
  if (jobsSummaryEl) {
1109
- const total = (cachedJobsData.meta ? cachedJobsData.meta.total : null) || items.length;
1110
- jobsSummaryEl.textContent = total + ' jobs';
1424
+ const total = (cachedJobsData.meta ? cachedJobsData.meta.total : null) || items.length
1425
+ jobsSummaryEl.textContent = total + ' jobs'
1111
1426
  }
1112
1427
 
1113
1428
  if (items.length === 0) {
1114
- jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">No jobs found</div>';
1115
- return;
1429
+ jobsBodyEl.innerHTML = '<div class="ss-dbg-empty">No jobs found</div>'
1430
+ return
1116
1431
  }
1117
1432
 
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>';
1433
+ let html =
1434
+ '<table class="ss-dbg-table"><thead><tr>' +
1435
+ '<th style="width:50px">ID</th>' +
1436
+ '<th>Name</th>' +
1437
+ '<th style="width:80px">Status</th>' +
1438
+ '<th style="width:60px">Attempts</th>' +
1439
+ '<th style="width:80px">Duration</th>' +
1440
+ '<th style="width:70px">Time</th>' +
1441
+ '<th style="width:50px"></th>' +
1442
+ '</tr></thead><tbody>'
1127
1443
 
1128
1444
  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;
1445
+ const j = items[i]
1446
+ const statusBadge =
1447
+ j.status === 'failed'
1448
+ ? 'red'
1449
+ : j.status === 'completed'
1450
+ ? 'green'
1451
+ : j.status === 'active'
1452
+ ? 'blue'
1453
+ : 'amber'
1454
+ html +=
1455
+ '<tr>' +
1456
+ '<td class="ss-dbg-c-dim">' +
1457
+ j.id +
1458
+ '</td>' +
1459
+ '<td class="ss-dbg-c-sql">' +
1460
+ esc(j.name || '') +
1461
+ '</td>' +
1462
+ '<td><span class="ss-dbg-badge ss-dbg-badge-' +
1463
+ statusBadge +
1464
+ '">' +
1465
+ esc(j.status || '') +
1466
+ '</span></td>' +
1467
+ '<td class="ss-dbg-c-muted" style="text-align:center">' +
1468
+ (j.attempts || j.attemptsMade || 0) +
1469
+ '</td>' +
1470
+ '<td class="ss-dbg-duration">' +
1471
+ (j.duration != null ? j.duration.toFixed(0) + 'ms' : '-') +
1472
+ '</td>' +
1473
+ '<td class="ss-dbg-event-time">' +
1474
+ timeAgo(j.timestamp || j.processedOn || j.created_at) +
1475
+ '</td>' +
1476
+ '<td>' +
1477
+ (j.status === 'failed'
1478
+ ? '<button class="ss-dbg-retry-btn" data-retry-id="' + j.id + '">Retry</button>'
1479
+ : '') +
1480
+ '</td>' +
1481
+ '</tr>'
1482
+ }
1483
+
1484
+ html += '</tbody></table>'
1485
+ jobsBodyEl.innerHTML = html
1144
1486
 
1145
1487
  // Retry buttons
1146
1488
  jobsBodyEl.querySelectorAll('.ss-dbg-retry-btn').forEach((btn) => {
1147
1489
  btn.addEventListener('click', (e) => {
1148
- e.stopPropagation();
1149
- const id = btn.getAttribute('data-retry-id');
1150
- btn.textContent = '...';
1151
- btn.disabled = true;
1490
+ e.stopPropagation()
1491
+ const id = btn.getAttribute('data-retry-id')
1492
+ btn.textContent = '...'
1493
+ btn.disabled = true
1152
1494
  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
- };
1495
+ .then(() => {
1496
+ btn.textContent = 'OK'
1497
+ setTimeout(fetchJobs, 1000)
1498
+ })
1499
+ .catch(() => {
1500
+ btn.textContent = 'Retry'
1501
+ btn.disabled = false
1502
+ })
1503
+ })
1504
+ })
1505
+ }
1158
1506
 
1159
1507
  jobFilters.forEach((btn) => {
1160
1508
  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
- });
1509
+ jobFilters.forEach((b) => b.classList.remove('ss-dbg-active'))
1510
+ btn.classList.add('ss-dbg-active')
1511
+ jobStatusFilter = btn.getAttribute('data-ss-dbg-job-status')
1512
+ fetchJobs()
1513
+ })
1514
+ })
1167
1515
 
1168
1516
  // ── 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 = '';
1517
+ const configBodyEl = document.getElementById('ss-dbg-config-body')
1518
+ const configSummaryEl = document.getElementById('ss-dbg-config-summary')
1519
+ const configSearchInput = document.getElementById('ss-dbg-search-config')
1520
+ const configTabs = panel.querySelectorAll('[data-ss-dbg-config-tab]')
1521
+ let configRawData = null
1522
+ let configActiveTab = 'config'
1523
+ let configSearchTerm = ''
1176
1524
 
1177
1525
  const flattenConfig = (obj, prefix) => {
1178
- const results = [];
1526
+ const results = []
1179
1527
  if (typeof obj !== 'object' || obj === null) {
1180
- results.push({ path: prefix, value: obj });
1181
- return results;
1528
+ results.push({ path: prefix, value: obj })
1529
+ return results
1182
1530
  }
1183
- const keys = Object.keys(obj);
1531
+ const keys = Object.keys(obj)
1184
1532
  for (let i = 0; i < keys.length; i++) {
1185
- const fullPath = prefix ? prefix + '.' + keys[i] : keys[i];
1186
- const val = obj[keys[i]];
1533
+ const fullPath = prefix ? prefix + '.' + keys[i] : keys[i]
1534
+ const val = obj[keys[i]]
1187
1535
  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]);
1536
+ const nested = flattenConfig(val, fullPath)
1537
+ for (let n = 0; n < nested.length; n++) results.push(nested[n])
1190
1538
  } else {
1191
- results.push({ path: fullPath, value: val });
1539
+ results.push({ path: fullPath, value: val })
1192
1540
  }
1193
1541
  }
1194
- return results;
1195
- };
1542
+ return results
1543
+ }
1196
1544
 
1197
1545
  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
- };
1546
+ if (typeof obj !== 'object' || obj === null || obj.__redacted) return 1
1547
+ let count = 0
1548
+ const keys = Object.keys(obj)
1549
+ for (let i = 0; i < keys.length; i++) count += countLeaves(obj[keys[i]])
1550
+ return count
1551
+ }
1204
1552
 
1205
1553
  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>';
1554
+ if (val === null || val === undefined) return '<span class="ss-dbg-config-val-null">null</span>'
1555
+ if (val === true) return '<span class="ss-dbg-config-val-true">true</span>'
1556
+ if (val === false) return '<span class="ss-dbg-config-val-false">false</span>'
1557
+ if (typeof val === 'number') return '<span class="ss-dbg-config-val-number">' + val + '</span>'
1210
1558
  if (Array.isArray(val)) {
1211
1559
  const items = val.map((item) => {
1212
- if (item === null || item === undefined) return 'null';
1560
+ if (item === null || item === undefined) return 'null'
1213
1561
  if (typeof item === 'object') {
1214
- try { return JSON.stringify(item); } catch { return String(item); }
1562
+ try {
1563
+ return JSON.stringify(item)
1564
+ } catch {
1565
+ return String(item)
1566
+ }
1215
1567
  }
1216
- return String(item);
1217
- });
1218
- return '<span class="ss-dbg-config-val-array">[' + esc(items.join(', ')) + ']</span>';
1568
+ return String(item)
1569
+ })
1570
+ return '<span class="ss-dbg-config-val-array">[' + esc(items.join(', ')) + ']</span>'
1219
1571
  }
1220
1572
  if (typeof val === 'object') {
1221
- try { return '<span class="ss-dbg-config-val-null">' + esc(JSON.stringify(val, null, 2)) + '</span>'; } catch { /* fall through */ }
1573
+ try {
1574
+ return (
1575
+ '<span class="ss-dbg-config-val-null">' + esc(JSON.stringify(val, null, 2)) + '</span>'
1576
+ )
1577
+ } catch {
1578
+ /* fall through */
1579
+ }
1222
1580
  }
1223
- return esc(String(val));
1224
- };
1581
+ return esc(String(val))
1582
+ }
1225
1583
 
1226
1584
  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
- };
1585
+ if (!term) return text
1586
+ const idx = text.toLowerCase().indexOf(term.toLowerCase())
1587
+ if (idx === -1) return text
1588
+ return (
1589
+ text.slice(0, idx) +
1590
+ '<mark class="ss-dbg-config-match">' +
1591
+ text.slice(idx, idx + term.length) +
1592
+ '</mark>' +
1593
+ text.slice(idx + term.length)
1594
+ )
1595
+ }
1232
1596
 
1233
- const isRedactedObj = (val) => val && typeof val === 'object' && val.__redacted === true;
1597
+ const isRedactedObj = (val) => val && typeof val === 'object' && val.__redacted === true
1234
1598
 
1235
1599
  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
- };
1600
+ const cls = prefix + '-config-redacted'
1601
+ const realVal = esc(val.value || '')
1602
+ return (
1603
+ '<span class="' +
1604
+ cls +
1605
+ ' ' +
1606
+ prefix +
1607
+ '-redacted-wrap" data-redacted-value="' +
1608
+ realVal +
1609
+ '">' +
1610
+ '<span class="' +
1611
+ prefix +
1612
+ '-redacted-display">' +
1613
+ esc(val.display) +
1614
+ '</span>' +
1615
+ '<span class="' +
1616
+ prefix +
1617
+ '-redacted-real" style="display:none">' +
1618
+ realVal +
1619
+ '</span>' +
1620
+ '<button type="button" class="' +
1621
+ prefix +
1622
+ '-redacted-reveal" title="Reveal value">' +
1623
+ '<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>' +
1624
+ '</button>' +
1625
+ '<button type="button" class="' +
1626
+ prefix +
1627
+ '-redacted-copy" title="Copy value">' +
1628
+ '<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>' +
1629
+ '</button>' +
1630
+ '</span>'
1631
+ )
1632
+ }
1249
1633
 
1250
1634
  const bindRedactedButtons = (container, prefix) => {
1251
1635
  container.querySelectorAll('.' + prefix + '-redacted-reveal').forEach((btn) => {
1252
1636
  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';
1637
+ e.stopPropagation()
1638
+ const wrap = btn.closest('.' + prefix + '-redacted-wrap')
1639
+ if (!wrap) return
1640
+ const display = wrap.querySelector('.' + prefix + '-redacted-display')
1641
+ const real = wrap.querySelector('.' + prefix + '-redacted-real')
1642
+ if (!display || !real) return
1643
+ const isHidden = real.style.display === 'none'
1644
+ display.style.display = isHidden ? 'none' : ''
1645
+ real.style.display = isHidden ? '' : 'none'
1262
1646
  btn.innerHTML = isHidden
1263
1647
  ? '<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
- });
1648
+ : '<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>'
1649
+ btn.title = isHidden ? 'Hide value' : 'Reveal value'
1650
+ })
1651
+ })
1268
1652
 
1269
1653
  container.querySelectorAll('.' + prefix + '-redacted-copy').forEach((btn) => {
1270
1654
  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;
1655
+ e.stopPropagation()
1656
+ const wrap = btn.closest('.' + prefix + '-redacted-wrap')
1657
+ if (!wrap) return
1658
+ const val = wrap.getAttribute('data-redacted-value')
1659
+ if (!val) return
1276
1660
  navigator.clipboard.writeText(val).then(() => {
1277
- btn.innerHTML = '\u2713';
1661
+ btn.innerHTML = '\u2713'
1278
1662
  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
- };
1663
+ btn.innerHTML =
1664
+ '<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>'
1665
+ }, 1200)
1666
+ })
1667
+ })
1668
+ })
1669
+ }
1285
1670
 
1286
1671
  const fetchConfig = () => {
1287
- if (!DASH_API) return;
1672
+ if (!DASH_API) return
1288
1673
  fetchJSON(DASH_API + '/config')
1289
1674
  .then((data) => {
1290
- configRawData = data;
1291
- fetched.config = true;
1292
- renderConfig();
1675
+ configRawData = data
1676
+ fetched.config = true
1677
+ renderConfig()
1293
1678
  })
1294
1679
  .catch(() => {
1295
- if (configBodyEl) configBodyEl.innerHTML = '<div class="ss-dbg-empty">Config not available</div>';
1296
- });
1297
- };
1680
+ if (configBodyEl)
1681
+ configBodyEl.innerHTML = '<div class="ss-dbg-empty">Config not available</div>'
1682
+ })
1683
+ }
1298
1684
 
1299
1685
  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>';
1686
+ const flat = flattenConfig(obj, prefix)
1687
+ let html =
1688
+ '<table class="ss-dbg-table"><thead><tr>' +
1689
+ '<th style="width:320px">Key</th><th>Value</th>' +
1690
+ '</tr></thead><tbody>'
1304
1691
  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
- };
1692
+ const item = flat[i]
1693
+ const relPath =
1694
+ item.path.indexOf(prefix + '.') === 0 ? item.path.slice(prefix.length + 1) : item.path
1695
+ const redacted = isRedactedObj(item.value)
1696
+ html +=
1697
+ '<tr>' +
1698
+ '<td><span class="ss-dbg-config-key">' +
1699
+ esc(relPath) +
1700
+ '</span></td>' +
1701
+ '<td>' +
1702
+ (redacted
1703
+ ? renderRedacted(item.value, 'ss-dbg')
1704
+ : '<span class="ss-dbg-config-val">' + formatConfigValue(item.value) + '</span>') +
1705
+ '</td>' +
1706
+ '</tr>'
1707
+ }
1708
+ html += '</tbody></table>'
1709
+ return html
1710
+ }
1316
1711
 
1317
1712
  const renderConfig = () => {
1318
- if (!configBodyEl || !configRawData) return;
1713
+ if (!configBodyEl || !configRawData) return
1319
1714
 
1320
- const source = configActiveTab === 'env' ? (configRawData.env || {}) : (configRawData.config || configRawData);
1321
- const flat = flattenConfig(source, '');
1322
- const term = configSearchTerm.toLowerCase();
1323
- let filtered = flat;
1715
+ const source =
1716
+ configActiveTab === 'env' ? configRawData.env || {} : configRawData.config || configRawData
1717
+ const flat = flattenConfig(source, '')
1718
+ const term = configSearchTerm.toLowerCase()
1719
+ let filtered = flat
1324
1720
  if (term) {
1325
1721
  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
- });
1722
+ var valStr = isRedactedObj(item.value) ? item.value.display : String(item.value)
1723
+ return (
1724
+ item.path.toLowerCase().indexOf(term) !== -1 || valStr.toLowerCase().indexOf(term) !== -1
1725
+ )
1726
+ })
1329
1727
  }
1330
1728
 
1331
1729
  if (configSummaryEl) {
1332
- configSummaryEl.textContent = filtered.length + (term ? ' of ' + flat.length : '') + ' entries';
1730
+ configSummaryEl.textContent =
1731
+ filtered.length + (term ? ' of ' + flat.length : '') + ' entries'
1333
1732
  }
1334
1733
 
1335
- let html = '';
1734
+ let html = ''
1336
1735
 
1337
1736
  if (configActiveTab === 'env') {
1338
1737
  // 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>';
1738
+ html +=
1739
+ '<div class="ss-dbg-config-table-wrap"><table class="ss-dbg-table"><thead><tr>' +
1740
+ '<th>Variable</th><th>Value</th>' +
1741
+ '</tr></thead><tbody>'
1342
1742
  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>';
1743
+ const item = filtered[i]
1744
+ const redacted = isRedactedObj(item.value)
1745
+ const displayVal = redacted ? item.value.display : String(item.value)
1746
+ html +=
1747
+ '<tr>' +
1748
+ '<td><span class="ss-dbg-config-key">' +
1749
+ highlightMatch(esc(item.path), term) +
1750
+ '</span></td>' +
1751
+ '<td>' +
1752
+ (redacted
1753
+ ? renderRedacted(item.value, 'ss-dbg')
1754
+ : '<span class="ss-dbg-config-val">' +
1755
+ highlightMatch(esc(displayVal), term) +
1756
+ '</span>') +
1757
+ '</td>' +
1758
+ '</tr>'
1350
1759
  }
1351
- html += '</tbody></table></div>';
1760
+ html += '</tbody></table></div>'
1352
1761
  } else {
1353
1762
  if (term) {
1354
1763
  // 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>';
1764
+ html +=
1765
+ '<div class="ss-dbg-config-table-wrap"><table class="ss-dbg-table"><thead><tr>' +
1766
+ '<th>Path</th><th>Value</th>' +
1767
+ '</tr></thead><tbody>'
1358
1768
  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>';
1769
+ const item = filtered[i]
1770
+ const redacted = isRedactedObj(item.value)
1771
+ const displayVal = redacted ? item.value.display : String(item.value)
1772
+ html +=
1773
+ '<tr>' +
1774
+ '<td><span class="ss-dbg-config-key" style="white-space:nowrap">' +
1775
+ highlightMatch(esc(item.path), term) +
1776
+ '</span></td>' +
1777
+ '<td>' +
1778
+ (redacted
1779
+ ? renderRedacted(item.value, 'ss-dbg')
1780
+ : '<span class="ss-dbg-config-val" style="word-break:break-all">' +
1781
+ highlightMatch(esc(displayVal), term) +
1782
+ '</span>') +
1783
+ '</td>' +
1784
+ '</tr>'
1366
1785
  }
1367
- html += '</tbody></table></div>';
1786
+ html += '</tbody></table></div>'
1368
1787
  } else {
1369
1788
  // Browse mode: collapsible sections
1370
- const topKeys = Object.keys(source);
1371
- html += '<div class="ss-dbg-config-sections">';
1789
+ const topKeys = Object.keys(source)
1790
+ html += '<div class="ss-dbg-config-sections">'
1372
1791
  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;
1792
+ const sectionKey = topKeys[t]
1793
+ const sectionVal = source[sectionKey]
1794
+ const childCount = countLeaves(sectionVal)
1795
+ const isObj =
1796
+ typeof sectionVal === 'object' && sectionVal !== null && !sectionVal.__redacted
1377
1797
 
1378
- html += '<div class="ss-dbg-config-section">';
1798
+ html += '<div class="ss-dbg-config-section">'
1379
1799
  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>';
1800
+ html +=
1801
+ '<div class="ss-dbg-config-section-header" data-config-section="' +
1802
+ esc(sectionKey) +
1803
+ '">' +
1804
+ '<span class="ss-dbg-config-toggle">\u25B6</span>' +
1805
+ '<span class="ss-dbg-config-key">' +
1806
+ esc(sectionKey) +
1807
+ '</span>' +
1808
+ '<span class="ss-dbg-config-count">' +
1809
+ childCount +
1810
+ ' entries</span>' +
1811
+ '</div>'
1812
+ html += '<div class="ss-dbg-config-section-body" style="display:none">'
1813
+ html += renderConfigTable(sectionVal, sectionKey)
1814
+ html += '</div>'
1388
1815
  } 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>';
1816
+ html +=
1817
+ '<div class="ss-dbg-config-section-header ss-dbg-config-leaf">' +
1818
+ '<span class="ss-dbg-config-key">' +
1819
+ esc(sectionKey) +
1820
+ '</span>' +
1821
+ '<span class="ss-dbg-config-val" style="margin-left:8px">' +
1822
+ formatConfigValue(sectionVal) +
1823
+ '</span>' +
1824
+ '</div>'
1393
1825
  }
1394
- html += '</div>';
1826
+ html += '</div>'
1395
1827
  }
1396
- html += '</div>';
1828
+ html += '</div>'
1397
1829
  }
1398
1830
  }
1399
1831
 
1400
- configBodyEl.innerHTML = html;
1832
+ configBodyEl.innerHTML = html
1401
1833
 
1402
1834
  // Bind section toggles
1403
1835
  configBodyEl.querySelectorAll('[data-config-section]').forEach((header) => {
1404
1836
  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
- });
1837
+ const sectionBody = header.nextElementSibling
1838
+ if (!sectionBody) return
1839
+ const isHidden = sectionBody.style.display === 'none'
1840
+ sectionBody.style.display = isHidden ? '' : 'none'
1841
+ const toggle = header.querySelector('.ss-dbg-config-toggle')
1842
+ if (toggle) toggle.textContent = isHidden ? '\u25BC' : '\u25B6'
1843
+ })
1844
+ })
1413
1845
 
1414
1846
  // Bind redacted reveal/copy buttons
1415
- bindRedactedButtons(configBodyEl, 'ss-dbg');
1416
- };
1847
+ bindRedactedButtons(configBodyEl, 'ss-dbg')
1848
+ }
1417
1849
 
1418
1850
  configTabs.forEach((btn) => {
1419
1851
  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
- });
1852
+ configTabs.forEach((b) => b.classList.remove('ss-dbg-active'))
1853
+ btn.classList.add('ss-dbg-active')
1854
+ configActiveTab = btn.getAttribute('data-ss-dbg-config-tab')
1855
+ renderConfig()
1856
+ })
1857
+ })
1426
1858
 
1427
1859
  if (configSearchInput) {
1428
1860
  configSearchInput.addEventListener('input', () => {
1429
- configSearchTerm = configSearchInput.value.trim();
1430
- renderConfig();
1431
- });
1861
+ configSearchTerm = configSearchInput.value.trim()
1862
+ renderConfig()
1863
+ })
1432
1864
  }
1433
1865
 
1434
1866
  // ── Custom panes: fetch, render, bind ───────────────────────────
1435
1867
  const getNestedValue = (obj, path) => {
1436
- const parts = path.split('.');
1437
- let cur = obj;
1868
+ const parts = path.split('.')
1869
+ let cur = obj
1438
1870
  for (let i = 0; i < parts.length; i++) {
1439
- if (cur == null) return undefined;
1440
- cur = cur[parts[i]];
1871
+ if (cur == null) return undefined
1872
+ cur = cur[parts[i]]
1441
1873
  }
1442
- return cur;
1443
- };
1874
+ return cur
1875
+ }
1444
1876
 
1445
1877
  const fetchCustomPane = (pane) => {
1446
- const bodyEl = document.getElementById('ss-dbg-' + pane.id + '-body');
1878
+ const bodyEl = document.getElementById('ss-dbg-' + pane.id + '-body')
1447
1879
  fetchJSON(pane.endpoint)
1448
1880
  .then((data) => {
1449
- const key = pane.dataKey || pane.id;
1450
- const rows = getNestedValue(data, key) || (Array.isArray(data) ? data : []);
1451
- customPaneState[pane.id].data = rows;
1452
- customPaneState[pane.id].fetched = true;
1453
- renderCustomPane(pane);
1881
+ const key = pane.dataKey || pane.id
1882
+ const rows = getNestedValue(data, key) || (Array.isArray(data) ? data : [])
1883
+ customPaneState[pane.id].data = rows
1884
+ customPaneState[pane.id].fetched = true
1885
+ renderCustomPane(pane)
1454
1886
  })
1455
1887
  .catch(() => {
1456
- if (bodyEl) bodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load ' + esc(pane.label) + '</div>';
1457
- });
1458
- };
1888
+ if (bodyEl)
1889
+ bodyEl.innerHTML =
1890
+ '<div class="ss-dbg-empty">Failed to load ' + esc(pane.label) + '</div>'
1891
+ })
1892
+ }
1459
1893
 
1460
1894
  const renderCustomPane = (pane) => {
1461
- const state = customPaneState[pane.id];
1462
- if (!state) return;
1463
- const bodyEl = document.getElementById('ss-dbg-' + pane.id + '-body');
1464
- const summaryEl = document.getElementById('ss-dbg-' + pane.id + '-summary');
1465
- if (!bodyEl) return;
1895
+ const state = customPaneState[pane.id]
1896
+ if (!state) return
1897
+ const bodyEl = document.getElementById('ss-dbg-' + pane.id + '-body')
1898
+ const summaryEl = document.getElementById('ss-dbg-' + pane.id + '-summary')
1899
+ if (!bodyEl) return
1466
1900
 
1467
- const filter = state.filter.toLowerCase();
1468
- let rows = state.data;
1901
+ const filter = state.filter.toLowerCase()
1902
+ let rows = state.data
1469
1903
 
1470
1904
  if (summaryEl) {
1471
- summaryEl.textContent = rows.length + ' ' + pane.label.toLowerCase();
1905
+ summaryEl.textContent = rows.length + ' ' + pane.label.toLowerCase()
1472
1906
  }
1473
1907
 
1474
1908
  if (filter) {
1475
- const searchCols = pane.columns.filter((c) => c.searchable);
1909
+ const searchCols = pane.columns.filter((c) => c.searchable)
1476
1910
  if (searchCols.length > 0) {
1477
1911
  rows = rows.filter((row) =>
1478
1912
  searchCols.some((c) => {
1479
- const v = row[c.key];
1480
- return v != null && String(v).toLowerCase().indexOf(filter) !== -1;
1913
+ const v = row[c.key]
1914
+ return v != null && String(v).toLowerCase().indexOf(filter) !== -1
1481
1915
  })
1482
- );
1916
+ )
1483
1917
  }
1484
1918
  }
1485
1919
 
1486
1920
  if (rows.length === 0) {
1487
- bodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching ' + esc(pane.label.toLowerCase()) : 'No ' + esc(pane.label.toLowerCase()) + ' recorded yet') + '</div>';
1488
- return;
1921
+ bodyEl.innerHTML =
1922
+ '<div class="ss-dbg-empty">' +
1923
+ (filter
1924
+ ? 'No matching ' + esc(pane.label.toLowerCase())
1925
+ : 'No ' + esc(pane.label.toLowerCase()) + ' recorded yet') +
1926
+ '</div>'
1927
+ return
1489
1928
  }
1490
1929
 
1491
- let html = '<table class="ss-dbg-table"><thead><tr>';
1930
+ let html = '<table class="ss-dbg-table"><thead><tr>'
1492
1931
  for (let c = 0; c < pane.columns.length; c++) {
1493
- const col = pane.columns[c];
1494
- html += '<th' + (col.width ? ' style="width:' + col.width + '"' : '') + '>' + esc(col.label) + '</th>';
1932
+ const col = pane.columns[c]
1933
+ html +=
1934
+ '<th' +
1935
+ (col.width ? ' style="width:' + col.width + '"' : '') +
1936
+ '>' +
1937
+ esc(col.label) +
1938
+ '</th>'
1495
1939
  }
1496
- html += '</tr></thead><tbody>';
1940
+ html += '</tr></thead><tbody>'
1497
1941
 
1498
1942
  for (let r = 0; r < rows.length; r++) {
1499
- html += '<tr>';
1943
+ html += '<tr>'
1500
1944
  for (let c = 0; c < pane.columns.length; c++) {
1501
- const col = pane.columns[c];
1502
- const val = rows[r][col.key];
1503
- const cellHtml = formatCell(val, col);
1945
+ const col = pane.columns[c]
1946
+ const val = rows[r][col.key]
1947
+ const cellHtml = formatCell(val, col)
1504
1948
  if (col.filterable && val != null) {
1505
- html += '<td class="ss-dbg-filterable" data-ss-filter-key="' + esc(col.key) + '" data-ss-filter-val="' + esc(String(val)) + '">' + cellHtml + '</td>';
1949
+ html +=
1950
+ '<td class="ss-dbg-filterable" data-ss-filter-key="' +
1951
+ esc(col.key) +
1952
+ '" data-ss-filter-val="' +
1953
+ esc(String(val)) +
1954
+ '">' +
1955
+ cellHtml +
1956
+ '</td>'
1506
1957
  } else {
1507
- html += '<td>' + cellHtml + '</td>';
1958
+ html += '<td>' + cellHtml + '</td>'
1508
1959
  }
1509
1960
  }
1510
- html += '</tr>';
1961
+ html += '</tr>'
1511
1962
  }
1512
1963
 
1513
- html += '</tbody></table>';
1514
- bodyEl.innerHTML = html;
1964
+ html += '</tbody></table>'
1965
+ bodyEl.innerHTML = html
1515
1966
 
1516
1967
  // Bind click-to-filter on filterable cells
1517
1968
  bodyEl.querySelectorAll('.ss-dbg-filterable').forEach((td) => {
1518
- td.style.cursor = 'pointer';
1969
+ td.style.cursor = 'pointer'
1519
1970
  td.addEventListener('click', () => {
1520
- const val = td.getAttribute('data-ss-filter-val');
1521
- const searchInput = document.getElementById('ss-dbg-search-' + pane.id);
1971
+ const val = td.getAttribute('data-ss-filter-val')
1972
+ const searchInput = document.getElementById('ss-dbg-search-' + pane.id)
1522
1973
  if (searchInput) {
1523
- searchInput.value = val;
1524
- state.filter = val;
1525
- renderCustomPane(pane);
1974
+ searchInput.value = val
1975
+ state.filter = val
1976
+ renderCustomPane(pane)
1526
1977
  }
1527
- });
1528
- });
1529
- };
1978
+ })
1979
+ })
1980
+ }
1530
1981
 
1531
1982
  // Bind search + clear for each custom pane
1532
1983
  for (let i = 0; i < customPanes.length; i++) {
1533
- const cp = customPanes[i];
1534
- const searchInput = document.getElementById('ss-dbg-search-' + cp.id);
1535
- const clearBtn = document.getElementById('ss-dbg-' + cp.id + '-clear');
1984
+ const cp = customPanes[i]
1985
+ const searchInput = document.getElementById('ss-dbg-search-' + cp.id)
1986
+ const clearBtn = document.getElementById('ss-dbg-' + cp.id + '-clear')
1536
1987
 
1537
1988
  if (searchInput) {
1538
1989
  searchInput.addEventListener('input', () => {
1539
- customPaneState[cp.id].filter = searchInput.value;
1540
- renderCustomPane(cp);
1541
- });
1990
+ customPaneState[cp.id].filter = searchInput.value
1991
+ renderCustomPane(cp)
1992
+ })
1542
1993
  }
1543
1994
  if (clearBtn) {
1544
1995
  clearBtn.addEventListener('click', () => {
1545
- customPaneState[cp.id].data = [];
1546
- customPaneState[cp.id].fetched = false;
1547
- if (searchInput) searchInput.value = '';
1548
- customPaneState[cp.id].filter = '';
1549
- renderCustomPane(cp);
1550
- });
1996
+ customPaneState[cp.id].data = []
1997
+ customPaneState[cp.id].fetched = false
1998
+ if (searchInput) searchInput.value = ''
1999
+ customPaneState[cp.id].filter = ''
2000
+ renderCustomPane(cp)
2001
+ })
1551
2002
  }
1552
2003
  }
1553
2004
 
1554
2005
  // ── Connection mode indicator ──────────────────────────────────
1555
- const POLL_INTERVAL_NORMAL = REFRESH_INTERVAL;
1556
- const POLL_INTERVAL_LIVE = 15000; // slow polling as fallback when live
2006
+ const POLL_INTERVAL_NORMAL = REFRESH_INTERVAL
2007
+ const POLL_INTERVAL_LIVE = 15000 // slow polling as fallback when live
1557
2008
 
1558
2009
  const updateConnectionIndicator = () => {
1559
- const el = document.getElementById('ss-dbg-conn-mode');
1560
- if (!el) return;
2010
+ const el = document.getElementById('ss-dbg-conn-mode')
2011
+ if (!el) return
1561
2012
  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';
2013
+ el.textContent = 'live'
2014
+ el.className = 'ss-dbg-conn-mode ss-dbg-conn-live'
2015
+ el.title = 'Connected via Transmit (SSE) — real-time updates'
1565
2016
  } 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';
2017
+ el.textContent = 'polling'
2018
+ el.className = 'ss-dbg-conn-mode ss-dbg-conn-polling'
2019
+ el.title = 'Polling every ' + POLL_INTERVAL_NORMAL / 1000 + 's'
1569
2020
  }
1570
- };
2021
+ }
1571
2022
 
1572
2023
  // ── Auto-refresh ────────────────────────────────────────────────
1573
2024
  const startRefresh = () => {
1574
- stopRefresh();
1575
- fetchMiniStats();
1576
- const interval = isLive ? POLL_INTERVAL_LIVE : POLL_INTERVAL_NORMAL;
2025
+ stopRefresh()
2026
+ fetchMiniStats()
2027
+ const interval = isLive ? POLL_INTERVAL_LIVE : POLL_INTERVAL_NORMAL
1577
2028
  refreshTimer = setInterval(() => {
1578
- if (!isOpen) return;
1579
- loadTab(activeTab);
1580
- fetchMiniStats();
1581
- }, interval);
1582
- };
2029
+ if (!isOpen) return
2030
+ loadTab(activeTab)
2031
+ fetchMiniStats()
2032
+ }, interval)
2033
+ }
1583
2034
 
1584
2035
  const stopRefresh = () => {
1585
2036
  if (refreshTimer) {
1586
- clearInterval(refreshTimer);
1587
- refreshTimer = null;
2037
+ clearInterval(refreshTimer)
2038
+ refreshTimer = null
1588
2039
  }
1589
- };
2040
+ }
1590
2041
 
1591
2042
  // ── Transmit (SSE) support ─────────────────────────────────────
1592
2043
  const initTransmit = () => {
1593
2044
  // window.Transmit is set by the inline IIFE injected before this module
1594
- const TransmitClass = (typeof window !== 'undefined' && window.Transmit) ? window.Transmit : null;
2045
+ const TransmitClass = typeof window !== 'undefined' && window.Transmit ? window.Transmit : null
1595
2046
 
1596
- if (!TransmitClass) return; // Transmit client not available
2047
+ if (!TransmitClass) return // Transmit client not available
1597
2048
 
1598
2049
  try {
1599
2050
  const transmit = new TransmitClass({
1600
2051
  baseUrl: window.location.origin,
1601
2052
  onSubscription: () => {
1602
- isLive = true;
1603
- updateConnectionIndicator();
2053
+ isLive = true
2054
+ updateConnectionIndicator()
1604
2055
  // Restart refresh with slower interval now that we have live updates
1605
- if (isOpen) startRefresh();
2056
+ if (isOpen) startRefresh()
1606
2057
  },
1607
2058
  onReconnectFailed: () => {
1608
- isLive = false;
1609
- updateConnectionIndicator();
1610
- if (isOpen) startRefresh();
2059
+ isLive = false
2060
+ updateConnectionIndicator()
2061
+ if (isOpen) startRefresh()
1611
2062
  },
1612
2063
  onSubscribeFailed: () => {
1613
- isLive = false;
1614
- updateConnectionIndicator();
1615
- }
1616
- });
2064
+ isLive = false
2065
+ updateConnectionIndicator()
2066
+ },
2067
+ })
1617
2068
 
1618
- transmitSub = transmit.subscription('server-stats/debug');
2069
+ transmitSub = transmit.subscription('server-stats/debug')
1619
2070
 
1620
2071
  transmitSub.onMessage((message) => {
1621
2072
  try {
1622
- const event = typeof message === 'string' ? JSON.parse(message) : message;
1623
- handleLiveEvent(event);
1624
- } catch { /* ignore */ }
1625
- });
2073
+ const event = typeof message === 'string' ? JSON.parse(message) : message
2074
+ handleLiveEvent(event)
2075
+ } catch {
2076
+ /* ignore */
2077
+ }
2078
+ })
1626
2079
 
1627
2080
  transmitSub.create().catch(() => {
1628
- isLive = false;
1629
- updateConnectionIndicator();
1630
- });
2081
+ isLive = false
2082
+ updateConnectionIndicator()
2083
+ })
1631
2084
  } catch {
1632
2085
  // Transmit init failed — stay on polling
1633
2086
  }
1634
- };
2087
+ }
1635
2088
 
1636
2089
  const handleLiveEvent = (event) => {
1637
- if (!isOpen) return;
2090
+ if (!isOpen) return
1638
2091
 
1639
2092
  // Backend sends { types: ['query', 'event', ...] }
1640
- const types = event.types || (event.type ? [event.type] : []);
2093
+ const types = event.types || (event.type ? [event.type] : [])
1641
2094
  const tabMap = {
1642
- 'query': 'queries',
1643
- 'event': 'events',
1644
- 'email': 'emails',
1645
- 'trace': 'timeline'
1646
- };
2095
+ query: 'queries',
2096
+ event: 'events',
2097
+ email: 'emails',
2098
+ trace: 'timeline',
2099
+ }
1647
2100
 
1648
- let shouldRefresh = false;
2101
+ let shouldRefresh = false
1649
2102
  for (let i = 0; i < types.length; i++) {
1650
- const targetTab = tabMap[types[i]];
2103
+ const targetTab = tabMap[types[i]]
1651
2104
  if (targetTab && targetTab === activeTab) {
1652
- shouldRefresh = true;
2105
+ shouldRefresh = true
1653
2106
  }
1654
2107
  if (types[i] === 'query') {
1655
- updateBarQueryBadge();
2108
+ updateBarQueryBadge()
1656
2109
  }
1657
2110
  }
1658
2111
 
1659
2112
  if (shouldRefresh) {
1660
- loadTab(activeTab);
2113
+ loadTab(activeTab)
1661
2114
  }
1662
- };
2115
+ }
1663
2116
 
1664
2117
  // Initialize Transmit after a short delay to let the page fully load
1665
- setTimeout(initTransmit, 500);
1666
- updateConnectionIndicator();
2118
+ setTimeout(initTransmit, 500)
2119
+ updateConnectionIndicator()
1667
2120
 
1668
2121
  // ── Stats bar query badge (always visible) ──────────────────────
1669
2122
  const updateBarQueryBadge = () => {
1670
- const el = document.getElementById('ss-b-dbg-queries');
1671
- if (!el) return;
2123
+ const el = document.getElementById('ss-b-dbg-queries')
2124
+ if (!el) return
1672
2125
 
1673
2126
  fetchJSON(BASE + '/queries')
1674
2127
  .then((data) => {
1675
- const s = data.summary || {};
1676
- const valEl = el.querySelector('.ss-value');
2128
+ const s = data.summary || {}
2129
+ const valEl = el.querySelector('.ss-value')
1677
2130
  if (valEl) {
1678
- valEl.textContent = (s.total || 0) + ' / ' + (s.avgDuration || 0).toFixed(1) + 'ms';
1679
- valEl.className = 'ss-value ' + (s.slow > 0 ? 'ss-amber' : 'ss-green');
2131
+ valEl.textContent = (s.total || 0) + ' / ' + (s.avgDuration || 0).toFixed(1) + 'ms'
2132
+ valEl.className = 'ss-value ' + (s.slow > 0 ? 'ss-amber' : 'ss-green')
1680
2133
  }
1681
2134
  })
1682
- .catch(() => {});
1683
- };
2135
+ .catch(() => {})
2136
+ }
1684
2137
 
1685
- updateBarQueryBadge();
1686
- setInterval(updateBarQueryBadge, 5000);
1687
- })();
2138
+ updateBarQueryBadge()
2139
+ setInterval(updateBarQueryBadge, 5000)
2140
+ })()