adonisjs-server-stats 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -9
- package/dist/src/dashboard/chart_aggregator.d.ts +21 -0
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -0
- package/dist/src/dashboard/chart_aggregator.js +89 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +147 -0
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_controller.js +1008 -0
- package/dist/src/dashboard/dashboard_routes.d.ts +16 -0
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_routes.js +88 -0
- package/dist/src/dashboard/dashboard_store.d.ts +158 -0
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -0
- package/dist/src/dashboard/dashboard_store.js +723 -0
- package/dist/src/dashboard/integrations/cache_inspector.d.ts +88 -0
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/cache_inspector.js +215 -0
- package/dist/src/dashboard/integrations/config_inspector.d.ts +33 -0
- package/dist/src/dashboard/integrations/config_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/config_inspector.js +155 -0
- package/dist/src/dashboard/integrations/index.d.ts +7 -0
- package/dist/src/dashboard/integrations/index.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/index.js +3 -0
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +106 -0
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -0
- package/dist/src/dashboard/integrations/queue_inspector.js +182 -0
- package/dist/src/dashboard/migrator.d.ts +18 -0
- package/dist/src/dashboard/migrator.d.ts.map +1 -0
- package/dist/src/dashboard/migrator.js +144 -0
- package/dist/src/dashboard/models/stats_email.d.ts +19 -0
- package/dist/src/dashboard/models/stats_email.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_email.js +66 -0
- package/dist/src/dashboard/models/stats_event.d.ts +14 -0
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_event.js +43 -0
- package/dist/src/dashboard/models/stats_log.d.ts +12 -0
- package/dist/src/dashboard/models/stats_log.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_log.js +42 -0
- package/dist/src/dashboard/models/stats_metric.d.ts +15 -0
- package/dist/src/dashboard/models/stats_metric.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_metric.js +50 -0
- package/dist/src/dashboard/models/stats_query.d.ts +20 -0
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_query.js +67 -0
- package/dist/src/dashboard/models/stats_request.d.ts +21 -0
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_request.js +61 -0
- package/dist/src/dashboard/models/stats_saved_filter.d.ts +11 -0
- package/dist/src/dashboard/models/stats_saved_filter.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_saved_filter.js +38 -0
- package/dist/src/dashboard/models/stats_trace.d.ts +19 -0
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -0
- package/dist/src/dashboard/models/stats_trace.js +67 -0
- package/dist/src/debug/debug_store.d.ts +5 -0
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -0
- package/dist/src/debug/email_collector.d.ts +2 -0
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +4 -0
- package/dist/src/debug/event_collector.d.ts +2 -0
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +11 -2
- package/dist/src/debug/query_collector.d.ts +2 -0
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +11 -0
- package/dist/src/debug/ring_buffer.d.ts +3 -0
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/ring_buffer.js +6 -0
- package/dist/src/debug/trace_collector.d.ts +4 -2
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +7 -2
- package/dist/src/debug/types.d.ts +8 -0
- package/dist/src/debug/types.d.ts.map +1 -1
- package/dist/src/edge/client/dashboard.css +1504 -0
- package/dist/src/edge/client/dashboard.js +2378 -0
- package/dist/src/edge/client/debug-panel.css +528 -108
- package/dist/src/edge/client/debug-panel.js +663 -22
- package/dist/src/edge/client/stats-bar.css +112 -38
- package/dist/src/edge/client/stats-bar.js +37 -3
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +21 -0
- package/dist/src/edge/views/dashboard.edge +382 -0
- package/dist/src/edge/views/debug-panel.edge +60 -14
- package/dist/src/edge/views/stats-bar.edge +9 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/middleware/request_tracking_middleware.d.ts +20 -0
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +66 -2
- package/dist/src/provider/server_stats_provider.d.ts +13 -0
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +175 -1
- package/dist/src/types.d.ts +42 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +14 -1
|
@@ -0,0 +1,2378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side SPA for the server-stats full-page dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Config is read from:
|
|
5
|
+
* - data-path on #ss-dash — dashboard base path (e.g. "/__stats")
|
|
6
|
+
* - data-tracing on #ss-dash — "1" if tracing enabled
|
|
7
|
+
* - <script id="ss-dash-config"> — JSON config object
|
|
8
|
+
*
|
|
9
|
+
* Hash-based routing: #overview, #requests, #queries, etc.
|
|
10
|
+
* Deep link support: #queries?id=42, #logs?requestId=abc123
|
|
11
|
+
*/
|
|
12
|
+
(function () {
|
|
13
|
+
var root = document.getElementById('ss-dash');
|
|
14
|
+
if (!root) return;
|
|
15
|
+
|
|
16
|
+
var BASE = (root.dataset.path || '/__stats').replace(/\/+$/, '');
|
|
17
|
+
var API = BASE + '/api';
|
|
18
|
+
var tracingEnabled = root.dataset.tracing === '1';
|
|
19
|
+
|
|
20
|
+
// Config from JSON script tag
|
|
21
|
+
var dashConfig = {};
|
|
22
|
+
try {
|
|
23
|
+
var cfgEl = document.getElementById('ss-dash-config');
|
|
24
|
+
if (cfgEl) dashConfig = JSON.parse(cfgEl.textContent || '{}');
|
|
25
|
+
} catch (e) { /* ignore */ }
|
|
26
|
+
|
|
27
|
+
var customPanes = dashConfig.customPanes || [];
|
|
28
|
+
|
|
29
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
30
|
+
var esc = function (s) {
|
|
31
|
+
if (typeof s !== 'string') s = '' + s;
|
|
32
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
var timeAgo = function (ts) {
|
|
36
|
+
if (!ts) return '-';
|
|
37
|
+
var d = typeof ts === 'string' ? new Date(ts).getTime() : ts;
|
|
38
|
+
var diff = Math.floor((Date.now() - d) / 1000);
|
|
39
|
+
if (diff < 0) return 'just now';
|
|
40
|
+
if (diff < 60) return diff + 's ago';
|
|
41
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
42
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
43
|
+
return Math.floor(diff / 86400) + 'd ago';
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
var formatTime = function (ts) {
|
|
47
|
+
var d = typeof ts === 'string' ? new Date(ts) : new Date(ts);
|
|
48
|
+
if (isNaN(d.getTime())) return '-';
|
|
49
|
+
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
50
|
+
+ '.' + String(d.getMilliseconds()).padStart(3, '0');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
var methodClass = function (m) {
|
|
54
|
+
return 'ss-dash-method ss-dash-method-' + (typeof m === 'string' ? m.toLowerCase() : '');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
var durationClass = function (ms) {
|
|
58
|
+
if (ms > 500) return 'ss-dash-very-slow';
|
|
59
|
+
if (ms > 100) return 'ss-dash-slow';
|
|
60
|
+
return '';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
var statusClass = function (code) {
|
|
64
|
+
if (code >= 500) return 'ss-dash-status-5xx';
|
|
65
|
+
if (code >= 400) return 'ss-dash-status-4xx';
|
|
66
|
+
if (code >= 300) return 'ss-dash-status-3xx';
|
|
67
|
+
return 'ss-dash-status-2xx';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
var compactPreview = function (val, maxLen) {
|
|
71
|
+
if (val === null) return 'null';
|
|
72
|
+
if (typeof val === 'string') return '"' + (val.length > 40 ? val.slice(0, 40) + '...' : val) + '"';
|
|
73
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
74
|
+
if (Array.isArray(val)) {
|
|
75
|
+
if (val.length === 0) return '[]';
|
|
76
|
+
var items = val.slice(0, 3).map(function (v) { return compactPreview(v, 30); });
|
|
77
|
+
var s = '[' + items.join(', ') + (val.length > 3 ? ', ...' + val.length + ' items' : '') + ']';
|
|
78
|
+
return s.length > maxLen ? '[' + val.length + ' items]' : s;
|
|
79
|
+
}
|
|
80
|
+
if (typeof val === 'object') {
|
|
81
|
+
var keys = Object.keys(val);
|
|
82
|
+
if (keys.length === 0) return '{}';
|
|
83
|
+
var pairs = [];
|
|
84
|
+
for (var i = 0; i < Math.min(keys.length, 4); i++) {
|
|
85
|
+
pairs.push(keys[i] + ': ' + compactPreview(val[keys[i]], 30));
|
|
86
|
+
}
|
|
87
|
+
var s2 = '{ ' + pairs.join(', ') + (keys.length > 4 ? ', ...+' + (keys.length - 4) : '') + ' }';
|
|
88
|
+
return s2.length > maxLen ? '{ ' + keys.slice(0, 6).join(', ') + (keys.length > 6 ? ', ...' : '') + ' }' : s2;
|
|
89
|
+
}
|
|
90
|
+
return String(val);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
var eventPreview = function (data) {
|
|
94
|
+
if (!data) return '-';
|
|
95
|
+
try {
|
|
96
|
+
var parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
97
|
+
return compactPreview(parsed, 100);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return data.length > 100 ? data.slice(0, 100) + '...' : data;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
var shortReqId = function (id) { return id ? id.slice(0, 8) : ''; };
|
|
104
|
+
|
|
105
|
+
var formatCell = function (value, col) {
|
|
106
|
+
if (value === null || value === undefined) return '<span style="color:var(--ss-dim)">-</span>';
|
|
107
|
+
var fmt = col.format || 'text';
|
|
108
|
+
switch (fmt) {
|
|
109
|
+
case 'time': return typeof value === 'number' ? formatTime(value) : esc(value);
|
|
110
|
+
case 'timeAgo': return '<span class="ss-dash-event-time">' + (typeof value === 'number' ? timeAgo(value) : esc(value)) + '</span>';
|
|
111
|
+
case 'duration': {
|
|
112
|
+
var ms = typeof value === 'number' ? value : parseFloat(value);
|
|
113
|
+
if (isNaN(ms)) return esc(value);
|
|
114
|
+
return '<span class="ss-dash-duration ' + durationClass(ms) + '">' + ms.toFixed(2) + 'ms</span>';
|
|
115
|
+
}
|
|
116
|
+
case 'method': return '<span class="' + methodClass(value) + '">' + esc(value) + '</span>';
|
|
117
|
+
case 'json': {
|
|
118
|
+
if (typeof value === 'string') { try { value = JSON.parse(value); } catch (e) { /* use as-is */ } }
|
|
119
|
+
var preview = typeof value === 'object' ? compactPreview(value, 100) : String(value);
|
|
120
|
+
return '<span class="ss-dash-data-preview" style="cursor:default">' + esc(preview) + '</span>';
|
|
121
|
+
}
|
|
122
|
+
case 'badge': {
|
|
123
|
+
var sv = String(value).toLowerCase();
|
|
124
|
+
var colorMap = col.badgeColorMap || {};
|
|
125
|
+
var color = colorMap[sv] || 'muted';
|
|
126
|
+
return '<span class="ss-dash-badge ss-dash-badge-' + esc(color) + '">' + esc(value) + '</span>';
|
|
127
|
+
}
|
|
128
|
+
default: return esc(value);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ── State ─────────────────────────────────────────────────────
|
|
133
|
+
var activeSection = 'overview';
|
|
134
|
+
var sidebarCollapsed = localStorage.getItem('ss-dash-sidebar') === 'collapsed';
|
|
135
|
+
var refreshTimer = null;
|
|
136
|
+
var transmitSub = null;
|
|
137
|
+
var isLive = false;
|
|
138
|
+
|
|
139
|
+
// Per-section pagination state
|
|
140
|
+
var pageState = {};
|
|
141
|
+
var PER_PAGE = 50;
|
|
142
|
+
|
|
143
|
+
var getPage = function (section) {
|
|
144
|
+
if (!pageState[section]) pageState[section] = { page: 1, total: 0 };
|
|
145
|
+
return pageState[section];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ── Fetch helper ──────────────────────────────────────────────
|
|
149
|
+
var fetchJSON = function (url) {
|
|
150
|
+
return fetch(url, { credentials: 'same-origin' })
|
|
151
|
+
.then(function (r) {
|
|
152
|
+
if (!r.ok) throw new Error(r.status);
|
|
153
|
+
return r.json();
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ── Theme ─────────────────────────────────────────────────────
|
|
158
|
+
var themeOverride = localStorage.getItem('ss-dash-theme');
|
|
159
|
+
var themeBtn = document.getElementById('ss-dash-theme-btn');
|
|
160
|
+
|
|
161
|
+
var applyTheme = function () {
|
|
162
|
+
if (themeOverride) {
|
|
163
|
+
root.setAttribute('data-theme', themeOverride);
|
|
164
|
+
} else {
|
|
165
|
+
root.removeAttribute('data-theme');
|
|
166
|
+
}
|
|
167
|
+
if (themeBtn) {
|
|
168
|
+
var isDark = themeOverride === 'dark' || (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
169
|
+
themeBtn.textContent = isDark ? '\u2600' : '\u263D';
|
|
170
|
+
themeBtn.title = isDark ? 'Switch to light theme' : 'Switch to dark theme';
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (themeBtn) {
|
|
175
|
+
themeBtn.addEventListener('click', function () {
|
|
176
|
+
var isDark = themeOverride === 'dark' || (!themeOverride && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
177
|
+
themeOverride = isDark ? 'light' : 'dark';
|
|
178
|
+
localStorage.setItem('ss-dash-theme', themeOverride);
|
|
179
|
+
applyTheme();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
applyTheme();
|
|
183
|
+
|
|
184
|
+
// ── Sidebar ───────────────────────────────────────────────────
|
|
185
|
+
var sidebar = document.getElementById('ss-dash-sidebar');
|
|
186
|
+
var sidebarToggle = document.getElementById('ss-dash-sidebar-toggle');
|
|
187
|
+
var navItems = root.querySelectorAll('[data-ss-section]');
|
|
188
|
+
|
|
189
|
+
var applySidebar = function () {
|
|
190
|
+
if (sidebar) sidebar.classList.toggle('ss-dash-collapsed', sidebarCollapsed);
|
|
191
|
+
if (sidebarToggle) sidebarToggle.innerHTML = sidebarCollapsed
|
|
192
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18l6-6-6-6"/></svg>'
|
|
193
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M15 18l-6-6 6-6"/></svg>';
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (sidebarToggle) {
|
|
197
|
+
sidebarToggle.addEventListener('click', function () {
|
|
198
|
+
sidebarCollapsed = !sidebarCollapsed;
|
|
199
|
+
localStorage.setItem('ss-dash-sidebar', sidebarCollapsed ? 'collapsed' : 'expanded');
|
|
200
|
+
applySidebar();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
applySidebar();
|
|
204
|
+
|
|
205
|
+
// ── Section switching ─────────────────────────────────────────
|
|
206
|
+
var switchSection = function (name) {
|
|
207
|
+
if (name === activeSection) return;
|
|
208
|
+
|
|
209
|
+
navItems.forEach(function (item) {
|
|
210
|
+
item.classList.toggle('ss-dash-active', item.getAttribute('data-ss-section') === name);
|
|
211
|
+
});
|
|
212
|
+
root.querySelectorAll('.ss-dash-pane').forEach(function (p) {
|
|
213
|
+
p.classList.toggle('ss-dash-active', p.id === 'ss-dash-pane-' + name);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
activeSection = name;
|
|
217
|
+
location.hash = name;
|
|
218
|
+
loadSection(name);
|
|
219
|
+
startRefresh();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
navItems.forEach(function (item) {
|
|
223
|
+
item.addEventListener('click', function () {
|
|
224
|
+
switchSection(item.getAttribute('data-ss-section'));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ── Data loading ──────────────────────────────────────────────
|
|
229
|
+
var sectionLoaded = {};
|
|
230
|
+
|
|
231
|
+
var loadSection = function (name) {
|
|
232
|
+
switch (name) {
|
|
233
|
+
case 'overview': fetchOverview(); break;
|
|
234
|
+
case 'requests': fetchRequests(); break;
|
|
235
|
+
case 'queries': fetchQueries(); break;
|
|
236
|
+
case 'events': fetchEvents(); break;
|
|
237
|
+
case 'routes':
|
|
238
|
+
if (!sectionLoaded.routes) fetchRoutes();
|
|
239
|
+
break;
|
|
240
|
+
case 'logs': fetchLogs(); break;
|
|
241
|
+
case 'emails': fetchEmails(); break;
|
|
242
|
+
case 'timeline': fetchTraces(); break;
|
|
243
|
+
case 'cache': fetchCache(); break;
|
|
244
|
+
case 'jobs': fetchJobs(); break;
|
|
245
|
+
case 'config':
|
|
246
|
+
if (!sectionLoaded.config) fetchConfig();
|
|
247
|
+
break;
|
|
248
|
+
default: {
|
|
249
|
+
var cp = customPanes.find(function (p) { return p.id === name; });
|
|
250
|
+
if (cp) fetchCustomPane(cp);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// ── Overview ──────────────────────────────────────────────────
|
|
256
|
+
var overviewRange = '1h';
|
|
257
|
+
|
|
258
|
+
var fetchOverview = function () {
|
|
259
|
+
Promise.all([
|
|
260
|
+
fetchJSON(API + '/overview?range=' + overviewRange),
|
|
261
|
+
fetchJSON(API + '/overview/chart?range=' + overviewRange)
|
|
262
|
+
])
|
|
263
|
+
.then(function (results) { renderOverview(results[0], results[1]); })
|
|
264
|
+
.catch(function () {
|
|
265
|
+
var el = document.getElementById('ss-dash-overview-content');
|
|
266
|
+
if (el) el.innerHTML = '<div class="ss-dash-empty">Failed to load overview</div>';
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Cache chart data so live updates don't wipe the chart
|
|
271
|
+
var lastChartData = [];
|
|
272
|
+
var lastSparklines = {};
|
|
273
|
+
|
|
274
|
+
var renderOverview = function (data, chartData) {
|
|
275
|
+
var el = document.getElementById('ss-dash-overview-content');
|
|
276
|
+
if (!el) return;
|
|
277
|
+
|
|
278
|
+
// Preserve sparklines and chart from previous full fetch when live data arrives
|
|
279
|
+
if (data.sparklines) lastSparklines = data.sparklines;
|
|
280
|
+
var sparklines = data.sparklines || lastSparklines;
|
|
281
|
+
if (chartData && chartData.buckets && chartData.buckets.length > 0) lastChartData = chartData.buckets;
|
|
282
|
+
var chart = (chartData && chartData.buckets) || lastChartData;
|
|
283
|
+
var slowest = data.slowestEndpoints || [];
|
|
284
|
+
var queryStats = data.queryStats || {};
|
|
285
|
+
var recentErrors = data.recentErrors || [];
|
|
286
|
+
|
|
287
|
+
var avgVal = data.avgResponseTime || 0;
|
|
288
|
+
var p95Val = data.p95ResponseTime || 0;
|
|
289
|
+
var rpmVal = data.requestsPerMinute || 0;
|
|
290
|
+
var errVal = data.errorRate || 0;
|
|
291
|
+
var hasData = (data.totalRequests || 0) > 0;
|
|
292
|
+
|
|
293
|
+
var avgClass = avgVal > 500 ? 'ss-dash-red' : avgVal > 200 ? 'ss-dash-amber' : 'ss-dash-accent';
|
|
294
|
+
var p95Class = p95Val > 500 ? 'ss-dash-red' : p95Val > 200 ? 'ss-dash-amber' : 'ss-dash-accent';
|
|
295
|
+
var errClass = errVal > 5 ? 'ss-dash-red' : errVal > 1 ? 'ss-dash-amber' : 'ss-dash-accent';
|
|
296
|
+
|
|
297
|
+
var fmtMs = function (v) { return hasData ? v.toFixed(1) + 'ms' : '-'; };
|
|
298
|
+
var fmtNum = function (v) { return hasData ? String(Math.round(v * 10) / 10) : '-'; };
|
|
299
|
+
var fmtPct = function (v) { return hasData ? v.toFixed(1) + '%' : '-'; };
|
|
300
|
+
|
|
301
|
+
var html = '<div class="ss-dash-overview">';
|
|
302
|
+
|
|
303
|
+
// Top cards
|
|
304
|
+
html += '<div class="ss-dash-cards">';
|
|
305
|
+
html += renderCard('Avg Response Time', fmtMs(avgVal), hasData ? avgClass : 'ss-dash-dim', sparklines.avgResponseTime);
|
|
306
|
+
html += renderCard('P95 Response Time', fmtMs(p95Val), hasData ? p95Class : 'ss-dash-dim', sparklines.p95ResponseTime);
|
|
307
|
+
html += renderCard('Requests / min', fmtNum(rpmVal), hasData ? 'ss-dash-accent' : 'ss-dash-dim', sparklines.requestsPerMinute);
|
|
308
|
+
html += renderCard('Error Rate', fmtPct(errVal), hasData ? errClass : 'ss-dash-dim', sparklines.errorRate);
|
|
309
|
+
html += '</div>';
|
|
310
|
+
|
|
311
|
+
// Chart
|
|
312
|
+
html += '<div class="ss-dash-chart-container">';
|
|
313
|
+
html += '<div class="ss-dash-chart-header">';
|
|
314
|
+
html += '<span class="ss-dash-chart-title">Request Volume</span>';
|
|
315
|
+
html += '<div class="ss-dash-btn-group">';
|
|
316
|
+
['5m', '15m', '30m', '1h', '6h', '24h', '7d'].forEach(function (r) {
|
|
317
|
+
html += '<button class="ss-dash-btn' + (r === overviewRange ? ' ss-dash-active' : '') + '" data-range="' + r + '">' + r + '</button>';
|
|
318
|
+
});
|
|
319
|
+
html += '</div></div>';
|
|
320
|
+
html += '<div class="ss-dash-chart" id="ss-dash-chart-area"></div>';
|
|
321
|
+
html += '</div>';
|
|
322
|
+
|
|
323
|
+
// Secondary cards
|
|
324
|
+
html += '<div class="ss-dash-secondary-cards">';
|
|
325
|
+
|
|
326
|
+
// Slowest endpoints
|
|
327
|
+
html += '<div class="ss-dash-secondary-card">';
|
|
328
|
+
html += '<div class="ss-dash-secondary-card-title">Slowest Endpoints</div>';
|
|
329
|
+
if (slowest.length > 0) {
|
|
330
|
+
html += '<ul class="ss-dash-secondary-list">';
|
|
331
|
+
slowest.forEach(function (ep) {
|
|
332
|
+
html += '<li><span title="' + esc(ep.url || ep.pattern || '-') + '">' + esc(ep.url || ep.pattern || '-') + '</span><span class="ss-dash-secondary-list-value ss-dash-duration ' + durationClass(ep.avgDuration || 0) + '">' + (ep.avgDuration || 0).toFixed(1) + 'ms</span></li>';
|
|
333
|
+
});
|
|
334
|
+
html += '</ul>';
|
|
335
|
+
} else {
|
|
336
|
+
html += '<div class="ss-dash-empty" style="min-height:60px">No data yet</div>';
|
|
337
|
+
}
|
|
338
|
+
html += '</div>';
|
|
339
|
+
|
|
340
|
+
// Query stats
|
|
341
|
+
html += '<div class="ss-dash-secondary-card">';
|
|
342
|
+
html += '<div class="ss-dash-secondary-card-title">Query Stats</div>';
|
|
343
|
+
html += '<ul class="ss-dash-secondary-list">';
|
|
344
|
+
html += '<li><span>Total Queries</span><span class="ss-dash-secondary-list-value">' + (queryStats.total || 0) + '</span></li>';
|
|
345
|
+
html += '<li><span>Avg Duration</span><span class="ss-dash-secondary-list-value">' + (queryStats.avgDuration || 0).toFixed(1) + 'ms</span></li>';
|
|
346
|
+
html += '<li><span>Queries / Request</span><span class="ss-dash-secondary-list-value">' + (queryStats.perRequest || 0).toFixed(1) + '</span></li>';
|
|
347
|
+
html += '</ul>';
|
|
348
|
+
html += '</div>';
|
|
349
|
+
|
|
350
|
+
// Recent errors
|
|
351
|
+
html += '<div class="ss-dash-secondary-card">';
|
|
352
|
+
html += '<div class="ss-dash-secondary-card-title">Recent Errors</div>';
|
|
353
|
+
if (recentErrors.length > 0) {
|
|
354
|
+
html += '<ul class="ss-dash-secondary-list">';
|
|
355
|
+
recentErrors.forEach(function (err) {
|
|
356
|
+
html += '<li><span style="color:var(--ss-red-fg)" title="' + esc(err.message || '') + '">' + esc(err.message || '') + '</span><span class="ss-dash-secondary-list-value">' + timeAgo(err.createdAt || err.created_at || err.timestamp) + '</span></li>';
|
|
357
|
+
});
|
|
358
|
+
html += '</ul>';
|
|
359
|
+
} else {
|
|
360
|
+
html += '<div class="ss-dash-empty" style="min-height:60px">No recent errors</div>';
|
|
361
|
+
}
|
|
362
|
+
html += '</div>';
|
|
363
|
+
|
|
364
|
+
html += '</div></div>';
|
|
365
|
+
el.innerHTML = html;
|
|
366
|
+
|
|
367
|
+
// Bind range buttons
|
|
368
|
+
el.querySelectorAll('[data-range]').forEach(function (btn) {
|
|
369
|
+
btn.addEventListener('click', function () {
|
|
370
|
+
overviewRange = btn.getAttribute('data-range');
|
|
371
|
+
fetchOverview();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Render SVG chart
|
|
376
|
+
if (chart.length > 0) renderBarChart(chart);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
var renderCard = function (title, value, colorClass, sparkline) {
|
|
380
|
+
var html = '<div class="ss-dash-card">';
|
|
381
|
+
html += '<div class="ss-dash-card-title">' + esc(title) + '</div>';
|
|
382
|
+
html += '<div class="ss-dash-card-value ' + colorClass + '">' + esc(value) + '</div>';
|
|
383
|
+
if (sparkline && sparkline.length > 1) {
|
|
384
|
+
html += '<div class="ss-dash-sparkline">' + renderSparklineSVG(sparkline) + '</div>';
|
|
385
|
+
}
|
|
386
|
+
html += '</div>';
|
|
387
|
+
return html;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
var renderSparklineSVG = function (points) {
|
|
391
|
+
var w = 200, h = 30;
|
|
392
|
+
var max = Math.max.apply(null, points) || 1;
|
|
393
|
+
var step = w / (points.length - 1);
|
|
394
|
+
|
|
395
|
+
var coords = points.map(function (v, i) {
|
|
396
|
+
return (i * step).toFixed(1) + ',' + (h - (v / max * h * 0.9 + h * 0.05)).toFixed(1);
|
|
397
|
+
});
|
|
398
|
+
var pathD = 'M' + coords.join(' L');
|
|
399
|
+
var areaD = pathD + ' L' + w + ',' + h + ' L0,' + h + ' Z';
|
|
400
|
+
|
|
401
|
+
return '<svg viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="none">'
|
|
402
|
+
+ '<path class="ss-dash-sparkline-area" d="' + areaD + '"/>'
|
|
403
|
+
+ '<path class="ss-dash-sparkline-line" d="' + pathD + '"/>'
|
|
404
|
+
+ '</svg>';
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
var renderBarChart = function (data) {
|
|
408
|
+
var container = document.getElementById('ss-dash-chart-area');
|
|
409
|
+
if (!container) return;
|
|
410
|
+
|
|
411
|
+
if (!data || data.length === 0) {
|
|
412
|
+
container.innerHTML = '<div class="ss-dash-empty" style="height:100%;display:flex;align-items:center;justify-content:center">No chart data for this range</div>';
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
var w = container.clientWidth || 600;
|
|
417
|
+
var h = container.clientHeight || 200;
|
|
418
|
+
var pad = { top: 12, right: 12, bottom: 28, left: 38 };
|
|
419
|
+
var cw = w - pad.left - pad.right;
|
|
420
|
+
var ch = h - pad.top - pad.bottom;
|
|
421
|
+
|
|
422
|
+
var maxCount = 0;
|
|
423
|
+
data.forEach(function (d) {
|
|
424
|
+
var total = (d.requestCount || 0) + (d.request_count || 0);
|
|
425
|
+
if (total > maxCount) maxCount = total;
|
|
426
|
+
});
|
|
427
|
+
// Add 10% headroom so bars don't touch the top
|
|
428
|
+
var yMax = maxCount > 0 ? Math.ceil(maxCount * 1.1) : 1;
|
|
429
|
+
|
|
430
|
+
// Choose nice Y-axis tick values
|
|
431
|
+
var yTicks = niceYTicks(yMax, 4);
|
|
432
|
+
var yTop = yTicks[yTicks.length - 1] || yMax;
|
|
433
|
+
|
|
434
|
+
var toY = function (val) { return pad.top + ch - (val / yTop) * ch; };
|
|
435
|
+
var toX = function (i) { return pad.left + (i / (data.length - 1 || 1)) * cw; };
|
|
436
|
+
|
|
437
|
+
// Build point arrays for the area chart
|
|
438
|
+
var totalPoints = [];
|
|
439
|
+
var errorPoints = [];
|
|
440
|
+
data.forEach(function (d, i) {
|
|
441
|
+
var total = (d.requestCount || 0) + (d.request_count || 0);
|
|
442
|
+
var errors = (d.errorCount || 0) + (d.error_count || 0);
|
|
443
|
+
totalPoints.push({ x: toX(i), y: toY(total), val: total });
|
|
444
|
+
errorPoints.push({ x: toX(i), y: toY(errors), val: errors });
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Smooth curve helper (monotone cubic spline)
|
|
448
|
+
var smoothPath = function (points) {
|
|
449
|
+
if (points.length < 2) return '';
|
|
450
|
+
if (points.length === 2) return 'M' + points[0].x.toFixed(1) + ',' + points[0].y.toFixed(1) + 'L' + points[1].x.toFixed(1) + ',' + points[1].y.toFixed(1);
|
|
451
|
+
|
|
452
|
+
var d = 'M' + points[0].x.toFixed(1) + ',' + points[0].y.toFixed(1);
|
|
453
|
+
for (var pi = 1; pi < points.length; pi++) {
|
|
454
|
+
var p0 = points[pi - 1];
|
|
455
|
+
var p1 = points[pi];
|
|
456
|
+
var cpx = (p0.x + p1.x) / 2;
|
|
457
|
+
d += ' C' + cpx.toFixed(1) + ',' + p0.y.toFixed(1) + ' ' + cpx.toFixed(1) + ',' + p1.y.toFixed(1) + ' ' + p1.x.toFixed(1) + ',' + p1.y.toFixed(1);
|
|
458
|
+
}
|
|
459
|
+
return d;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
var baseline = pad.top + ch;
|
|
463
|
+
|
|
464
|
+
var svg = '<svg viewBox="0 0 ' + w + ' ' + h + '" class="ss-dash-chart-svg">';
|
|
465
|
+
|
|
466
|
+
// Defs: gradients
|
|
467
|
+
svg += '<defs>';
|
|
468
|
+
svg += '<linearGradient id="ss-cg-total" x1="0" y1="0" x2="0" y2="1">';
|
|
469
|
+
svg += '<stop offset="0%" stop-color="var(--ss-accent)" stop-opacity="0.3"/>';
|
|
470
|
+
svg += '<stop offset="100%" stop-color="var(--ss-accent)" stop-opacity="0.02"/>';
|
|
471
|
+
svg += '</linearGradient>';
|
|
472
|
+
svg += '<linearGradient id="ss-cg-error" x1="0" y1="0" x2="0" y2="1">';
|
|
473
|
+
svg += '<stop offset="0%" stop-color="var(--ss-red-fg)" stop-opacity="0.35"/>';
|
|
474
|
+
svg += '<stop offset="100%" stop-color="var(--ss-red-fg)" stop-opacity="0.02"/>';
|
|
475
|
+
svg += '</linearGradient>';
|
|
476
|
+
svg += '</defs>';
|
|
477
|
+
|
|
478
|
+
// Horizontal grid lines
|
|
479
|
+
yTicks.forEach(function (val) {
|
|
480
|
+
var yy = toY(val);
|
|
481
|
+
svg += '<line x1="' + pad.left + '" y1="' + yy.toFixed(1) + '" x2="' + (w - pad.right) + '" y2="' + yy.toFixed(1) + '" stroke="var(--ss-border-faint)" stroke-width="0.5" stroke-dasharray="3,3"/>';
|
|
482
|
+
svg += '<text x="' + (pad.left - 6) + '" y="' + yy.toFixed(1) + '" text-anchor="end" fill="var(--ss-dim)" font-size="9" dominant-baseline="middle">' + val + '</text>';
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Total requests: filled area + line
|
|
486
|
+
var totalPath = smoothPath(totalPoints);
|
|
487
|
+
if (totalPath) {
|
|
488
|
+
var last = totalPoints[totalPoints.length - 1];
|
|
489
|
+
var first = totalPoints[0];
|
|
490
|
+
svg += '<path d="' + totalPath + ' L' + last.x.toFixed(1) + ',' + baseline + ' L' + first.x.toFixed(1) + ',' + baseline + ' Z" fill="url(#ss-cg-total)"/>';
|
|
491
|
+
svg += '<path d="' + totalPath + '" fill="none" stroke="var(--ss-accent)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Error requests: filled area + line (if any errors)
|
|
495
|
+
var hasErrors = errorPoints.some(function (p) { return p.val > 0; });
|
|
496
|
+
if (hasErrors) {
|
|
497
|
+
var errorPath = smoothPath(errorPoints);
|
|
498
|
+
if (errorPath) {
|
|
499
|
+
var eLast = errorPoints[errorPoints.length - 1];
|
|
500
|
+
var eFirst = errorPoints[0];
|
|
501
|
+
svg += '<path d="' + errorPath + ' L' + eLast.x.toFixed(1) + ',' + baseline + ' L' + eFirst.x.toFixed(1) + ',' + baseline + ' Z" fill="url(#ss-cg-error)"/>';
|
|
502
|
+
svg += '<path d="' + errorPath + '" fill="none" stroke="var(--ss-red-fg)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="4,2"/>';
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Interactive dots and hover zones
|
|
507
|
+
data.forEach(function (d, i) {
|
|
508
|
+
var total = (d.requestCount || 0) + (d.request_count || 0);
|
|
509
|
+
var errors = (d.errorCount || 0) + (d.error_count || 0);
|
|
510
|
+
var success = total - errors;
|
|
511
|
+
var cx = totalPoints[i].x;
|
|
512
|
+
var cy = totalPoints[i].y;
|
|
513
|
+
|
|
514
|
+
// Invisible wide hover target
|
|
515
|
+
var sliceW = cw / (data.length || 1);
|
|
516
|
+
svg += '<rect x="' + (cx - sliceW / 2).toFixed(1) + '" y="' + pad.top + '" width="' + sliceW.toFixed(1) + '" height="' + ch + '" fill="transparent" class="ss-dash-chart-hover-zone" data-idx="' + i + '"/>';
|
|
517
|
+
|
|
518
|
+
// Visible dot (only for non-zero)
|
|
519
|
+
if (total > 0) {
|
|
520
|
+
svg += '<circle cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="2.5" fill="var(--ss-accent)" stroke="var(--ss-surface)" stroke-width="1" class="ss-dash-chart-dot" data-idx="' + i + '"/>';
|
|
521
|
+
}
|
|
522
|
+
if (errors > 0) {
|
|
523
|
+
var ey = errorPoints[i].y;
|
|
524
|
+
svg += '<circle cx="' + cx.toFixed(1) + '" cy="' + ey.toFixed(1) + '" r="2" fill="var(--ss-red-fg)" stroke="var(--ss-surface)" stroke-width="1" class="ss-dash-chart-dot ss-dash-chart-dot-err" data-idx="' + i + '"/>';
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// X axis labels
|
|
529
|
+
var maxLabels = Math.min(10, data.length);
|
|
530
|
+
var labelInterval = Math.max(1, Math.ceil(data.length / maxLabels));
|
|
531
|
+
data.forEach(function (d, i) {
|
|
532
|
+
if (i % labelInterval === 0 || i === data.length - 1) {
|
|
533
|
+
var x = toX(i);
|
|
534
|
+
var label = '';
|
|
535
|
+
if (d.bucket) {
|
|
536
|
+
var bd = new Date(d.bucket);
|
|
537
|
+
label = bd.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
|
538
|
+
}
|
|
539
|
+
svg += '<text x="' + x.toFixed(1) + '" y="' + (h - 6) + '" text-anchor="middle" fill="var(--ss-dim)" font-size="9">' + esc(label) + '</text>';
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
svg += '</svg>';
|
|
544
|
+
|
|
545
|
+
// Tooltip element
|
|
546
|
+
svg += '<div class="ss-dash-chart-tooltip" id="ss-dash-chart-tip" style="display:none"></div>';
|
|
547
|
+
|
|
548
|
+
container.innerHTML = svg;
|
|
549
|
+
|
|
550
|
+
// Hover interactivity
|
|
551
|
+
var tip = document.getElementById('ss-dash-chart-tip');
|
|
552
|
+
var svgEl = container.querySelector('svg');
|
|
553
|
+
if (svgEl && tip) {
|
|
554
|
+
var dots = container.querySelectorAll('.ss-dash-chart-dot');
|
|
555
|
+
var zones = container.querySelectorAll('.ss-dash-chart-hover-zone');
|
|
556
|
+
|
|
557
|
+
var showTip = function (idx, x) {
|
|
558
|
+
var d = data[idx];
|
|
559
|
+
if (!d) return;
|
|
560
|
+
var total = (d.requestCount || 0) + (d.request_count || 0);
|
|
561
|
+
var errors = (d.errorCount || 0) + (d.error_count || 0);
|
|
562
|
+
var success = total - errors;
|
|
563
|
+
var time = '';
|
|
564
|
+
if (d.bucket) {
|
|
565
|
+
var bd = new Date(d.bucket);
|
|
566
|
+
time = bd.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
|
567
|
+
}
|
|
568
|
+
tip.innerHTML = '<div style="font-weight:600;margin-bottom:2px;color:var(--ss-text)">' + esc(time) + '</div>'
|
|
569
|
+
+ '<div style="color:var(--ss-accent)">' + total + ' requests</div>'
|
|
570
|
+
+ (errors > 0 ? '<div style="color:var(--ss-red-fg)">' + errors + ' errors</div>' : '');
|
|
571
|
+
tip.style.display = 'block';
|
|
572
|
+
|
|
573
|
+
// Position tooltip
|
|
574
|
+
var tipW = tip.offsetWidth || 100;
|
|
575
|
+
var left = x - tipW / 2;
|
|
576
|
+
if (left < 0) left = 4;
|
|
577
|
+
if (left + tipW > w) left = w - tipW - 4;
|
|
578
|
+
tip.style.left = left + 'px';
|
|
579
|
+
tip.style.top = (pad.top - 4) + 'px';
|
|
580
|
+
|
|
581
|
+
// Highlight dots for this index
|
|
582
|
+
dots.forEach(function (dot) {
|
|
583
|
+
var isActive = dot.getAttribute('data-idx') === String(idx);
|
|
584
|
+
dot.setAttribute('r', isActive ? (dot.classList.contains('ss-dash-chart-dot-err') ? '3.5' : '4') : (dot.classList.contains('ss-dash-chart-dot-err') ? '2' : '2.5'));
|
|
585
|
+
dot.style.opacity = isActive ? '1' : '0.5';
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
var hideTip = function () {
|
|
590
|
+
tip.style.display = 'none';
|
|
591
|
+
dots.forEach(function (dot) {
|
|
592
|
+
dot.setAttribute('r', dot.classList.contains('ss-dash-chart-dot-err') ? '2' : '2.5');
|
|
593
|
+
dot.style.opacity = '1';
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
zones.forEach(function (zone) {
|
|
598
|
+
zone.addEventListener('mouseenter', function () {
|
|
599
|
+
var idx = parseInt(zone.getAttribute('data-idx'), 10);
|
|
600
|
+
var rect = container.getBoundingClientRect();
|
|
601
|
+
var px = totalPoints[idx] ? totalPoints[idx].x : 0;
|
|
602
|
+
showTip(idx, px);
|
|
603
|
+
});
|
|
604
|
+
zone.addEventListener('mouseleave', hideTip);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Legend
|
|
609
|
+
var legend = document.getElementById('ss-dash-chart-legend');
|
|
610
|
+
if (!legend) {
|
|
611
|
+
legend = document.createElement('div');
|
|
612
|
+
legend.id = 'ss-dash-chart-legend';
|
|
613
|
+
legend.className = 'ss-dash-chart-legend';
|
|
614
|
+
container.parentNode.insertBefore(legend, container.nextSibling);
|
|
615
|
+
}
|
|
616
|
+
legend.innerHTML = '<span class="ss-dash-chart-legend-item"><span class="ss-dash-legend-dot" style="background:var(--ss-accent)"></span>Requests</span>'
|
|
617
|
+
+ (hasErrors ? '<span class="ss-dash-chart-legend-item"><span class="ss-dash-legend-dot" style="background:var(--ss-red-fg)"></span>Errors</span>' : '');
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// Compute nice Y-axis tick values
|
|
621
|
+
var niceYTicks = function (max, count) {
|
|
622
|
+
if (max <= 0) return [0];
|
|
623
|
+
var raw = max / count;
|
|
624
|
+
var mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
625
|
+
var nice = raw / mag;
|
|
626
|
+
var step;
|
|
627
|
+
if (nice <= 1) step = mag;
|
|
628
|
+
else if (nice <= 2) step = 2 * mag;
|
|
629
|
+
else if (nice <= 5) step = 5 * mag;
|
|
630
|
+
else step = 10 * mag;
|
|
631
|
+
|
|
632
|
+
var ticks = [];
|
|
633
|
+
for (var v = step; v <= max + step * 0.5; v += step) {
|
|
634
|
+
ticks.push(Math.round(v));
|
|
635
|
+
}
|
|
636
|
+
return ticks;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// ── Requests ──────────────────────────────────────────────────
|
|
640
|
+
var fetchRequests = function () {
|
|
641
|
+
var ps = getPage('requests');
|
|
642
|
+
fetchJSON(API + '/requests?page=' + ps.page + '&limit=' + PER_PAGE)
|
|
643
|
+
.then(function (data) { renderRequests(data); })
|
|
644
|
+
.catch(function () { setInner('ss-dash-requests-body', '<div class="ss-dash-empty">Failed to load requests</div>'); });
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
var renderRequests = function (data) {
|
|
648
|
+
var items = data.data || data.requests || [];
|
|
649
|
+
var ps = getPage('requests');
|
|
650
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
651
|
+
|
|
652
|
+
setInner('ss-dash-requests-summary', ps.total + ' requests');
|
|
653
|
+
|
|
654
|
+
if (items.length === 0) {
|
|
655
|
+
setInner('ss-dash-requests-body', '<div class="ss-dash-empty">No requests recorded yet</div>');
|
|
656
|
+
renderPagination('requests', ps);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
var CT = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
|
661
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
662
|
+
+ '<th style="width:50px">#</th>'
|
|
663
|
+
+ '<th style="width:60px">Method</th>'
|
|
664
|
+
+ '<th>URL</th>'
|
|
665
|
+
+ '<th style="width:55px">Status</th>'
|
|
666
|
+
+ '<th style="width:80px">Duration</th>'
|
|
667
|
+
+ '<th style="width:50px">Spans</th>'
|
|
668
|
+
+ '<th style="width:30px" title="Warnings">⚠</th>'
|
|
669
|
+
+ '<th style="width:60px">Time</th>'
|
|
670
|
+
+ '</tr></thead><tbody>';
|
|
671
|
+
|
|
672
|
+
items.forEach(function (r) {
|
|
673
|
+
html += '<tr class="ss-dash-clickable" data-request-id="' + r.id + '">'
|
|
674
|
+
+ '<td style="color:var(--ss-dim);' + CT + '">' + r.id + '</td>'
|
|
675
|
+
+ '<td><span class="' + methodClass(r.method) + '">' + esc(r.method) + '</span></td>'
|
|
676
|
+
+ '<td style="color:var(--ss-text);' + CT + '" title="' + esc(r.url) + '">' + esc(r.url) + '</td>'
|
|
677
|
+
+ '<td><span class="ss-dash-status ' + statusClass(r.status_code || r.statusCode) + '">' + (r.status_code || r.statusCode) + '</span></td>'
|
|
678
|
+
+ '<td class="ss-dash-duration ' + durationClass(r.duration) + '">' + (r.duration || 0).toFixed(1) + 'ms</td>'
|
|
679
|
+
+ '<td style="color:var(--ss-muted);text-align:center">' + (r.span_count || r.spanCount || 0) + '</td>'
|
|
680
|
+
+ '<td style="text-align:center">' + ((r.warning_count || r.warningCount || 0) > 0 ? '<span style="color:var(--ss-amber-fg)">' + (r.warning_count || r.warningCount) + '</span>' : '<span style="color:var(--ss-dim)">-</span>') + '</td>'
|
|
681
|
+
+ '<td class="ss-dash-event-time" style="white-space:nowrap">' + timeAgo(r.createdAt || r.created_at || r.timestamp) + '</td>'
|
|
682
|
+
+ '</tr>';
|
|
683
|
+
});
|
|
684
|
+
html += '</tbody></table>';
|
|
685
|
+
|
|
686
|
+
setInner('ss-dash-requests-body', html);
|
|
687
|
+
updateBadge('requests', ps.total);
|
|
688
|
+
renderPagination('requests', ps);
|
|
689
|
+
|
|
690
|
+
// Click to expand trace
|
|
691
|
+
var body = document.getElementById('ss-dash-requests-body');
|
|
692
|
+
if (body) {
|
|
693
|
+
body.querySelectorAll('[data-request-id]').forEach(function (row) {
|
|
694
|
+
row.addEventListener('click', function () {
|
|
695
|
+
var id = row.getAttribute('data-request-id');
|
|
696
|
+
fetchJSON(API + '/requests/' + id)
|
|
697
|
+
.then(function (trace) { showRequestDetail(trace); })
|
|
698
|
+
.catch(function () { /* ignore */ });
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
var showRequestDetail = function (trace) {
|
|
705
|
+
var listEl = document.getElementById('ss-dash-requests-list');
|
|
706
|
+
var detailEl = document.getElementById('ss-dash-requests-detail');
|
|
707
|
+
var titleEl = document.getElementById('ss-dash-requests-detail-title');
|
|
708
|
+
var waterfallEl = document.getElementById('ss-dash-requests-waterfall');
|
|
709
|
+
if (!listEl || !detailEl) return;
|
|
710
|
+
|
|
711
|
+
listEl.style.display = 'none';
|
|
712
|
+
detailEl.style.display = 'flex';
|
|
713
|
+
detailEl.classList.add('ss-dash-active');
|
|
714
|
+
|
|
715
|
+
if (titleEl) {
|
|
716
|
+
titleEl.innerHTML = '<span class="' + methodClass(trace.method) + '">' + esc(trace.method) + '</span> '
|
|
717
|
+
+ esc(trace.url) + ' '
|
|
718
|
+
+ '<span class="ss-dash-status ' + statusClass(trace.status_code || trace.statusCode) + '">' + (trace.status_code || trace.statusCode) + '</span>'
|
|
719
|
+
+ '<span class="ss-dash-tl-meta">' + (trace.total_duration || trace.totalDuration || trace.duration || 0).toFixed(1) + 'ms</span>';
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (waterfallEl) renderWaterfall(waterfallEl, trace);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// Back button for requests detail
|
|
726
|
+
var reqBackBtn = document.getElementById('ss-dash-requests-back');
|
|
727
|
+
if (reqBackBtn) {
|
|
728
|
+
reqBackBtn.addEventListener('click', function () {
|
|
729
|
+
var listEl = document.getElementById('ss-dash-requests-list');
|
|
730
|
+
var detailEl = document.getElementById('ss-dash-requests-detail');
|
|
731
|
+
if (listEl) listEl.style.display = '';
|
|
732
|
+
if (detailEl) { detailEl.style.display = 'none'; detailEl.classList.remove('ss-dash-active'); }
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ── Queries ───────────────────────────────────────────────────
|
|
737
|
+
var queryGrouped = false;
|
|
738
|
+
|
|
739
|
+
var fetchQueries = function () {
|
|
740
|
+
if (queryGrouped) {
|
|
741
|
+
fetchJSON(API + '/queries/grouped')
|
|
742
|
+
.then(function (data) { renderQueriesGrouped(data); })
|
|
743
|
+
.catch(function () { setInner('ss-dash-queries-body', '<div class="ss-dash-empty">Failed to load queries</div>'); });
|
|
744
|
+
} else {
|
|
745
|
+
var ps = getPage('queries');
|
|
746
|
+
fetchJSON(API + '/queries?page=' + ps.page + '&limit=' + PER_PAGE)
|
|
747
|
+
.then(function (data) { renderQueries(data); })
|
|
748
|
+
.catch(function () { setInner('ss-dash-queries-body', '<div class="ss-dash-empty">Failed to load queries</div>'); });
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
var renderQueries = function (data) {
|
|
753
|
+
var items = data.data || data.queries || [];
|
|
754
|
+
var summary = data.summary || data.meta || {};
|
|
755
|
+
var ps = getPage('queries');
|
|
756
|
+
ps.total = summary.total || (data.meta ? data.meta.total : items.length);
|
|
757
|
+
|
|
758
|
+
setInner('ss-dash-queries-summary', (ps.total || items.length) + ' queries'
|
|
759
|
+
+ (summary.slow > 0 ? ', ' + summary.slow + ' slow' : '')
|
|
760
|
+
+ (summary.duplicates > 0 ? ', ' + summary.duplicates + ' dup' : '')
|
|
761
|
+
+ ', avg ' + (summary.avgDuration || 0).toFixed(1) + 'ms');
|
|
762
|
+
|
|
763
|
+
updateBadge('queries', ps.total);
|
|
764
|
+
|
|
765
|
+
if (items.length === 0) {
|
|
766
|
+
setInner('ss-dash-queries-body', '<div class="ss-dash-empty">No queries recorded yet</div>');
|
|
767
|
+
renderPagination('queries', ps);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
772
|
+
+ '<th style="width:50px">#</th>'
|
|
773
|
+
+ '<th>SQL</th>'
|
|
774
|
+
+ '<th style="width:75px">Duration</th>'
|
|
775
|
+
+ '<th style="width:60px">Method</th>'
|
|
776
|
+
+ '<th style="width:100px">Model</th>'
|
|
777
|
+
+ '<th style="width:80px">Connection</th>'
|
|
778
|
+
+ '<th style="width:50px">Time</th>'
|
|
779
|
+
+ '<th style="width:70px">EXPLAIN</th>'
|
|
780
|
+
+ '</tr></thead><tbody>';
|
|
781
|
+
|
|
782
|
+
items.forEach(function (q) {
|
|
783
|
+
var dur = q.duration || 0;
|
|
784
|
+
var sqlMethod = q.method || q.sql_method || '';
|
|
785
|
+
var modelName = q.model || '-';
|
|
786
|
+
html += '<tr>'
|
|
787
|
+
+ '<td style="color:var(--ss-dim)">' + q.id + '</td>'
|
|
788
|
+
+ '<td><span class="ss-dash-sql" title="Click to expand" onclick="this.classList.toggle(\'ss-dash-expanded\')">' + esc(q.sql || q.sql_text || '') + '</span></td>'
|
|
789
|
+
+ '<td class="ss-dash-duration ' + durationClass(dur) + '">' + dur.toFixed(2) + 'ms</td>'
|
|
790
|
+
+ '<td><span class="' + methodClass(sqlMethod) + '">' + esc(sqlMethod) + '</span></td>'
|
|
791
|
+
+ '<td style="color:var(--ss-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(modelName) + '">' + esc(modelName) + '</td>'
|
|
792
|
+
+ '<td style="color:var(--ss-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(q.connection || '-') + '</td>'
|
|
793
|
+
+ '<td class="ss-dash-event-time" style="white-space:nowrap">' + timeAgo(q.createdAt || q.created_at || q.timestamp) + '</td>'
|
|
794
|
+
+ '<td>' + ((sqlMethod || '').toLowerCase() === 'select' ? '<button class="ss-dash-explain-btn" data-query-id="' + q.id + '">EXPLAIN</button>' : '') + '</td>'
|
|
795
|
+
+ '</tr>';
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
html += '</tbody></table>';
|
|
799
|
+
setInner('ss-dash-queries-body', html);
|
|
800
|
+
renderPagination('queries', ps);
|
|
801
|
+
bindExplainButtons();
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
var renderQueriesGrouped = function (data) {
|
|
805
|
+
var groups = data.groups || data.data || [];
|
|
806
|
+
|
|
807
|
+
setInner('ss-dash-queries-summary', groups.length + ' query patterns');
|
|
808
|
+
updateBadge('queries', groups.length);
|
|
809
|
+
|
|
810
|
+
if (groups.length === 0) {
|
|
811
|
+
setInner('ss-dash-queries-body', '<div class="ss-dash-empty">No queries recorded yet</div>');
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
816
|
+
+ '<th>Pattern</th>'
|
|
817
|
+
+ '<th style="width:55px">Count</th>'
|
|
818
|
+
+ '<th style="width:70px">Avg</th>'
|
|
819
|
+
+ '<th style="width:70px">Min</th>'
|
|
820
|
+
+ '<th style="width:70px">Max</th>'
|
|
821
|
+
+ '<th style="width:70px">Total</th>'
|
|
822
|
+
+ '<th style="width:55px">% Time</th>'
|
|
823
|
+
+ '</tr></thead><tbody>';
|
|
824
|
+
|
|
825
|
+
groups.forEach(function (g) {
|
|
826
|
+
var isDup = (g.count || 0) >= 3;
|
|
827
|
+
html += '<tr>'
|
|
828
|
+
+ '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><span class="ss-dash-sql" onclick="this.classList.toggle(\'ss-dash-expanded\')">' + esc(g.pattern || g.sql_normalized || '') + '</span>'
|
|
829
|
+
+ (isDup ? ' <span class="ss-dash-dup">DUP</span>' : '') + '</td>'
|
|
830
|
+
+ '<td style="color:var(--ss-muted);text-align:center">' + (g.count || 0) + '</td>'
|
|
831
|
+
+ '<td class="ss-dash-duration ' + durationClass(g.avg_duration || 0) + '">' + (g.avg_duration || 0).toFixed(2) + 'ms</td>'
|
|
832
|
+
+ '<td class="ss-dash-duration">' + (g.min_duration || 0).toFixed(2) + 'ms</td>'
|
|
833
|
+
+ '<td class="ss-dash-duration ' + durationClass(g.max_duration || 0) + '">' + (g.max_duration || 0).toFixed(2) + 'ms</td>'
|
|
834
|
+
+ '<td class="ss-dash-duration">' + (g.total_duration || 0).toFixed(1) + 'ms</td>'
|
|
835
|
+
+ '<td style="color:var(--ss-muted);text-align:center">' + (g.pct_time || 0).toFixed(1) + '%</td>'
|
|
836
|
+
+ '</tr>';
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
html += '</tbody></table>';
|
|
840
|
+
setInner('ss-dash-queries-body', html);
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// ── EXPLAIN: render a PostgreSQL JSON plan as a tree ──────────
|
|
844
|
+
var renderPlanNode = function (node, depth) {
|
|
845
|
+
if (!node) return '';
|
|
846
|
+
depth = depth || 0;
|
|
847
|
+
var indent = depth * 20;
|
|
848
|
+
var html = '<div class="ss-dash-explain-node" style="margin-left:' + indent + 'px">';
|
|
849
|
+
var nodeType = node['Node Type'] || 'Unknown';
|
|
850
|
+
var relation = node['Relation Name'] ? ' on <strong>' + esc(node['Relation Name']) + '</strong>' : '';
|
|
851
|
+
var alias = node['Alias'] && node['Alias'] !== node['Relation Name'] ? ' (' + esc(node['Alias']) + ')' : '';
|
|
852
|
+
var idx = node['Index Name'] ? ' using <em>' + esc(node['Index Name']) + '</em>' : '';
|
|
853
|
+
|
|
854
|
+
html += '<div class="ss-dash-explain-node-header">'
|
|
855
|
+
+ '<span class="ss-dash-explain-node-type">' + esc(nodeType) + '</span>'
|
|
856
|
+
+ relation + alias + idx
|
|
857
|
+
+ '</div>';
|
|
858
|
+
|
|
859
|
+
// Key metrics row
|
|
860
|
+
var metrics = [];
|
|
861
|
+
if (node['Startup Cost'] != null) metrics.push('cost=' + node['Startup Cost'] + '..' + node['Total Cost']);
|
|
862
|
+
if (node['Plan Rows'] != null) metrics.push('rows=' + node['Plan Rows']);
|
|
863
|
+
if (node['Plan Width'] != null) metrics.push('width=' + node['Plan Width']);
|
|
864
|
+
if (node['Filter']) metrics.push('filter: ' + esc(node['Filter']));
|
|
865
|
+
if (node['Index Cond']) metrics.push('cond: ' + esc(node['Index Cond']));
|
|
866
|
+
if (node['Hash Cond']) metrics.push('hash: ' + esc(node['Hash Cond']));
|
|
867
|
+
if (node['Join Type']) metrics.push('join: ' + esc(node['Join Type']));
|
|
868
|
+
if (node['Sort Key']) metrics.push('sort: ' + esc(Array.isArray(node['Sort Key']) ? node['Sort Key'].join(', ') : node['Sort Key']));
|
|
869
|
+
|
|
870
|
+
if (metrics.length > 0) {
|
|
871
|
+
html += '<div class="ss-dash-explain-metrics">' + metrics.join(' · ') + '</div>';
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Recurse into child plans
|
|
875
|
+
var plans = node['Plans'] || [];
|
|
876
|
+
for (var i = 0; i < plans.length; i++) {
|
|
877
|
+
html += renderPlanNode(plans[i], depth + 1);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
html += '</div>';
|
|
881
|
+
return html;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
var renderExplainPlan = function (plan) {
|
|
885
|
+
if (!plan || !Array.isArray(plan) || plan.length === 0) {
|
|
886
|
+
return '<div class="ss-dash-explain-result">No plan data returned</div>';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// JSON format: array of objects with a "Plan" key
|
|
890
|
+
var topPlan = plan[0];
|
|
891
|
+
if (topPlan && topPlan['Plan']) {
|
|
892
|
+
return '<div class="ss-dash-explain-result">' + renderPlanNode(topPlan['Plan'], 0) + '</div>';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Fallback: plain rows table (for non-JSON EXPLAIN output)
|
|
896
|
+
if (typeof topPlan === 'object') {
|
|
897
|
+
var cols = Object.keys(topPlan);
|
|
898
|
+
var tbl = '<table><thead><tr>';
|
|
899
|
+
cols.forEach(function (c) { tbl += '<th>' + esc(c) + '</th>'; });
|
|
900
|
+
tbl += '</tr></thead><tbody>';
|
|
901
|
+
plan.forEach(function (r) {
|
|
902
|
+
tbl += '<tr>';
|
|
903
|
+
cols.forEach(function (c) { tbl += '<td>' + esc(r[c] != null ? String(r[c]) : '-') + '</td>'; });
|
|
904
|
+
tbl += '</tr>';
|
|
905
|
+
});
|
|
906
|
+
tbl += '</tbody></table>';
|
|
907
|
+
return '<div class="ss-dash-explain-result">' + tbl + '</div>';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return '<div class="ss-dash-explain-result">No plan data returned</div>';
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// EXPLAIN buttons
|
|
914
|
+
var bindExplainButtons = function () {
|
|
915
|
+
var body = document.getElementById('ss-dash-queries-body');
|
|
916
|
+
if (!body) return;
|
|
917
|
+
body.querySelectorAll('.ss-dash-explain-btn').forEach(function (btn) {
|
|
918
|
+
btn.addEventListener('click', function (e) {
|
|
919
|
+
e.stopPropagation();
|
|
920
|
+
var id = btn.getAttribute('data-query-id');
|
|
921
|
+
var row = btn.closest('tr');
|
|
922
|
+
if (!row) return;
|
|
923
|
+
|
|
924
|
+
// Toggle: if already shown, remove it
|
|
925
|
+
var existing = row.nextElementSibling;
|
|
926
|
+
if (existing && existing.classList.contains('ss-dash-explain-row')) {
|
|
927
|
+
existing.remove();
|
|
928
|
+
btn.classList.remove('ss-dash-explain-btn-active');
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
btn.textContent = '...';
|
|
933
|
+
btn.disabled = true;
|
|
934
|
+
fetchJSON(API + '/queries/' + id + '/explain')
|
|
935
|
+
.then(function (data) {
|
|
936
|
+
// Remove any existing explain row
|
|
937
|
+
var prev = row.nextElementSibling;
|
|
938
|
+
if (prev && prev.classList.contains('ss-dash-explain-row')) prev.remove();
|
|
939
|
+
|
|
940
|
+
var tr = document.createElement('tr');
|
|
941
|
+
tr.className = 'ss-dash-explain-row';
|
|
942
|
+
var td = document.createElement('td');
|
|
943
|
+
td.colSpan = 8;
|
|
944
|
+
td.className = 'ss-dash-explain';
|
|
945
|
+
|
|
946
|
+
if (data.error) {
|
|
947
|
+
td.innerHTML = '<div class="ss-dash-explain-result ss-dash-explain-error">'
|
|
948
|
+
+ '<strong>Error:</strong> ' + esc(data.error)
|
|
949
|
+
+ (data.message ? '<br>' + esc(data.message) : '')
|
|
950
|
+
+ '</div>';
|
|
951
|
+
} else {
|
|
952
|
+
td.innerHTML = renderExplainPlan(data.plan || data.rows || []);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
tr.appendChild(td);
|
|
956
|
+
row.parentNode.insertBefore(tr, row.nextSibling);
|
|
957
|
+
btn.textContent = 'EXPLAIN';
|
|
958
|
+
btn.disabled = false;
|
|
959
|
+
btn.classList.add('ss-dash-explain-btn-active');
|
|
960
|
+
})
|
|
961
|
+
.catch(function (err) {
|
|
962
|
+
btn.textContent = 'EXPLAIN';
|
|
963
|
+
btn.disabled = false;
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// Grouped toggle
|
|
970
|
+
var queryGroupBtn = document.getElementById('ss-dash-queries-group-btn');
|
|
971
|
+
if (queryGroupBtn) {
|
|
972
|
+
queryGroupBtn.addEventListener('click', function () {
|
|
973
|
+
queryGrouped = !queryGrouped;
|
|
974
|
+
queryGroupBtn.classList.toggle('ss-dash-active', queryGrouped);
|
|
975
|
+
queryGroupBtn.textContent = queryGrouped ? 'List View' : 'Grouped';
|
|
976
|
+
fetchQueries();
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ── Events ────────────────────────────────────────────────────
|
|
981
|
+
var fetchEvents = function () {
|
|
982
|
+
var ps = getPage('events');
|
|
983
|
+
fetchJSON(API + '/events?page=' + ps.page + '&limit=' + PER_PAGE)
|
|
984
|
+
.then(function (data) { renderEvents(data); })
|
|
985
|
+
.catch(function () { setInner('ss-dash-events-body', '<div class="ss-dash-empty">Failed to load events</div>'); });
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
var renderEvents = function (data) {
|
|
989
|
+
var items = data.data || data.events || [];
|
|
990
|
+
var ps = getPage('events');
|
|
991
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
992
|
+
|
|
993
|
+
setInner('ss-dash-events-summary', ps.total + ' events');
|
|
994
|
+
|
|
995
|
+
if (items.length === 0) {
|
|
996
|
+
setInner('ss-dash-events-body', '<div class="ss-dash-empty">No events recorded yet</div>');
|
|
997
|
+
renderPagination('events', ps);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1002
|
+
+ '<th style="width:50px">#</th>'
|
|
1003
|
+
+ '<th style="width:200px">Event</th>'
|
|
1004
|
+
+ '<th>Data</th>'
|
|
1005
|
+
+ '<th style="width:80px">Time</th>'
|
|
1006
|
+
+ '</tr></thead><tbody>';
|
|
1007
|
+
|
|
1008
|
+
items.forEach(function (ev, idx) {
|
|
1009
|
+
var hasData = ev.data && ev.data !== '-';
|
|
1010
|
+
var preview = hasData ? eventPreview(ev.data) : '-';
|
|
1011
|
+
var evName = ev.event_name || ev.event || '';
|
|
1012
|
+
html += '<tr>'
|
|
1013
|
+
+ '<td style="color:var(--ss-dim)">' + ev.id + '</td>'
|
|
1014
|
+
+ '<td class="ss-dash-event-name" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(evName) + '">' + esc(evName) + '</td>'
|
|
1015
|
+
+ '<td class="ss-dash-event-data" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'
|
|
1016
|
+
+ (hasData
|
|
1017
|
+
? '<span class="ss-dash-data-preview" data-ev-idx="' + idx + '">' + esc(preview) + '</span>'
|
|
1018
|
+
+ '<pre class="ss-dash-data-full" id="ss-dash-evdata-' + idx + '" style="display:none">' + esc(typeof ev.data === 'string' ? ev.data : JSON.stringify(ev.data, null, 2)) + '</pre>'
|
|
1019
|
+
: '<span style="color:var(--ss-dim)">-</span>')
|
|
1020
|
+
+ '</td>'
|
|
1021
|
+
+ '<td class="ss-dash-event-time">' + timeAgo(ev.createdAt || ev.created_at || ev.timestamp) + '</td>'
|
|
1022
|
+
+ '</tr>';
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
html += '</tbody></table>';
|
|
1026
|
+
setInner('ss-dash-events-body', html);
|
|
1027
|
+
renderPagination('events', ps);
|
|
1028
|
+
bindDataExpand('ss-dash-events-body');
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// ── Routes ────────────────────────────────────────────────────
|
|
1032
|
+
var fetchRoutes = function () {
|
|
1033
|
+
fetchJSON(API + '/routes')
|
|
1034
|
+
.then(function (data) {
|
|
1035
|
+
sectionLoaded.routes = true;
|
|
1036
|
+
renderRoutes(data);
|
|
1037
|
+
})
|
|
1038
|
+
.catch(function () { setInner('ss-dash-routes-body', '<div class="ss-dash-empty">Failed to load routes</div>'); });
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
var renderRoutes = function (data) {
|
|
1042
|
+
var items = data.routes || data.data || [];
|
|
1043
|
+
setInner('ss-dash-routes-summary', items.length + ' routes');
|
|
1044
|
+
|
|
1045
|
+
if (items.length === 0) {
|
|
1046
|
+
setInner('ss-dash-routes-body', '<div class="ss-dash-empty">No routes available</div>');
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1051
|
+
+ '<th style="width:70px">Method</th>'
|
|
1052
|
+
+ '<th style="width:25%">Pattern</th>'
|
|
1053
|
+
+ '<th style="width:18%">Name</th>'
|
|
1054
|
+
+ '<th style="width:32%">Handler</th>'
|
|
1055
|
+
+ '<th style="width:120px">Middleware</th>'
|
|
1056
|
+
+ '</tr></thead><tbody>';
|
|
1057
|
+
|
|
1058
|
+
var cellTrunc = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
|
1059
|
+
items.forEach(function (r) {
|
|
1060
|
+
html += '<tr>'
|
|
1061
|
+
+ '<td><span class="' + methodClass(r.method) + '">' + esc(r.method) + '</span></td>'
|
|
1062
|
+
+ '<td style="color:var(--ss-text);' + cellTrunc + '" title="' + esc(r.pattern) + '">' + esc(r.pattern) + '</td>'
|
|
1063
|
+
+ '<td style="color:var(--ss-muted);' + cellTrunc + '" title="' + esc(r.name || '-') + '">' + esc(r.name || '-') + '</td>'
|
|
1064
|
+
+ '<td style="color:var(--ss-handler-color);' + cellTrunc + '" title="' + esc(r.handler) + '">' + esc(r.handler) + '</td>'
|
|
1065
|
+
+ '<td style="color:var(--ss-dim);font-size:10px;' + cellTrunc + '" title="' + (r.middleware && r.middleware.length ? esc(r.middleware.join(', ')) : '-') + '">' + (r.middleware && r.middleware.length ? esc(r.middleware.join(', ')) : '-') + '</td>'
|
|
1066
|
+
+ '</tr>';
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
html += '</tbody></table>';
|
|
1070
|
+
setInner('ss-dash-routes-body', html);
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// ── Logs ──────────────────────────────────────────────────────
|
|
1074
|
+
var logLevelFilter = 'all';
|
|
1075
|
+
var logReqIdFilter = '';
|
|
1076
|
+
var logStructuredFilters = [];
|
|
1077
|
+
var logSavedFilters = [];
|
|
1078
|
+
|
|
1079
|
+
var fetchLogs = function () {
|
|
1080
|
+
var ps = getPage('logs');
|
|
1081
|
+
var params = 'page=' + ps.page + '&limit=' + PER_PAGE;
|
|
1082
|
+
if (logLevelFilter !== 'all') params += '&level=' + logLevelFilter;
|
|
1083
|
+
if (logReqIdFilter) params += '&request_id=' + encodeURIComponent(logReqIdFilter);
|
|
1084
|
+
logStructuredFilters.forEach(function (f) {
|
|
1085
|
+
params += '&filter_field=' + encodeURIComponent(f.field)
|
|
1086
|
+
+ '&filter_op=' + encodeURIComponent(f.op)
|
|
1087
|
+
+ '&filter_value=' + encodeURIComponent(f.value);
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
fetchJSON(API + '/logs?' + params)
|
|
1091
|
+
.then(function (data) { renderLogs(data); })
|
|
1092
|
+
.catch(function () { setInner('ss-dash-logs-body', '<div class="ss-dash-empty">Failed to load logs</div>'); });
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
var renderLogs = function (data) {
|
|
1096
|
+
var items = data.data || data.logs || data.entries || [];
|
|
1097
|
+
var ps = getPage('logs');
|
|
1098
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
1099
|
+
|
|
1100
|
+
if (items.length === 0) {
|
|
1101
|
+
var hint = '';
|
|
1102
|
+
if (logReqIdFilter) hint = ' matching request ' + logReqIdFilter;
|
|
1103
|
+
else if (logLevelFilter !== 'all') hint = ' for ' + logLevelFilter;
|
|
1104
|
+
setInner('ss-dash-logs-body', '<div class="ss-dash-empty">No log entries' + hint + '</div>');
|
|
1105
|
+
renderPagination('logs', ps);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
var html = '';
|
|
1110
|
+
items.forEach(function (e) {
|
|
1111
|
+
var level = (e.level || e.levelName || e.level_name || 'info').toLowerCase();
|
|
1112
|
+
var msg = e.message || e.msg || '';
|
|
1113
|
+
var ts = e.createdAt || e.created_at || e.time || e.timestamp || 0;
|
|
1114
|
+
var reqId = e.request_id || e['x-request-id'] || '';
|
|
1115
|
+
|
|
1116
|
+
html += '<div class="ss-dash-log-entry">'
|
|
1117
|
+
+ '<span class="ss-dash-log-level ss-dash-log-level-' + esc(level) + '">' + esc(level.toUpperCase()) + '</span>'
|
|
1118
|
+
+ '<span class="ss-dash-log-time">' + (ts ? formatTime(ts) : '-') + '</span>'
|
|
1119
|
+
+ (reqId
|
|
1120
|
+
? '<span class="ss-dash-log-reqid" data-reqid="' + esc(reqId) + '" title="' + esc(reqId) + '">' + esc(shortReqId(reqId)) + '</span>'
|
|
1121
|
+
: '<span class="ss-dash-log-reqid-empty">-</span>')
|
|
1122
|
+
+ '<span class="ss-dash-log-msg">' + esc(msg) + '</span>'
|
|
1123
|
+
+ '</div>';
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
setInner('ss-dash-logs-body', html);
|
|
1127
|
+
updateBadge('logs', ps.total);
|
|
1128
|
+
renderPagination('logs', ps);
|
|
1129
|
+
|
|
1130
|
+
// Click request ID to filter
|
|
1131
|
+
var logBody = document.getElementById('ss-dash-logs-body');
|
|
1132
|
+
if (logBody) {
|
|
1133
|
+
logBody.querySelectorAll('.ss-dash-log-reqid').forEach(function (el) {
|
|
1134
|
+
el.addEventListener('click', function () {
|
|
1135
|
+
logReqIdFilter = el.getAttribute('data-reqid') || '';
|
|
1136
|
+
var input = document.getElementById('ss-dash-log-reqid-input');
|
|
1137
|
+
if (input) input.value = logReqIdFilter;
|
|
1138
|
+
var clearBtn = document.getElementById('ss-dash-log-reqid-clear');
|
|
1139
|
+
if (clearBtn) clearBtn.style.display = logReqIdFilter ? '' : 'none';
|
|
1140
|
+
getPage('logs').page = 1;
|
|
1141
|
+
fetchLogs();
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
// Log level filters
|
|
1148
|
+
root.querySelectorAll('[data-ss-log-level]').forEach(function (btn) {
|
|
1149
|
+
btn.addEventListener('click', function () {
|
|
1150
|
+
root.querySelectorAll('[data-ss-log-level]').forEach(function (b) { b.classList.remove('ss-dash-active'); });
|
|
1151
|
+
btn.classList.add('ss-dash-active');
|
|
1152
|
+
logLevelFilter = btn.getAttribute('data-ss-log-level');
|
|
1153
|
+
getPage('logs').page = 1;
|
|
1154
|
+
fetchLogs();
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Log request ID filter
|
|
1159
|
+
var logReqIdInput = document.getElementById('ss-dash-log-reqid-input');
|
|
1160
|
+
var logReqIdClear = document.getElementById('ss-dash-log-reqid-clear');
|
|
1161
|
+
if (logReqIdInput) {
|
|
1162
|
+
logReqIdInput.addEventListener('input', function () {
|
|
1163
|
+
logReqIdFilter = logReqIdInput.value.trim();
|
|
1164
|
+
if (logReqIdClear) logReqIdClear.style.display = logReqIdFilter ? '' : 'none';
|
|
1165
|
+
getPage('logs').page = 1;
|
|
1166
|
+
fetchLogs();
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (logReqIdClear) {
|
|
1170
|
+
logReqIdClear.addEventListener('click', function () {
|
|
1171
|
+
logReqIdFilter = '';
|
|
1172
|
+
if (logReqIdInput) logReqIdInput.value = '';
|
|
1173
|
+
logReqIdClear.style.display = 'none';
|
|
1174
|
+
getPage('logs').page = 1;
|
|
1175
|
+
fetchLogs();
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Structured search: add filter
|
|
1180
|
+
var logAddFilterBtn = document.getElementById('ss-dash-log-add-filter');
|
|
1181
|
+
if (logAddFilterBtn) {
|
|
1182
|
+
logAddFilterBtn.addEventListener('click', function () {
|
|
1183
|
+
var fieldEl = document.getElementById('ss-dash-log-filter-field');
|
|
1184
|
+
var opEl = document.getElementById('ss-dash-log-filter-op');
|
|
1185
|
+
var valEl = document.getElementById('ss-dash-log-filter-value');
|
|
1186
|
+
if (!fieldEl || !opEl || !valEl) return;
|
|
1187
|
+
var field = fieldEl.value;
|
|
1188
|
+
var op = opEl.value;
|
|
1189
|
+
var val = valEl.value.trim();
|
|
1190
|
+
if (!field || !val) return;
|
|
1191
|
+
logStructuredFilters.push({ field: field, op: op, value: val });
|
|
1192
|
+
valEl.value = '';
|
|
1193
|
+
renderFilterChips();
|
|
1194
|
+
getPage('logs').page = 1;
|
|
1195
|
+
fetchLogs();
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
var renderFilterChips = function () {
|
|
1200
|
+
var container = document.getElementById('ss-dash-log-filter-chips');
|
|
1201
|
+
if (!container) return;
|
|
1202
|
+
var html = '';
|
|
1203
|
+
logStructuredFilters.forEach(function (f, i) {
|
|
1204
|
+
html += '<span class="ss-dash-filter-chip">'
|
|
1205
|
+
+ esc(f.field) + ' ' + esc(f.op) + ' ' + esc(f.value)
|
|
1206
|
+
+ ' <button class="ss-dash-filter-chip-remove" data-chip-idx="' + i + '">×</button>'
|
|
1207
|
+
+ '</span>';
|
|
1208
|
+
});
|
|
1209
|
+
container.innerHTML = html;
|
|
1210
|
+
container.querySelectorAll('.ss-dash-filter-chip-remove').forEach(function (btn) {
|
|
1211
|
+
btn.addEventListener('click', function () {
|
|
1212
|
+
var idx = parseInt(btn.getAttribute('data-chip-idx'), 10);
|
|
1213
|
+
logStructuredFilters.splice(idx, 1);
|
|
1214
|
+
renderFilterChips();
|
|
1215
|
+
getPage('logs').page = 1;
|
|
1216
|
+
fetchLogs();
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Saved filters
|
|
1222
|
+
var fetchSavedFilters = function () {
|
|
1223
|
+
fetchJSON(API + '/filters?section=logs')
|
|
1224
|
+
.then(function (data) {
|
|
1225
|
+
logSavedFilters = data.filters || data.data || [];
|
|
1226
|
+
renderSavedFilters();
|
|
1227
|
+
})
|
|
1228
|
+
.catch(function () { /* ignore */ });
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
var renderSavedFilters = function () {
|
|
1232
|
+
var sel = document.getElementById('ss-dash-log-saved-select');
|
|
1233
|
+
if (!sel) return;
|
|
1234
|
+
var html = '<option value="">Saved Filters...</option>';
|
|
1235
|
+
logSavedFilters.forEach(function (f) {
|
|
1236
|
+
html += '<option value="' + f.id + '">' + esc(f.name) + '</option>';
|
|
1237
|
+
});
|
|
1238
|
+
sel.innerHTML = html;
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
var savedFilterSelect = document.getElementById('ss-dash-log-saved-select');
|
|
1242
|
+
if (savedFilterSelect) {
|
|
1243
|
+
savedFilterSelect.addEventListener('change', function () {
|
|
1244
|
+
var id = savedFilterSelect.value;
|
|
1245
|
+
if (!id) return;
|
|
1246
|
+
var filter = logSavedFilters.find(function (f) { return String(f.id) === id; });
|
|
1247
|
+
if (filter && filter.filter_config) {
|
|
1248
|
+
try {
|
|
1249
|
+
var cfg = typeof filter.filter_config === 'string' ? JSON.parse(filter.filter_config) : filter.filter_config;
|
|
1250
|
+
if (cfg.level) logLevelFilter = cfg.level;
|
|
1251
|
+
if (cfg.filters) logStructuredFilters = cfg.filters;
|
|
1252
|
+
renderFilterChips();
|
|
1253
|
+
getPage('logs').page = 1;
|
|
1254
|
+
fetchLogs();
|
|
1255
|
+
} catch (e) { /* ignore */ }
|
|
1256
|
+
}
|
|
1257
|
+
savedFilterSelect.value = '';
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
var saveFilterBtn = document.getElementById('ss-dash-log-save-filter');
|
|
1262
|
+
if (saveFilterBtn) {
|
|
1263
|
+
saveFilterBtn.addEventListener('click', function () {
|
|
1264
|
+
var name = prompt('Filter preset name:');
|
|
1265
|
+
if (!name) return;
|
|
1266
|
+
var config = {
|
|
1267
|
+
level: logLevelFilter,
|
|
1268
|
+
requestId: logReqIdFilter,
|
|
1269
|
+
filters: logStructuredFilters
|
|
1270
|
+
};
|
|
1271
|
+
fetch(API + '/filters', {
|
|
1272
|
+
method: 'POST',
|
|
1273
|
+
credentials: 'same-origin',
|
|
1274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1275
|
+
body: JSON.stringify({ name: name, section: 'logs', filter_config: config })
|
|
1276
|
+
}).then(function () { fetchSavedFilters(); }).catch(function () { /* ignore */ });
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
var deleteFilterBtn = document.getElementById('ss-dash-log-delete-filter');
|
|
1281
|
+
if (deleteFilterBtn) {
|
|
1282
|
+
deleteFilterBtn.addEventListener('click', function () {
|
|
1283
|
+
var sel = document.getElementById('ss-dash-log-saved-select');
|
|
1284
|
+
if (!sel || !sel.value) return;
|
|
1285
|
+
fetch(API + '/filters/' + sel.value, { method: 'DELETE', credentials: 'same-origin' })
|
|
1286
|
+
.then(function () { fetchSavedFilters(); }).catch(function () { /* ignore */ });
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
fetchSavedFilters();
|
|
1291
|
+
|
|
1292
|
+
// ── Emails ────────────────────────────────────────────────────
|
|
1293
|
+
var fetchEmails = function () {
|
|
1294
|
+
var ps = getPage('emails');
|
|
1295
|
+
fetchJSON(API + '/emails?page=' + ps.page + '&limit=' + PER_PAGE)
|
|
1296
|
+
.then(function (data) { renderEmails(data); })
|
|
1297
|
+
.catch(function () { setInner('ss-dash-emails-body', '<div class="ss-dash-empty">Failed to load emails</div>'); });
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
var renderEmails = function (data) {
|
|
1301
|
+
var items = data.data || data.emails || [];
|
|
1302
|
+
var ps = getPage('emails');
|
|
1303
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
1304
|
+
|
|
1305
|
+
setInner('ss-dash-emails-summary', ps.total + ' emails');
|
|
1306
|
+
|
|
1307
|
+
if (items.length === 0) {
|
|
1308
|
+
setInner('ss-dash-emails-body', '<div class="ss-dash-empty">No emails captured yet</div>');
|
|
1309
|
+
renderPagination('emails', ps);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
var CT = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
|
1314
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1315
|
+
+ '<th style="width:40px">#</th>'
|
|
1316
|
+
+ '<th style="width:160px">From</th>'
|
|
1317
|
+
+ '<th style="width:160px">To</th>'
|
|
1318
|
+
+ '<th>Subject</th>'
|
|
1319
|
+
+ '<th style="width:70px">Status</th>'
|
|
1320
|
+
+ '<th style="width:70px">Mailer</th>'
|
|
1321
|
+
+ '<th style="width:30px" title="Attachments">ATT</th>'
|
|
1322
|
+
+ '<th style="width:60px">Time</th>'
|
|
1323
|
+
+ '</tr></thead><tbody>';
|
|
1324
|
+
|
|
1325
|
+
items.forEach(function (e) {
|
|
1326
|
+
var fromAddr = e.from_addr || e.from || '';
|
|
1327
|
+
var toAddr = e.to_addr || e.to || '';
|
|
1328
|
+
html += '<tr class="ss-dash-email-row" data-email-id="' + e.id + '">'
|
|
1329
|
+
+ '<td style="color:var(--ss-dim)">' + e.id + '</td>'
|
|
1330
|
+
+ '<td style="color:var(--ss-text-secondary);' + CT + '" title="' + esc(fromAddr) + '">' + esc(fromAddr) + '</td>'
|
|
1331
|
+
+ '<td style="color:var(--ss-text-secondary);' + CT + '" title="' + esc(toAddr) + '">' + esc(toAddr) + '</td>'
|
|
1332
|
+
+ '<td style="color:var(--ss-sql-color);' + CT + '" title="' + esc(e.subject || '') + '">' + esc(e.subject || '') + '</td>'
|
|
1333
|
+
+ '<td><span class="ss-dash-email-status ss-dash-email-status-' + esc(e.status || '') + '">' + esc(e.status || '') + '</span></td>'
|
|
1334
|
+
+ '<td style="color:var(--ss-muted);' + CT + '">' + esc(e.mailer || '') + '</td>'
|
|
1335
|
+
+ '<td style="color:var(--ss-dim);text-align:center">' + ((e.attachment_count || e.attachmentCount || 0) > 0 ? (e.attachment_count || e.attachmentCount) : '-') + '</td>'
|
|
1336
|
+
+ '<td class="ss-dash-event-time" style="white-space:nowrap">' + timeAgo(e.createdAt || e.created_at || e.timestamp) + '</td>'
|
|
1337
|
+
+ '</tr>';
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
html += '</tbody></table>';
|
|
1341
|
+
setInner('ss-dash-emails-body', html);
|
|
1342
|
+
renderPagination('emails', ps);
|
|
1343
|
+
|
|
1344
|
+
var body = document.getElementById('ss-dash-emails-body');
|
|
1345
|
+
if (body) {
|
|
1346
|
+
body.querySelectorAll('.ss-dash-email-row').forEach(function (row) {
|
|
1347
|
+
row.addEventListener('click', function () {
|
|
1348
|
+
var id = row.getAttribute('data-email-id');
|
|
1349
|
+
showEmailPreview(id, items);
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
var showEmailPreview = function (id, emails) {
|
|
1356
|
+
var previewEl = document.getElementById('ss-dash-email-preview');
|
|
1357
|
+
var metaEl = document.getElementById('ss-dash-email-preview-meta');
|
|
1358
|
+
var iframeEl = document.getElementById('ss-dash-email-iframe');
|
|
1359
|
+
if (!previewEl || !iframeEl) return;
|
|
1360
|
+
|
|
1361
|
+
var email = emails.find(function (e) { return String(e.id) === String(id); });
|
|
1362
|
+
if (metaEl && email) {
|
|
1363
|
+
metaEl.innerHTML =
|
|
1364
|
+
'<strong>Subject:</strong> ' + esc(email.subject || '')
|
|
1365
|
+
+ ' | <strong>From:</strong> ' + esc(email.from_addr || email.from || '')
|
|
1366
|
+
+ ' | <strong>To:</strong> ' + esc(email.to_addr || email.to || '')
|
|
1367
|
+
+ (email.cc ? ' | <strong>CC:</strong> ' + esc(email.cc) : '')
|
|
1368
|
+
+ ' | <strong>Status:</strong> <span class="ss-dash-email-status ss-dash-email-status-' + esc(email.status || '') + '">' + esc(email.status || '') + '</span>'
|
|
1369
|
+
+ ' | <strong>Mailer:</strong> ' + esc(email.mailer || '');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
iframeEl.src = API + '/emails/' + id + '/preview';
|
|
1373
|
+
previewEl.style.display = 'flex';
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
var emailPreviewClose = document.getElementById('ss-dash-email-preview-close');
|
|
1377
|
+
if (emailPreviewClose) {
|
|
1378
|
+
emailPreviewClose.addEventListener('click', function () {
|
|
1379
|
+
var previewEl = document.getElementById('ss-dash-email-preview');
|
|
1380
|
+
var iframeEl = document.getElementById('ss-dash-email-iframe');
|
|
1381
|
+
if (previewEl) previewEl.style.display = 'none';
|
|
1382
|
+
if (iframeEl) iframeEl.src = 'about:blank';
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// ── Timeline / Traces ─────────────────────────────────────────
|
|
1387
|
+
var fetchTraces = function () {
|
|
1388
|
+
if (!tracingEnabled) {
|
|
1389
|
+
setInner('ss-dash-timeline-body', '<div class="ss-dash-empty">Tracing is not enabled. Set tracing: true in config.</div>');
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
var ps = getPage('timeline');
|
|
1393
|
+
fetchJSON(API + '/traces?page=' + ps.page + '&limit=' + PER_PAGE)
|
|
1394
|
+
.then(function (data) { renderTraces(data); })
|
|
1395
|
+
.catch(function () { setInner('ss-dash-timeline-body', '<div class="ss-dash-empty">Failed to load traces</div>'); });
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
var renderTraces = function (data) {
|
|
1399
|
+
var items = data.data || data.traces || [];
|
|
1400
|
+
var ps = getPage('timeline');
|
|
1401
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
1402
|
+
|
|
1403
|
+
setInner('ss-dash-timeline-summary', ps.total + ' requests');
|
|
1404
|
+
|
|
1405
|
+
if (items.length === 0) {
|
|
1406
|
+
setInner('ss-dash-timeline-body', '<div class="ss-dash-empty">No requests traced yet</div>');
|
|
1407
|
+
renderPagination('timeline', ps);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1412
|
+
+ '<th style="width:50px">#</th>'
|
|
1413
|
+
+ '<th style="width:60px">Method</th>'
|
|
1414
|
+
+ '<th>URL</th>'
|
|
1415
|
+
+ '<th style="width:55px">Status</th>'
|
|
1416
|
+
+ '<th style="width:80px">Duration</th>'
|
|
1417
|
+
+ '<th style="width:50px">Spans</th>'
|
|
1418
|
+
+ '<th style="width:60px">Time</th>'
|
|
1419
|
+
+ '</tr></thead><tbody>';
|
|
1420
|
+
|
|
1421
|
+
items.forEach(function (t) {
|
|
1422
|
+
html += '<tr class="ss-dash-clickable" data-trace-id="' + t.id + '">'
|
|
1423
|
+
+ '<td style="color:var(--ss-dim)">' + t.id + '</td>'
|
|
1424
|
+
+ '<td><span class="' + methodClass(t.method) + '">' + esc(t.method) + '</span></td>'
|
|
1425
|
+
+ '<td style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ss-text)" title="' + esc(t.url) + '">' + esc(t.url) + '</td>'
|
|
1426
|
+
+ '<td><span class="ss-dash-status ' + statusClass(t.status_code || t.statusCode) + '">' + (t.status_code || t.statusCode) + '</span></td>'
|
|
1427
|
+
+ '<td class="ss-dash-duration ' + durationClass(t.total_duration || t.totalDuration) + '">' + (t.total_duration || t.totalDuration || 0).toFixed(1) + 'ms</td>'
|
|
1428
|
+
+ '<td style="color:var(--ss-muted);text-align:center">' + (t.span_count || t.spanCount || 0) + '</td>'
|
|
1429
|
+
+ '<td class="ss-dash-event-time" style="white-space:nowrap">' + timeAgo(t.createdAt || t.created_at || t.timestamp) + '</td>'
|
|
1430
|
+
+ '</tr>';
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
html += '</tbody></table>';
|
|
1434
|
+
setInner('ss-dash-timeline-body', html);
|
|
1435
|
+
renderPagination('timeline', ps);
|
|
1436
|
+
|
|
1437
|
+
var body = document.getElementById('ss-dash-timeline-body');
|
|
1438
|
+
if (body) {
|
|
1439
|
+
body.querySelectorAll('[data-trace-id]').forEach(function (row) {
|
|
1440
|
+
row.addEventListener('click', function () {
|
|
1441
|
+
var id = row.getAttribute('data-trace-id');
|
|
1442
|
+
fetchJSON(API + '/traces/' + id)
|
|
1443
|
+
.then(function (trace) { showTraceDetail(trace); })
|
|
1444
|
+
.catch(function () { /* ignore */ });
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
var showTraceDetail = function (trace) {
|
|
1451
|
+
var listEl = document.getElementById('ss-dash-timeline-list');
|
|
1452
|
+
var detailEl = document.getElementById('ss-dash-timeline-detail');
|
|
1453
|
+
var titleEl = document.getElementById('ss-dash-timeline-detail-title');
|
|
1454
|
+
var waterfallEl = document.getElementById('ss-dash-timeline-waterfall');
|
|
1455
|
+
if (!listEl || !detailEl) return;
|
|
1456
|
+
|
|
1457
|
+
listEl.style.display = 'none';
|
|
1458
|
+
detailEl.style.display = 'flex';
|
|
1459
|
+
detailEl.classList.add('ss-dash-active');
|
|
1460
|
+
|
|
1461
|
+
if (titleEl) {
|
|
1462
|
+
titleEl.innerHTML = '<span class="' + methodClass(trace.method) + '">' + esc(trace.method) + '</span> '
|
|
1463
|
+
+ esc(trace.url) + ' '
|
|
1464
|
+
+ '<span class="ss-dash-status ' + statusClass(trace.status_code || trace.statusCode) + '">' + (trace.status_code || trace.statusCode) + '</span>'
|
|
1465
|
+
+ '<span class="ss-dash-tl-meta">' + (trace.total_duration || trace.totalDuration || 0).toFixed(1) + 'ms · '
|
|
1466
|
+
+ (trace.span_count || trace.spanCount || 0) + ' spans</span>';
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (waterfallEl) renderWaterfall(waterfallEl, trace);
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
var timelineBackBtn = document.getElementById('ss-dash-timeline-back');
|
|
1473
|
+
if (timelineBackBtn) {
|
|
1474
|
+
timelineBackBtn.addEventListener('click', function () {
|
|
1475
|
+
var listEl = document.getElementById('ss-dash-timeline-list');
|
|
1476
|
+
var detailEl = document.getElementById('ss-dash-timeline-detail');
|
|
1477
|
+
if (listEl) listEl.style.display = '';
|
|
1478
|
+
if (detailEl) { detailEl.style.display = 'none'; detailEl.classList.remove('ss-dash-active'); }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ── Waterfall renderer (shared) ───────────────────────────────
|
|
1483
|
+
var renderWaterfall = function (container, trace) {
|
|
1484
|
+
var spans = trace.spans || [];
|
|
1485
|
+
if (typeof spans === 'string') { try { spans = JSON.parse(spans); } catch (e) { spans = []; } }
|
|
1486
|
+
var total = trace.total_duration || trace.totalDuration || trace.duration || 1;
|
|
1487
|
+
|
|
1488
|
+
var html = '<div class="ss-dash-tl-legend">'
|
|
1489
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:#6d28d9"></span>DB</div>'
|
|
1490
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:#1e3a5f"></span>Request</div>'
|
|
1491
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:#059669"></span>Mail</div>'
|
|
1492
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:#b45309"></span>Event</div>'
|
|
1493
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:#0e7490"></span>View</div>'
|
|
1494
|
+
+ '<div class="ss-dash-tl-legend-item"><span class="ss-dash-tl-legend-dot" style="background:var(--ss-dim)"></span>Custom</div>'
|
|
1495
|
+
+ '</div>';
|
|
1496
|
+
|
|
1497
|
+
if (spans.length === 0) {
|
|
1498
|
+
html += '<div class="ss-dash-empty">No spans captured for this request</div>';
|
|
1499
|
+
} else {
|
|
1500
|
+
var depthMap = {};
|
|
1501
|
+
for (var i = 0; i < spans.length; i++) {
|
|
1502
|
+
var s = spans[i];
|
|
1503
|
+
depthMap[s.id] = s.parentId ? (depthMap[s.parentId] || 0) + 1 : 0;
|
|
1504
|
+
}
|
|
1505
|
+
var sorted = spans.slice().sort(function (a, b) { return a.startOffset - b.startOffset; });
|
|
1506
|
+
|
|
1507
|
+
for (var j = 0; j < sorted.length; j++) {
|
|
1508
|
+
var sp = sorted[j];
|
|
1509
|
+
var depth = depthMap[sp.id] || 0;
|
|
1510
|
+
var leftPct = (sp.startOffset / total * 100).toFixed(2);
|
|
1511
|
+
var widthPct = Math.max(sp.duration / total * 100, 0.5).toFixed(2);
|
|
1512
|
+
var indent = depth * 16;
|
|
1513
|
+
var catLabel = sp.category === 'db' ? 'DB' : sp.category;
|
|
1514
|
+
var metaStr = sp.metadata ? Object.entries(sp.metadata).filter(function (e) { return e[1] != null; }).map(function (e) { return e[0] + '=' + e[1]; }).join(', ') : '';
|
|
1515
|
+
var tooltip = sp.label + ' (' + sp.duration.toFixed(2) + 'ms)' + (metaStr ? '\n' + metaStr : '');
|
|
1516
|
+
|
|
1517
|
+
var badgeCat = sp.category === 'db' ? 'purple' : sp.category === 'mail' ? 'green' : sp.category === 'event' ? 'amber' : sp.category === 'view' ? 'blue' : 'muted';
|
|
1518
|
+
|
|
1519
|
+
html += '<div class="ss-dash-tl-row">'
|
|
1520
|
+
+ '<div class="ss-dash-tl-label" style="padding-left:' + (8 + indent) + 'px" title="' + esc(tooltip) + '">'
|
|
1521
|
+
+ '<span class="ss-dash-badge ss-dash-badge-' + badgeCat + '" style="font-size:9px;margin-right:4px">' + esc(catLabel) + '</span>'
|
|
1522
|
+
+ esc(sp.label.length > 50 ? sp.label.slice(0, 50) + '...' : sp.label)
|
|
1523
|
+
+ '</div>'
|
|
1524
|
+
+ '<div class="ss-dash-tl-track">'
|
|
1525
|
+
+ '<div class="ss-dash-tl-bar ss-dash-tl-bar-' + esc(sp.category) + '" style="left:' + leftPct + '%;width:' + widthPct + '%" title="' + esc(tooltip) + '"></div>'
|
|
1526
|
+
+ '</div>'
|
|
1527
|
+
+ '<span class="ss-dash-tl-dur">' + sp.duration.toFixed(2) + 'ms</span>'
|
|
1528
|
+
+ '</div>';
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Warnings
|
|
1533
|
+
var warnings = trace.warnings || [];
|
|
1534
|
+
if (typeof warnings === 'string') { try { warnings = JSON.parse(warnings); } catch (e) { warnings = []; } }
|
|
1535
|
+
if (warnings.length > 0) {
|
|
1536
|
+
html += '<div class="ss-dash-tl-warnings">'
|
|
1537
|
+
+ '<div class="ss-dash-tl-warnings-title">Warnings (' + warnings.length + ')</div>';
|
|
1538
|
+
warnings.forEach(function (w) {
|
|
1539
|
+
html += '<div class="ss-dash-tl-warning">' + esc(w) + '</div>';
|
|
1540
|
+
});
|
|
1541
|
+
html += '</div>';
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
container.innerHTML = html;
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// ── Cache ─────────────────────────────────────────────────────
|
|
1548
|
+
var fetchCache = function () {
|
|
1549
|
+
fetchJSON(API + '/cache')
|
|
1550
|
+
.then(function (data) { renderCache(data); })
|
|
1551
|
+
.catch(function () { setInner('ss-dash-cache-body', '<div class="ss-dash-empty">Cache not available</div>'); });
|
|
1552
|
+
};
|
|
1553
|
+
|
|
1554
|
+
var renderCache = function (data) {
|
|
1555
|
+
var stats = data.stats || {};
|
|
1556
|
+
var keys = data.keys || data.data || [];
|
|
1557
|
+
|
|
1558
|
+
var statsHtml = '<div class="ss-dash-cache-stats">'
|
|
1559
|
+
+ '<div class="ss-dash-cache-stat"><span class="ss-dash-cache-stat-label">Hit Rate:</span><span class="ss-dash-cache-stat-value">' + (stats.hitRate || 0).toFixed(1) + '%</span></div>'
|
|
1560
|
+
+ '<div class="ss-dash-cache-stat"><span class="ss-dash-cache-stat-label">Hits:</span><span class="ss-dash-cache-stat-value">' + (stats.hits || 0) + '</span></div>'
|
|
1561
|
+
+ '<div class="ss-dash-cache-stat"><span class="ss-dash-cache-stat-label">Misses:</span><span class="ss-dash-cache-stat-value">' + (stats.misses || 0) + '</span></div>'
|
|
1562
|
+
+ '<div class="ss-dash-cache-stat"><span class="ss-dash-cache-stat-label">Keys:</span><span class="ss-dash-cache-stat-value">' + (stats.keyCount || keys.length || 0) + '</span></div>'
|
|
1563
|
+
+ '</div>';
|
|
1564
|
+
|
|
1565
|
+
setInner('ss-dash-cache-stats-area', statsHtml);
|
|
1566
|
+
|
|
1567
|
+
if (keys.length === 0) {
|
|
1568
|
+
setInner('ss-dash-cache-body', '<div class="ss-dash-empty">No cache keys found</div>');
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1573
|
+
+ '<th>Key</th>'
|
|
1574
|
+
+ '<th style="width:80px">Type</th>'
|
|
1575
|
+
+ '<th style="width:80px">TTL</th>'
|
|
1576
|
+
+ '<th style="width:80px">Size</th>'
|
|
1577
|
+
+ '</tr></thead><tbody>';
|
|
1578
|
+
|
|
1579
|
+
keys.forEach(function (k) {
|
|
1580
|
+
html += '<tr class="ss-dash-clickable" data-cache-key="' + esc(k.key || '') + '">'
|
|
1581
|
+
+ '<td style="color:var(--ss-sql-color);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(k.key || '') + '">' + esc(k.key || '') + '</td>'
|
|
1582
|
+
+ '<td style="color:var(--ss-muted)">' + esc(k.type || '-') + '</td>'
|
|
1583
|
+
+ '<td style="color:var(--ss-muted)">' + (k.ttl != null ? k.ttl + 's' : '-') + '</td>'
|
|
1584
|
+
+ '<td style="color:var(--ss-dim)">' + (k.size != null ? k.size + 'B' : '-') + '</td>'
|
|
1585
|
+
+ '</tr>';
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
html += '</tbody></table>';
|
|
1589
|
+
setInner('ss-dash-cache-body', html);
|
|
1590
|
+
|
|
1591
|
+
var body = document.getElementById('ss-dash-cache-body');
|
|
1592
|
+
if (body) {
|
|
1593
|
+
body.querySelectorAll('[data-cache-key]').forEach(function (row) {
|
|
1594
|
+
row.addEventListener('click', function () {
|
|
1595
|
+
var key = row.getAttribute('data-cache-key');
|
|
1596
|
+
fetchJSON(API + '/cache/' + encodeURIComponent(key))
|
|
1597
|
+
.then(function (data) {
|
|
1598
|
+
setInner('ss-dash-cache-detail', '<div class="ss-dash-cache-detail"><strong>Key:</strong> ' + esc(key) + '<pre class="ss-dash-data-full" style="display:block">' + esc(JSON.stringify(data.value || data, null, 2)) + '</pre></div>');
|
|
1599
|
+
})
|
|
1600
|
+
.catch(function () { /* ignore */ });
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
// ── Jobs ──────────────────────────────────────────────────────
|
|
1607
|
+
var jobStatusFilter = '';
|
|
1608
|
+
|
|
1609
|
+
var fetchJobs = function () {
|
|
1610
|
+
var ps = getPage('jobs');
|
|
1611
|
+
var params = 'page=' + ps.page + '&limit=' + PER_PAGE;
|
|
1612
|
+
if (jobStatusFilter) params += '&status=' + jobStatusFilter;
|
|
1613
|
+
|
|
1614
|
+
fetchJSON(API + '/jobs?' + params)
|
|
1615
|
+
.then(function (data) { renderJobs(data); })
|
|
1616
|
+
.catch(function () { setInner('ss-dash-jobs-body', '<div class="ss-dash-empty">Jobs/Queue not available</div>'); });
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
var renderJobs = function (data) {
|
|
1620
|
+
var items = data.data || data.jobs || [];
|
|
1621
|
+
var stats = data.stats || {};
|
|
1622
|
+
var ps = getPage('jobs');
|
|
1623
|
+
ps.total = data.meta ? data.meta.total : (data.total || items.length);
|
|
1624
|
+
|
|
1625
|
+
var statsHtml = '<div class="ss-dash-job-stats">'
|
|
1626
|
+
+ '<div class="ss-dash-job-stat"><span class="ss-dash-job-stat-label">Active:</span><span class="ss-dash-job-stat-value">' + (stats.active || 0) + '</span></div>'
|
|
1627
|
+
+ '<div class="ss-dash-job-stat"><span class="ss-dash-job-stat-label">Waiting:</span><span class="ss-dash-job-stat-value">' + (stats.waiting || 0) + '</span></div>'
|
|
1628
|
+
+ '<div class="ss-dash-job-stat"><span class="ss-dash-job-stat-label">Delayed:</span><span class="ss-dash-job-stat-value">' + (stats.delayed || 0) + '</span></div>'
|
|
1629
|
+
+ '<div class="ss-dash-job-stat"><span class="ss-dash-job-stat-label">Completed:</span><span class="ss-dash-job-stat-value">' + (stats.completed || 0) + '</span></div>'
|
|
1630
|
+
+ '<div class="ss-dash-job-stat"><span class="ss-dash-job-stat-label">Failed:</span><span class="ss-dash-job-stat-value" style="color:var(--ss-red-fg)">' + (stats.failed || 0) + '</span></div>'
|
|
1631
|
+
+ '</div>';
|
|
1632
|
+
setInner('ss-dash-jobs-stats-area', statsHtml);
|
|
1633
|
+
|
|
1634
|
+
if (items.length === 0) {
|
|
1635
|
+
setInner('ss-dash-jobs-body', '<div class="ss-dash-empty">No jobs found</div>');
|
|
1636
|
+
renderPagination('jobs', ps);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
var CT = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
|
1641
|
+
var html = '<table class="ss-dash-table" style="table-layout:fixed"><thead><tr>'
|
|
1642
|
+
+ '<th style="width:50px">ID</th>'
|
|
1643
|
+
+ '<th style="width:160px">Name</th>'
|
|
1644
|
+
+ '<th style="width:80px">Status</th>'
|
|
1645
|
+
+ '<th>Payload</th>'
|
|
1646
|
+
+ '<th style="width:55px">Tries</th>'
|
|
1647
|
+
+ '<th style="width:75px">Duration</th>'
|
|
1648
|
+
+ '<th style="width:60px">Time</th>'
|
|
1649
|
+
+ '<th style="width:50px"></th>'
|
|
1650
|
+
+ '</tr></thead><tbody>';
|
|
1651
|
+
|
|
1652
|
+
items.forEach(function (j) {
|
|
1653
|
+
var statusBadge = j.status === 'failed' ? 'red' : j.status === 'completed' ? 'green' : j.status === 'active' ? 'blue' : 'amber';
|
|
1654
|
+
html += '<tr class="ss-dash-clickable" data-job-id="' + j.id + '">'
|
|
1655
|
+
+ '<td style="color:var(--ss-dim)">' + j.id + '</td>'
|
|
1656
|
+
+ '<td style="color:var(--ss-sql-color);' + CT + '" title="' + esc(j.name || '') + '">' + esc(j.name || '') + '</td>'
|
|
1657
|
+
+ '<td><span class="ss-dash-badge ss-dash-badge-' + statusBadge + '">' + esc(j.status || '') + '</span></td>'
|
|
1658
|
+
+ '<td style="color:var(--ss-muted);font-size:10px;' + CT + '">' + esc(j.payload ? compactPreview(j.payload, 60) : '-') + '</td>'
|
|
1659
|
+
+ '<td style="color:var(--ss-muted);text-align:center">' + (j.attempts || j.attemptsMade || 0) + '</td>'
|
|
1660
|
+
+ '<td class="ss-dash-duration">' + (j.duration != null ? j.duration.toFixed(0) + 'ms' : '-') + '</td>'
|
|
1661
|
+
+ '<td class="ss-dash-event-time" style="white-space:nowrap">' + timeAgo(j.timestamp || j.processedOn || j.created_at) + '</td>'
|
|
1662
|
+
+ '<td>' + (j.status === 'failed' ? '<button class="ss-dash-retry-btn" data-retry-id="' + j.id + '">Retry</button>' : '') + '</td>'
|
|
1663
|
+
+ '</tr>';
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
html += '</tbody></table>';
|
|
1667
|
+
setInner('ss-dash-jobs-body', html);
|
|
1668
|
+
renderPagination('jobs', ps);
|
|
1669
|
+
|
|
1670
|
+
// Retry buttons
|
|
1671
|
+
var body = document.getElementById('ss-dash-jobs-body');
|
|
1672
|
+
if (body) {
|
|
1673
|
+
body.querySelectorAll('.ss-dash-retry-btn').forEach(function (btn) {
|
|
1674
|
+
btn.addEventListener('click', function (e) {
|
|
1675
|
+
e.stopPropagation();
|
|
1676
|
+
var id = btn.getAttribute('data-retry-id');
|
|
1677
|
+
btn.textContent = '...';
|
|
1678
|
+
btn.disabled = true;
|
|
1679
|
+
fetch(API + '/jobs/' + id + '/retry', { method: 'POST', credentials: 'same-origin' })
|
|
1680
|
+
.then(function () { btn.textContent = 'OK'; setTimeout(fetchJobs, 1000); })
|
|
1681
|
+
.catch(function () { btn.textContent = 'Retry'; btn.disabled = false; });
|
|
1682
|
+
});
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
// Job status filter buttons
|
|
1688
|
+
root.querySelectorAll('[data-ss-job-status]').forEach(function (btn) {
|
|
1689
|
+
btn.addEventListener('click', function () {
|
|
1690
|
+
root.querySelectorAll('[data-ss-job-status]').forEach(function (b) { b.classList.remove('ss-dash-active'); });
|
|
1691
|
+
btn.classList.add('ss-dash-active');
|
|
1692
|
+
jobStatusFilter = btn.getAttribute('data-ss-job-status');
|
|
1693
|
+
getPage('jobs').page = 1;
|
|
1694
|
+
fetchJobs();
|
|
1695
|
+
});
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
// ── Config ────────────────────────────────────────────────────
|
|
1699
|
+
// ── Config: state ─────────────────────────────────────────────
|
|
1700
|
+
var configRawData = null;
|
|
1701
|
+
var configActiveTab = 'config';
|
|
1702
|
+
var configSearchTerm = '';
|
|
1703
|
+
|
|
1704
|
+
/** Check if a value is a redacted marker object. */
|
|
1705
|
+
var isRedactedObj = function (val) {
|
|
1706
|
+
return val && typeof val === 'object' && val.__redacted === true;
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
/** Render a redacted value with reveal/copy buttons. */
|
|
1710
|
+
var renderRedacted = function (val, prefix) {
|
|
1711
|
+
var cls = prefix + '-config-redacted';
|
|
1712
|
+
var realVal = esc(val.value || '');
|
|
1713
|
+
return '<span class="' + cls + ' ' + prefix + '-redacted-wrap" data-redacted-value="' + realVal + '">'
|
|
1714
|
+
+ '<span class="' + prefix + '-redacted-display">' + esc(val.display) + '</span>'
|
|
1715
|
+
+ '<span class="' + prefix + '-redacted-real" style="display:none">' + realVal + '</span>'
|
|
1716
|
+
+ '<button type="button" class="' + prefix + '-redacted-reveal" title="Reveal value">'
|
|
1717
|
+
+ '<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>'
|
|
1718
|
+
+ '</button>'
|
|
1719
|
+
+ '<button type="button" class="' + prefix + '-redacted-copy" title="Copy value">'
|
|
1720
|
+
+ '<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>'
|
|
1721
|
+
+ '</button>'
|
|
1722
|
+
+ '</span>';
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
/** Bind reveal/copy click handlers inside a container. */
|
|
1726
|
+
var bindRedactedButtons = function (container, prefix) {
|
|
1727
|
+
container.querySelectorAll('.' + prefix + '-redacted-reveal').forEach(function (btn) {
|
|
1728
|
+
btn.addEventListener('click', function (e) {
|
|
1729
|
+
e.stopPropagation();
|
|
1730
|
+
var wrap = btn.closest('.' + prefix + '-redacted-wrap');
|
|
1731
|
+
if (!wrap) return;
|
|
1732
|
+
var display = wrap.querySelector('.' + prefix + '-redacted-display');
|
|
1733
|
+
var real = wrap.querySelector('.' + prefix + '-redacted-real');
|
|
1734
|
+
if (!display || !real) return;
|
|
1735
|
+
var isHidden = real.style.display === 'none';
|
|
1736
|
+
display.style.display = isHidden ? 'none' : '';
|
|
1737
|
+
real.style.display = isHidden ? '' : 'none';
|
|
1738
|
+
// Toggle icon between eye and eye-off
|
|
1739
|
+
btn.innerHTML = isHidden
|
|
1740
|
+
? '<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>'
|
|
1741
|
+
: '<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>';
|
|
1742
|
+
btn.title = isHidden ? 'Hide value' : 'Reveal value';
|
|
1743
|
+
});
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
container.querySelectorAll('.' + prefix + '-redacted-copy').forEach(function (btn) {
|
|
1747
|
+
btn.addEventListener('click', function (e) {
|
|
1748
|
+
e.stopPropagation();
|
|
1749
|
+
var wrap = btn.closest('.' + prefix + '-redacted-wrap');
|
|
1750
|
+
if (!wrap) return;
|
|
1751
|
+
var val = wrap.getAttribute('data-redacted-value');
|
|
1752
|
+
if (!val) return;
|
|
1753
|
+
navigator.clipboard.writeText(val).then(function () {
|
|
1754
|
+
btn.innerHTML = '\u2713';
|
|
1755
|
+
btn.classList.add(prefix + '-copy-row-ok');
|
|
1756
|
+
setTimeout(function () {
|
|
1757
|
+
btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1758
|
+
btn.classList.remove(prefix + '-copy-row-ok');
|
|
1759
|
+
}, 1200);
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
var fetchConfig = function () {
|
|
1766
|
+
fetchJSON(API + '/config')
|
|
1767
|
+
.then(function (data) {
|
|
1768
|
+
sectionLoaded.config = true;
|
|
1769
|
+
configRawData = data;
|
|
1770
|
+
renderConfig();
|
|
1771
|
+
})
|
|
1772
|
+
.catch(function () { setInner('ss-dash-config-body', '<div class="ss-dash-empty">Config not available</div>'); });
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
var renderConfig = function () {
|
|
1776
|
+
var body = document.getElementById('ss-dash-config-body');
|
|
1777
|
+
if (!body || !configRawData) return;
|
|
1778
|
+
|
|
1779
|
+
var source = configActiveTab === 'env' ? (configRawData.env || {}) : (configRawData.config || configRawData);
|
|
1780
|
+
|
|
1781
|
+
// Flatten to dot-notation paths for search
|
|
1782
|
+
var flat = flattenConfig(source, '');
|
|
1783
|
+
var filtered = flat;
|
|
1784
|
+
var term = configSearchTerm.toLowerCase();
|
|
1785
|
+
if (term) {
|
|
1786
|
+
filtered = flat.filter(function (item) {
|
|
1787
|
+
var valStr = isRedactedObj(item.value) ? item.value.display : String(item.value);
|
|
1788
|
+
return item.path.toLowerCase().indexOf(term) !== -1 || valStr.toLowerCase().indexOf(term) !== -1;
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
var html = '';
|
|
1793
|
+
|
|
1794
|
+
if (configActiveTab === 'env') {
|
|
1795
|
+
// Env vars: simple table
|
|
1796
|
+
html += '<div class="ss-dash-config-table-wrap"><table class="ss-dash-table ss-dash-config-env-table"><thead><tr>'
|
|
1797
|
+
+ '<th>Variable</th><th>Value</th><th style="width:36px"></th>'
|
|
1798
|
+
+ '</tr></thead><tbody>';
|
|
1799
|
+
filtered.forEach(function (item) {
|
|
1800
|
+
var redacted = isRedactedObj(item.value);
|
|
1801
|
+
var displayVal = redacted ? item.value.display : String(item.value);
|
|
1802
|
+
var copyVal = esc(item.path + '=' + displayVal);
|
|
1803
|
+
html += '<tr>'
|
|
1804
|
+
+ '<td class="ss-dash-env-key"><span class="ss-dash-config-key">' + highlightMatch(esc(item.path), term) + '</span></td>'
|
|
1805
|
+
+ '<td class="ss-dash-env-val">' + (redacted ? renderRedacted(item.value, 'ss-dash') : '<span class="ss-dash-config-val">' + highlightMatch(esc(displayVal), term) + '</span>') + '</td>'
|
|
1806
|
+
+ '<td>' + (redacted ? '' : '<button class="ss-dash-copy-row-btn" data-copy-val="' + copyVal + '" title="Copy">\u2398</button>') + '</td>'
|
|
1807
|
+
+ '</tr>';
|
|
1808
|
+
});
|
|
1809
|
+
html += '</tbody></table></div>';
|
|
1810
|
+
} else {
|
|
1811
|
+
// App config: grouped by top-level section
|
|
1812
|
+
if (term) {
|
|
1813
|
+
// Search mode: flat list of matching paths
|
|
1814
|
+
html += '<div class="ss-dash-config-table-wrap"><table class="ss-dash-table"><thead><tr>'
|
|
1815
|
+
+ '<th>Path</th><th>Value</th><th style="width:36px"></th>'
|
|
1816
|
+
+ '</tr></thead><tbody>';
|
|
1817
|
+
filtered.forEach(function (item) {
|
|
1818
|
+
var redacted = isRedactedObj(item.value);
|
|
1819
|
+
var displayVal = redacted ? item.value.display : String(item.value);
|
|
1820
|
+
var copyVal = esc(item.path + ': ' + displayVal);
|
|
1821
|
+
html += '<tr>'
|
|
1822
|
+
+ '<td><span class="ss-dash-config-key" style="white-space:nowrap">' + highlightMatch(esc(item.path), term) + '</span></td>'
|
|
1823
|
+
+ '<td>' + (redacted ? renderRedacted(item.value, 'ss-dash') : '<span class="ss-dash-config-val" style="word-break:break-all">' + highlightMatch(esc(displayVal), term) + '</span>') + '</td>'
|
|
1824
|
+
+ '<td>' + (redacted ? '' : '<button class="ss-dash-copy-row-btn" data-copy-val="' + copyVal + '" title="Copy">\u2398</button>') + '</td>'
|
|
1825
|
+
+ '</tr>';
|
|
1826
|
+
});
|
|
1827
|
+
html += '</tbody></table></div>';
|
|
1828
|
+
html += '<div style="padding:4px 16px;font-size:10px;color:var(--ss-muted)">' + filtered.length + ' of ' + flat.length + ' entries</div>';
|
|
1829
|
+
} else {
|
|
1830
|
+
// Browse mode: collapsible sections by top-level key
|
|
1831
|
+
var topKeys = Object.keys(source);
|
|
1832
|
+
html += '<div class="ss-dash-config-sections">';
|
|
1833
|
+
topKeys.forEach(function (sectionKey) {
|
|
1834
|
+
var sectionVal = source[sectionKey];
|
|
1835
|
+
var childCount = countLeaves(sectionVal);
|
|
1836
|
+
var isObj = typeof sectionVal === 'object' && sectionVal !== null && !sectionVal.__redacted;
|
|
1837
|
+
|
|
1838
|
+
html += '<div class="ss-dash-config-section">';
|
|
1839
|
+
if (isObj) {
|
|
1840
|
+
html += '<div class="ss-dash-config-section-header" data-config-section="' + esc(sectionKey) + '">'
|
|
1841
|
+
+ '<span class="ss-dash-config-toggle">\u25B6</span>'
|
|
1842
|
+
+ '<span class="ss-dash-config-key">' + esc(sectionKey) + '</span>'
|
|
1843
|
+
+ '<span class="ss-dash-config-count">' + childCount + ' entries</span>'
|
|
1844
|
+
+ '</div>';
|
|
1845
|
+
html += '<div class="ss-dash-config-section-body" style="display:none">';
|
|
1846
|
+
html += renderConfigTable(sectionVal, sectionKey);
|
|
1847
|
+
html += '</div>';
|
|
1848
|
+
} else {
|
|
1849
|
+
html += '<div class="ss-dash-config-section-header ss-dash-config-leaf">'
|
|
1850
|
+
+ '<span class="ss-dash-config-key">' + esc(sectionKey) + '</span>'
|
|
1851
|
+
+ '<span class="ss-dash-config-val" style="margin-left:8px">' + esc(String(sectionVal)) + '</span>'
|
|
1852
|
+
+ '</div>';
|
|
1853
|
+
}
|
|
1854
|
+
html += '</div>';
|
|
1855
|
+
});
|
|
1856
|
+
html += '</div>';
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
body.innerHTML = html;
|
|
1861
|
+
|
|
1862
|
+
// Bind section toggles
|
|
1863
|
+
body.querySelectorAll('[data-config-section]').forEach(function (header) {
|
|
1864
|
+
header.addEventListener('click', function () {
|
|
1865
|
+
var sectionBody = header.nextElementSibling;
|
|
1866
|
+
if (!sectionBody) return;
|
|
1867
|
+
var isHidden = sectionBody.style.display === 'none';
|
|
1868
|
+
sectionBody.style.display = isHidden ? '' : 'none';
|
|
1869
|
+
var toggle = header.querySelector('.ss-dash-config-toggle');
|
|
1870
|
+
if (toggle) toggle.textContent = isHidden ? '\u25BC' : '\u25B6';
|
|
1871
|
+
});
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// Bind row copy buttons
|
|
1875
|
+
body.querySelectorAll('.ss-dash-copy-row-btn').forEach(function (btn) {
|
|
1876
|
+
btn.addEventListener('click', function (e) {
|
|
1877
|
+
e.stopPropagation();
|
|
1878
|
+
var val = btn.getAttribute('data-copy-val');
|
|
1879
|
+
if (!val) return;
|
|
1880
|
+
navigator.clipboard.writeText(val).then(function () {
|
|
1881
|
+
btn.textContent = '\u2713';
|
|
1882
|
+
btn.classList.add('ss-dash-copy-row-ok');
|
|
1883
|
+
setTimeout(function () { btn.textContent = '\u2398'; btn.classList.remove('ss-dash-copy-row-ok'); }, 1200);
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// Bind redacted reveal/copy buttons
|
|
1889
|
+
bindRedactedButtons(body, 'ss-dash');
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
/** Render a nested object as a flat key-value table. */
|
|
1893
|
+
var renderConfigTable = function (obj, prefix) {
|
|
1894
|
+
var flat = flattenConfig(obj, prefix);
|
|
1895
|
+
var html = '<table class="ss-dash-table ss-dash-config-inner-table"><thead><tr>'
|
|
1896
|
+
+ '<th style="width:35%">Key</th><th>Value</th><th style="width:36px"></th>'
|
|
1897
|
+
+ '</tr></thead><tbody>';
|
|
1898
|
+
flat.forEach(function (item) {
|
|
1899
|
+
// Show relative path (strip the section prefix)
|
|
1900
|
+
var relPath = item.path.indexOf(prefix + '.') === 0 ? item.path.slice(prefix.length + 1) : item.path;
|
|
1901
|
+
var redacted = isRedactedObj(item.value);
|
|
1902
|
+
var displayVal = redacted ? item.value.display : String(item.value);
|
|
1903
|
+
var copyVal = esc(item.path + ': ' + displayVal);
|
|
1904
|
+
var valStr = redacted ? item.value.display : ((typeof item.value === 'object' && item.value !== null) ? JSON.stringify(item.value) : String(item.value));
|
|
1905
|
+
html += '<tr>'
|
|
1906
|
+
+ '<td title="' + esc(relPath) + '"><span class="ss-dash-config-key">' + esc(relPath) + '</span></td>'
|
|
1907
|
+
+ '<td title="' + esc(valStr) + '">' + (redacted ? renderRedacted(item.value, 'ss-dash') : '<span class="ss-dash-config-val">' + formatConfigValue(item.value) + '</span>') + '</td>'
|
|
1908
|
+
+ '<td>' + (redacted ? '' : '<button class="ss-dash-copy-row-btn" data-copy-val="' + copyVal + '" title="Copy">\u2398</button>') + '</td>'
|
|
1909
|
+
+ '</tr>';
|
|
1910
|
+
});
|
|
1911
|
+
html += '</tbody></table>';
|
|
1912
|
+
return html;
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
/** Flatten a nested object into [{path, value}] dot-notation entries. */
|
|
1916
|
+
var flattenConfig = function (obj, prefix) {
|
|
1917
|
+
var results = [];
|
|
1918
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
1919
|
+
results.push({ path: prefix, value: obj });
|
|
1920
|
+
return results;
|
|
1921
|
+
}
|
|
1922
|
+
var keys = Object.keys(obj);
|
|
1923
|
+
keys.forEach(function (key) {
|
|
1924
|
+
var fullPath = prefix ? prefix + '.' + key : key;
|
|
1925
|
+
var val = obj[key];
|
|
1926
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val) && !val.__redacted) {
|
|
1927
|
+
results = results.concat(flattenConfig(val, fullPath));
|
|
1928
|
+
} else {
|
|
1929
|
+
results.push({ path: fullPath, value: val });
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
return results;
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
/** Count leaf values in a nested object. */
|
|
1936
|
+
var countLeaves = function (obj) {
|
|
1937
|
+
if (typeof obj !== 'object' || obj === null || obj.__redacted) return 1;
|
|
1938
|
+
var count = 0;
|
|
1939
|
+
Object.keys(obj).forEach(function (k) { count += countLeaves(obj[k]); });
|
|
1940
|
+
return count;
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
/** Format a config value with type-aware coloring. */
|
|
1944
|
+
var formatConfigValue = function (val) {
|
|
1945
|
+
if (val === null || val === undefined) return '<span style="color:var(--ss-dim)">null</span>';
|
|
1946
|
+
if (val === true) return '<span style="color:var(--ss-green-fg)">true</span>';
|
|
1947
|
+
if (val === false) return '<span style="color:var(--ss-red-fg)">false</span>';
|
|
1948
|
+
if (typeof val === 'number') return '<span style="color:var(--ss-amber-fg)">' + val + '</span>';
|
|
1949
|
+
if (Array.isArray(val)) {
|
|
1950
|
+
var items = val.map(function (item) {
|
|
1951
|
+
if (item === null || item === undefined) return 'null';
|
|
1952
|
+
if (typeof item === 'object') {
|
|
1953
|
+
try { return JSON.stringify(item); } catch (e) { return String(item); }
|
|
1954
|
+
}
|
|
1955
|
+
return String(item);
|
|
1956
|
+
});
|
|
1957
|
+
return '<span style="color:var(--ss-purple-fg)">[' + esc(items.join(', ')) + ']</span>';
|
|
1958
|
+
}
|
|
1959
|
+
if (typeof val === 'object') {
|
|
1960
|
+
try { return '<span style="color:var(--ss-dim)">' + esc(JSON.stringify(val, null, 2)) + '</span>'; } catch (e) { /* fall through */ }
|
|
1961
|
+
}
|
|
1962
|
+
return esc(String(val));
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
/** Highlight matching substring in text. */
|
|
1966
|
+
var highlightMatch = function (text, term) {
|
|
1967
|
+
if (!term) return text;
|
|
1968
|
+
var idx = text.toLowerCase().indexOf(term.toLowerCase());
|
|
1969
|
+
if (idx === -1) return text;
|
|
1970
|
+
return text.slice(0, idx) + '<mark class="ss-dash-config-match">' + text.slice(idx, idx + term.length) + '</mark>' + text.slice(idx + term.length);
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// ── Config: tab switching ───────────────────────────────────────
|
|
1974
|
+
document.querySelectorAll('[data-config-tab]').forEach(function (btn) {
|
|
1975
|
+
btn.addEventListener('click', function () {
|
|
1976
|
+
configActiveTab = btn.getAttribute('data-config-tab');
|
|
1977
|
+
document.querySelectorAll('[data-config-tab]').forEach(function (b) { b.classList.remove('ss-dash-active'); });
|
|
1978
|
+
btn.classList.add('ss-dash-active');
|
|
1979
|
+
renderConfig();
|
|
1980
|
+
});
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// ── Config: search ──────────────────────────────────────────────
|
|
1984
|
+
var configSearchEl = document.getElementById('ss-dash-config-search');
|
|
1985
|
+
if (configSearchEl) {
|
|
1986
|
+
var configSearchTimer = null;
|
|
1987
|
+
configSearchEl.addEventListener('input', function () {
|
|
1988
|
+
clearTimeout(configSearchTimer);
|
|
1989
|
+
configSearchTimer = setTimeout(function () {
|
|
1990
|
+
configSearchTerm = configSearchEl.value.trim();
|
|
1991
|
+
renderConfig();
|
|
1992
|
+
}, 200);
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// ── Config: expand/collapse all ─────────────────────────────────
|
|
1997
|
+
var expandAllBtn = document.getElementById('ss-dash-config-expand-all');
|
|
1998
|
+
var collapseAllBtn = document.getElementById('ss-dash-config-collapse-all');
|
|
1999
|
+
if (expandAllBtn) {
|
|
2000
|
+
expandAllBtn.addEventListener('click', function () {
|
|
2001
|
+
var body = document.getElementById('ss-dash-config-body');
|
|
2002
|
+
if (!body) return;
|
|
2003
|
+
body.querySelectorAll('.ss-dash-config-section-body').forEach(function (el) { el.style.display = ''; });
|
|
2004
|
+
body.querySelectorAll('.ss-dash-config-toggle').forEach(function (el) { el.textContent = '\u25BC'; });
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
if (collapseAllBtn) {
|
|
2008
|
+
collapseAllBtn.addEventListener('click', function () {
|
|
2009
|
+
var body = document.getElementById('ss-dash-config-body');
|
|
2010
|
+
if (!body) return;
|
|
2011
|
+
body.querySelectorAll('.ss-dash-config-section-body').forEach(function (el) { el.style.display = 'none'; });
|
|
2012
|
+
body.querySelectorAll('.ss-dash-config-toggle').forEach(function (el) { el.textContent = '\u25B6'; });
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// ── Config: copy button ─────────────────────────────────────────
|
|
2017
|
+
var configCopyBtn = document.getElementById('ss-dash-config-copy');
|
|
2018
|
+
if (configCopyBtn) {
|
|
2019
|
+
configCopyBtn.addEventListener('click', function () {
|
|
2020
|
+
if (!configRawData) return;
|
|
2021
|
+
var source = configActiveTab === 'env' ? (configRawData.env || {}) : (configRawData.config || configRawData);
|
|
2022
|
+
navigator.clipboard.writeText(JSON.stringify(source, null, 2)).then(function () {
|
|
2023
|
+
configCopyBtn.textContent = 'Copied!';
|
|
2024
|
+
setTimeout(function () { configCopyBtn.textContent = 'Copy JSON'; }, 1500);
|
|
2025
|
+
});
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// ── Custom Panes ──────────────────────────────────────────────
|
|
2030
|
+
var customPaneState = {};
|
|
2031
|
+
customPanes.forEach(function (cp) {
|
|
2032
|
+
customPaneState[cp.id] = { data: [], fetched: false, filter: '' };
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
var getNestedValue = function (obj, path) {
|
|
2036
|
+
var parts = path.split('.');
|
|
2037
|
+
var cur = obj;
|
|
2038
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2039
|
+
if (cur == null) return undefined;
|
|
2040
|
+
cur = cur[parts[i]];
|
|
2041
|
+
}
|
|
2042
|
+
return cur;
|
|
2043
|
+
};
|
|
2044
|
+
|
|
2045
|
+
var fetchCustomPane = function (pane) {
|
|
2046
|
+
fetchJSON(pane.endpoint)
|
|
2047
|
+
.then(function (data) {
|
|
2048
|
+
var key = pane.dataKey || pane.id;
|
|
2049
|
+
var rows = getNestedValue(data, key) || (Array.isArray(data) ? data : []);
|
|
2050
|
+
customPaneState[pane.id].data = rows;
|
|
2051
|
+
customPaneState[pane.id].fetched = true;
|
|
2052
|
+
renderCustomPane(pane);
|
|
2053
|
+
})
|
|
2054
|
+
.catch(function () {
|
|
2055
|
+
setInner('ss-dash-' + pane.id + '-body', '<div class="ss-dash-empty">Failed to load ' + esc(pane.label) + '</div>');
|
|
2056
|
+
});
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
var renderCustomPane = function (pane) {
|
|
2060
|
+
var state = customPaneState[pane.id];
|
|
2061
|
+
if (!state) return;
|
|
2062
|
+
var bodyEl = document.getElementById('ss-dash-' + pane.id + '-body');
|
|
2063
|
+
var summaryEl = document.getElementById('ss-dash-' + pane.id + '-summary');
|
|
2064
|
+
if (!bodyEl) return;
|
|
2065
|
+
|
|
2066
|
+
var filter = state.filter.toLowerCase();
|
|
2067
|
+
var rows = state.data;
|
|
2068
|
+
|
|
2069
|
+
if (summaryEl) summaryEl.textContent = rows.length + ' ' + pane.label.toLowerCase();
|
|
2070
|
+
|
|
2071
|
+
if (filter) {
|
|
2072
|
+
var searchCols = pane.columns.filter(function (c) { return c.searchable; });
|
|
2073
|
+
if (searchCols.length > 0) {
|
|
2074
|
+
rows = rows.filter(function (row) {
|
|
2075
|
+
return searchCols.some(function (c) {
|
|
2076
|
+
var v = row[c.key];
|
|
2077
|
+
return v != null && String(v).toLowerCase().indexOf(filter) !== -1;
|
|
2078
|
+
});
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (rows.length === 0) {
|
|
2084
|
+
bodyEl.innerHTML = '<div class="ss-dash-empty">' + (filter ? 'No matching ' + esc(pane.label.toLowerCase()) : 'No ' + esc(pane.label.toLowerCase()) + ' recorded yet') + '</div>';
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
var html = '<table class="ss-dash-table"><thead><tr>';
|
|
2089
|
+
pane.columns.forEach(function (col) {
|
|
2090
|
+
html += '<th' + (col.width ? ' style="width:' + col.width + '"' : '') + '>' + esc(col.label) + '</th>';
|
|
2091
|
+
});
|
|
2092
|
+
html += '</tr></thead><tbody>';
|
|
2093
|
+
|
|
2094
|
+
rows.forEach(function (row) {
|
|
2095
|
+
html += '<tr>';
|
|
2096
|
+
pane.columns.forEach(function (col) {
|
|
2097
|
+
var val = row[col.key];
|
|
2098
|
+
html += '<td>' + formatCell(val, col) + '</td>';
|
|
2099
|
+
});
|
|
2100
|
+
html += '</tr>';
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
html += '</tbody></table>';
|
|
2104
|
+
bodyEl.innerHTML = html;
|
|
2105
|
+
};
|
|
2106
|
+
|
|
2107
|
+
// Bind search/clear for custom panes
|
|
2108
|
+
customPanes.forEach(function (cp) {
|
|
2109
|
+
var searchInput = document.getElementById('ss-dash-search-' + cp.id);
|
|
2110
|
+
var clearBtn = document.getElementById('ss-dash-' + cp.id + '-clear');
|
|
2111
|
+
if (searchInput) {
|
|
2112
|
+
searchInput.addEventListener('input', function () {
|
|
2113
|
+
customPaneState[cp.id].filter = searchInput.value;
|
|
2114
|
+
renderCustomPane(cp);
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
if (clearBtn) {
|
|
2118
|
+
clearBtn.addEventListener('click', function () {
|
|
2119
|
+
customPaneState[cp.id].data = [];
|
|
2120
|
+
customPaneState[cp.id].fetched = false;
|
|
2121
|
+
if (searchInput) searchInput.value = '';
|
|
2122
|
+
customPaneState[cp.id].filter = '';
|
|
2123
|
+
renderCustomPane(cp);
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
// ── Pagination ────────────────────────────────────────────────
|
|
2129
|
+
var renderPagination = function (section, ps) {
|
|
2130
|
+
var el = document.getElementById('ss-dash-pagination-' + section);
|
|
2131
|
+
if (!el) return;
|
|
2132
|
+
|
|
2133
|
+
var totalPages = Math.ceil(ps.total / PER_PAGE) || 1;
|
|
2134
|
+
if (totalPages <= 1) { el.innerHTML = ''; return; }
|
|
2135
|
+
|
|
2136
|
+
var html = '<button class="ss-dash-page-btn" data-page="prev" ' + (ps.page <= 1 ? 'disabled' : '') + '>« Prev</button>';
|
|
2137
|
+
html += '<span class="ss-dash-page-info">Page ' + ps.page + ' of ' + totalPages + '</span>';
|
|
2138
|
+
html += '<button class="ss-dash-page-btn" data-page="next" ' + (ps.page >= totalPages ? 'disabled' : '') + '>Next »</button>';
|
|
2139
|
+
|
|
2140
|
+
el.innerHTML = html;
|
|
2141
|
+
el.querySelectorAll('.ss-dash-page-btn').forEach(function (btn) {
|
|
2142
|
+
btn.addEventListener('click', function () {
|
|
2143
|
+
var dir = btn.getAttribute('data-page');
|
|
2144
|
+
if (dir === 'prev' && ps.page > 1) ps.page--;
|
|
2145
|
+
else if (dir === 'next' && ps.page < totalPages) ps.page++;
|
|
2146
|
+
loadSection(section);
|
|
2147
|
+
});
|
|
2148
|
+
});
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// ── Badge updates ─────────────────────────────────────────────
|
|
2152
|
+
var updateBadge = function (section, count) {
|
|
2153
|
+
var badge = root.querySelector('[data-ss-section="' + section + '"] .ss-dash-nav-badge');
|
|
2154
|
+
if (badge && count != null) badge.textContent = count;
|
|
2155
|
+
};
|
|
2156
|
+
|
|
2157
|
+
// ── DOM helpers ───────────────────────────────────────────────
|
|
2158
|
+
var setInner = function (id, html) {
|
|
2159
|
+
var el = document.getElementById(id);
|
|
2160
|
+
if (el) el.innerHTML = html;
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
var bindDataExpand = function (containerId) {
|
|
2164
|
+
var container = document.getElementById(containerId);
|
|
2165
|
+
if (!container) return;
|
|
2166
|
+
container.querySelectorAll('.ss-dash-data-preview').forEach(function (el) {
|
|
2167
|
+
el.addEventListener('click', function () {
|
|
2168
|
+
var idx = el.getAttribute('data-ev-idx');
|
|
2169
|
+
var pre = document.getElementById('ss-dash-evdata-' + idx);
|
|
2170
|
+
if (pre) {
|
|
2171
|
+
var open = pre.style.display !== 'none';
|
|
2172
|
+
pre.style.display = open ? 'none' : 'block';
|
|
2173
|
+
el.style.display = open ? '' : 'none';
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
});
|
|
2177
|
+
container.querySelectorAll('.ss-dash-data-full').forEach(function (el) {
|
|
2178
|
+
el.addEventListener('click', function () {
|
|
2179
|
+
el.style.display = 'none';
|
|
2180
|
+
var idx = el.id.replace('ss-dash-evdata-', '');
|
|
2181
|
+
var preview = container.querySelector('[data-ev-idx="' + idx + '"]');
|
|
2182
|
+
if (preview) preview.style.display = '';
|
|
2183
|
+
});
|
|
2184
|
+
});
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
// ── Auto-refresh ──────────────────────────────────────────────
|
|
2188
|
+
var startRefresh = function () {
|
|
2189
|
+
stopRefresh();
|
|
2190
|
+
if (isLive) return; // Transmit handles live updates
|
|
2191
|
+
refreshTimer = setInterval(function () {
|
|
2192
|
+
loadSection(activeSection);
|
|
2193
|
+
}, activeSection === 'overview' ? 5000 : 3000);
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
var stopRefresh = function () {
|
|
2197
|
+
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
// ── Transmit real-time ────────────────────────────────────────
|
|
2201
|
+
var ssLog = function (msg, data) {
|
|
2202
|
+
var prefix = '%c[server-stats]%c ';
|
|
2203
|
+
if (data !== undefined) {
|
|
2204
|
+
console.log(prefix + msg, 'color:#34d399;font-weight:bold', 'color:inherit', data);
|
|
2205
|
+
} else {
|
|
2206
|
+
console.log(prefix + msg, 'color:#34d399;font-weight:bold', 'color:inherit');
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var ssWarn = function (msg, data) {
|
|
2210
|
+
var prefix = '[server-stats] ';
|
|
2211
|
+
if (data !== undefined) {
|
|
2212
|
+
console.warn(prefix + msg, data);
|
|
2213
|
+
} else {
|
|
2214
|
+
console.warn(prefix + msg);
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
var setConnectionStatus = function (status) {
|
|
2219
|
+
var dot = document.getElementById('ss-dash-live-dot');
|
|
2220
|
+
var label = document.getElementById('ss-dash-live-label');
|
|
2221
|
+
if (status === 'live') {
|
|
2222
|
+
isLive = true;
|
|
2223
|
+
if (dot) dot.classList.add('ss-dash-connected');
|
|
2224
|
+
if (label) { label.textContent = 'Live'; label.classList.add('ss-dash-connected'); }
|
|
2225
|
+
stopRefresh();
|
|
2226
|
+
} else {
|
|
2227
|
+
isLive = false;
|
|
2228
|
+
if (dot) dot.classList.remove('ss-dash-connected');
|
|
2229
|
+
if (label) { label.textContent = 'Polling'; label.classList.remove('ss-dash-connected'); }
|
|
2230
|
+
startRefresh();
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
|
|
2234
|
+
var initTransmit = function () {
|
|
2235
|
+
ssLog('Initializing real-time connection...');
|
|
2236
|
+
|
|
2237
|
+
if (typeof Transmit === 'undefined' && typeof window.Transmit === 'undefined') {
|
|
2238
|
+
ssWarn('Transmit client not found. The @adonisjs/transmit-client package may not be installed. Falling back to polling.');
|
|
2239
|
+
startRefresh();
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
ssLog('Transmit client found, creating subscription...');
|
|
2244
|
+
|
|
2245
|
+
try {
|
|
2246
|
+
var TransmitClass = typeof Transmit !== 'undefined' ? Transmit : window.Transmit;
|
|
2247
|
+
ssLog('TransmitClass type: ' + typeof TransmitClass);
|
|
2248
|
+
|
|
2249
|
+
// onSubscription and onReconnectFailed are constructor options, NOT subscription methods
|
|
2250
|
+
var transmit = new TransmitClass({
|
|
2251
|
+
baseUrl: window.location.origin,
|
|
2252
|
+
onSubscription: function (channel) {
|
|
2253
|
+
ssLog('Subscription active on channel: ' + channel + ' — switched to live mode');
|
|
2254
|
+
setConnectionStatus('live');
|
|
2255
|
+
},
|
|
2256
|
+
onReconnectAttempt: function (attempt) {
|
|
2257
|
+
ssLog('Reconnect attempt #' + attempt);
|
|
2258
|
+
},
|
|
2259
|
+
onReconnectFailed: function () {
|
|
2260
|
+
ssWarn('Transmit reconnection failed — falling back to polling');
|
|
2261
|
+
setConnectionStatus('polling');
|
|
2262
|
+
},
|
|
2263
|
+
onSubscribeFailed: function (channel) {
|
|
2264
|
+
ssWarn('Subscribe failed for channel: ' + channel + ' — falling back to polling');
|
|
2265
|
+
setConnectionStatus('polling');
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
transmitSub = transmit.subscription('server-stats/dashboard');
|
|
2270
|
+
ssLog('Subscription instance created');
|
|
2271
|
+
|
|
2272
|
+
// Start polling while we wait for subscription to connect
|
|
2273
|
+
startRefresh();
|
|
2274
|
+
|
|
2275
|
+
transmitSub.onMessage(function (message) {
|
|
2276
|
+
try {
|
|
2277
|
+
var event = typeof message === 'string' ? JSON.parse(message) : message;
|
|
2278
|
+
var kind = event.type || (event.avgResponseTime !== undefined ? 'overview' : 'unknown');
|
|
2279
|
+
ssLog('Live event received: ' + kind);
|
|
2280
|
+
handleLiveEvent(event);
|
|
2281
|
+
} catch (e) { /* ignore */ }
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
ssLog('Calling transmitSub.create()...');
|
|
2285
|
+
var createResult = transmitSub.create();
|
|
2286
|
+
if (createResult && typeof createResult.then === 'function') {
|
|
2287
|
+
createResult.then(function () {
|
|
2288
|
+
ssLog('transmitSub.create() resolved — subscription is active');
|
|
2289
|
+
}).catch(function (err) {
|
|
2290
|
+
ssWarn('transmitSub.create() rejected:', err && err.message ? err.message : err);
|
|
2291
|
+
setConnectionStatus('polling');
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
ssWarn('Transmit init error:', e && e.message ? e.message : e);
|
|
2296
|
+
startRefresh();
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
var handleLiveEvent = function (event) {
|
|
2301
|
+
// Detect overview data broadcast (has avgResponseTime but no type)
|
|
2302
|
+
if (event && typeof event.avgResponseTime === 'number') {
|
|
2303
|
+
if (activeSection === 'overview') {
|
|
2304
|
+
renderOverview(event, null);
|
|
2305
|
+
}
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// Typed events for specific sections
|
|
2310
|
+
var type = event.type;
|
|
2311
|
+
var sectionMap = {
|
|
2312
|
+
'request': 'requests',
|
|
2313
|
+
'query': 'queries',
|
|
2314
|
+
'event': 'events',
|
|
2315
|
+
'log': 'logs',
|
|
2316
|
+
'email': 'emails',
|
|
2317
|
+
'trace': 'timeline'
|
|
2318
|
+
};
|
|
2319
|
+
var section = sectionMap[type];
|
|
2320
|
+
|
|
2321
|
+
if (activeSection === 'overview') { fetchOverview(); return; }
|
|
2322
|
+
if (section && section === activeSection) { loadSection(activeSection); }
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
// ── Hash-based routing ────────────────────────────────────────
|
|
2326
|
+
var parseHash = function () {
|
|
2327
|
+
var hash = location.hash.replace('#', '');
|
|
2328
|
+
var parts = hash.split('?');
|
|
2329
|
+
var section = parts[0] || 'overview';
|
|
2330
|
+
var params = {};
|
|
2331
|
+
if (parts[1]) {
|
|
2332
|
+
parts[1].split('&').forEach(function (p) {
|
|
2333
|
+
var kv = p.split('=');
|
|
2334
|
+
params[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || '');
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
return { section: section, params: params };
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
var initRoute = function () {
|
|
2341
|
+
var route = parseHash();
|
|
2342
|
+
var section = route.section;
|
|
2343
|
+
|
|
2344
|
+
// Validate section exists
|
|
2345
|
+
var valid = ['overview', 'requests', 'queries', 'events', 'routes', 'logs', 'emails', 'timeline', 'cache', 'jobs', 'config'];
|
|
2346
|
+
customPanes.forEach(function (cp) { valid.push(cp.id); });
|
|
2347
|
+
if (valid.indexOf(section) === -1) section = 'overview';
|
|
2348
|
+
|
|
2349
|
+
// Apply deep link params
|
|
2350
|
+
if (route.params.requestId && section === 'logs') {
|
|
2351
|
+
logReqIdFilter = route.params.requestId;
|
|
2352
|
+
var input = document.getElementById('ss-dash-log-reqid-input');
|
|
2353
|
+
if (input) input.value = logReqIdFilter;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Switch to section
|
|
2357
|
+
activeSection = section;
|
|
2358
|
+
navItems.forEach(function (item) {
|
|
2359
|
+
item.classList.toggle('ss-dash-active', item.getAttribute('data-ss-section') === section);
|
|
2360
|
+
});
|
|
2361
|
+
root.querySelectorAll('.ss-dash-pane').forEach(function (p) {
|
|
2362
|
+
p.classList.toggle('ss-dash-active', p.id === 'ss-dash-pane-' + section);
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
loadSection(section);
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
window.addEventListener('hashchange', function () {
|
|
2369
|
+
var route = parseHash();
|
|
2370
|
+
if (route.section !== activeSection) {
|
|
2371
|
+
switchSection(route.section);
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
// ── Init ──────────────────────────────────────────────────────
|
|
2376
|
+
initRoute();
|
|
2377
|
+
initTransmit();
|
|
2378
|
+
})();
|