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.
- package/README.md +272 -142
- package/dist/configure.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.d.ts +2 -2
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/server_stats_controller.d.ts +1 -1
- package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.js +8 -8
- package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_controller.js +244 -522
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_routes.js +7 -2
- package/dist/src/dashboard/dashboard_store.d.ts +6 -3
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +54 -78
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.js +3 -1
- package/dist/src/dashboard/models/stats_event.d.ts +1 -1
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.d.ts +2 -2
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.js +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
- package/dist/src/debug/debug_store.d.ts +6 -6
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -10
- package/dist/src/debug/email_collector.d.ts +0 -9
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +6 -28
- package/dist/src/debug/event_collector.d.ts +1 -1
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +17 -17
- package/dist/src/debug/query_collector.d.ts +1 -1
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +13 -14
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.d.ts +1 -1
- package/dist/src/debug/route_inspector.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.js +12 -12
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +6 -5
- package/dist/src/edge/client/dashboard.css +516 -171
- package/dist/src/edge/client/dashboard.js +2756 -1662
- package/dist/src/edge/client/debug-panel.css +476 -133
- package/dist/src/edge/client/debug-panel.js +1496 -1043
- package/dist/src/edge/client/stats-bar.css +64 -30
- package/dist/src/edge/client/stats-bar.js +598 -319
- package/dist/src/edge/plugin.d.ts +1 -1
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +41 -59
- package/dist/src/edge/views/stats-bar.edge +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +7 -6
- package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
- package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts +1 -1
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +33 -32
- package/dist/src/types.d.ts +2 -2
- package/dist/src/utils/json_helpers.d.ts +8 -0
- package/dist/src/utils/json_helpers.d.ts.map +1 -0
- package/dist/src/utils/json_helpers.js +21 -0
- package/dist/src/utils/mail_helpers.d.ts +13 -0
- package/dist/src/utils/mail_helpers.d.ts.map +1 -0
- package/dist/src/utils/mail_helpers.js +26 -0
- package/dist/src/utils/math_helpers.d.ts +8 -0
- package/dist/src/utils/math_helpers.d.ts.map +1 -0
- package/dist/src/utils/math_helpers.js +11 -0
- package/dist/src/utils/time_helpers.d.ts +12 -0
- package/dist/src/utils/time_helpers.d.ts.map +1 -0
- package/dist/src/utils/time_helpers.js +32 -0
- package/dist/src/utils/transmit_client.d.ts +9 -0
- package/dist/src/utils/transmit_client.d.ts.map +1 -0
- package/dist/src/utils/transmit_client.js +20 -0
- 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 =
|
|
31
|
-
|
|
32
|
-
|
|
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 =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 ||
|
|
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 ?
|
|
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 =
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
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
|
|
87
|
-
|
|
96
|
+
if (typeof s !== 'string') s = '' + s
|
|
97
|
+
return s
|
|
98
|
+
.replace(/&/g, '&')
|
|
99
|
+
.replace(/</g, '<')
|
|
100
|
+
.replace(/>/g, '>')
|
|
101
|
+
.replace(/"/g, '"')
|
|
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
|
|
99
|
-
|
|
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')
|
|
115
|
-
|
|
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 =
|
|
120
|
-
|
|
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 =
|
|
132
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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 =
|
|
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(
|
|
306
|
-
q
|
|
307
|
-
|
|
308
|
-
|
|
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 =
|
|
314
|
-
|
|
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 =
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 +=
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
+ '</
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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(
|
|
388
|
-
e
|
|
389
|
-
|
|
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 =
|
|
395
|
-
|
|
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 =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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 +=
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
+
|
|
413
|
-
+
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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">⎘</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(() => {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
551
|
+
btn.textContent = '\u2713'
|
|
552
|
+
setTimeout(() => {
|
|
553
|
+
btn.innerHTML = '⎘'
|
|
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(
|
|
500
|
-
r
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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 =
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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 =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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 :
|
|
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 +=
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
+
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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)
|
|
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(
|
|
677
|
-
(e
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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 =
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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">📎</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 +=
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
+
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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> ' +
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
898
|
+
'<strong>Subject:</strong> ' +
|
|
899
|
+
esc(email.subject) +
|
|
900
|
+
' | <strong>From:</strong> ' +
|
|
901
|
+
esc(email.from) +
|
|
902
|
+
' | <strong>To:</strong> ' +
|
|
903
|
+
esc(email.to) +
|
|
904
|
+
(email.cc ? ' | <strong>CC:</strong> ' + esc(email.cc) : '') +
|
|
905
|
+
' | <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
|
+
' | <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(
|
|
801
|
-
t
|
|
802
|
-
|
|
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 =
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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">⚠</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 +=
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
+
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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)
|
|
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="' +
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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 · ' +
|
|
1090
|
+
trace.spanCount +
|
|
1091
|
+
' spans · ' +
|
|
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 =
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
+ '
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
+
|
|
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 +=
|
|
928
|
-
|
|
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 =
|
|
966
|
-
|
|
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 ' +
|
|
970
|
-
|
|
971
|
-
|
|
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)
|
|
994
|
-
|
|
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">' +
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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 =
|
|
1024
|
-
|
|
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 =
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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 +=
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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 =
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
+
|
|
1058
|
-
|
|
1059
|
-
|
|
1349
|
+
cacheBodyEl.innerHTML =
|
|
1350
|
+
'<div class="ss-dbg-cache-detail">' +
|
|
1351
|
+
'<button type="button" class="ss-dbg-btn-clear" id="ss-dbg-cache-back">← Back</button>' +
|
|
1352
|
+
' <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(() => {
|
|
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)
|
|
1088
|
-
|
|
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 =
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
+
|
|
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 =
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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 =
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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(() => {
|
|
1154
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
+
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
+
|
|
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 =
|
|
1280
|
-
|
|
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)
|
|
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 =
|
|
1302
|
-
|
|
1303
|
-
|
|
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 =
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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 =
|
|
1321
|
-
|
|
1322
|
-
const
|
|
1323
|
-
|
|
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
|
|
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 =
|
|
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 +=
|
|
1340
|
-
|
|
1341
|
-
|
|
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 +=
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
+
|
|
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 +=
|
|
1356
|
-
|
|
1357
|
-
|
|
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 +=
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
+
|
|
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 =
|
|
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 +=
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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 +=
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
+
|
|
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)
|
|
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 =
|
|
1488
|
-
|
|
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 +=
|
|
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 +=
|
|
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
|
|
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 ' +
|
|
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 =
|
|
2045
|
+
const TransmitClass = typeof window !== 'undefined' && window.Transmit ? window.Transmit : null
|
|
1595
2046
|
|
|
1596
|
-
if (!TransmitClass) return
|
|
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 {
|
|
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
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
})()
|