adonisjs-server-stats 1.4.0 → 1.5.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 +114 -116
- package/dist/configure.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.d.ts +2 -2
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/server_stats_controller.d.ts +1 -1
- package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.js +8 -8
- package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_controller.js +244 -522
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_routes.js +7 -2
- package/dist/src/dashboard/dashboard_store.d.ts +6 -3
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +54 -78
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.js +3 -1
- package/dist/src/dashboard/models/stats_event.d.ts +1 -1
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.d.ts +2 -2
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.js +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
- package/dist/src/debug/debug_store.d.ts +6 -6
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -10
- package/dist/src/debug/email_collector.d.ts +0 -9
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +6 -28
- package/dist/src/debug/event_collector.d.ts +1 -1
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +17 -17
- package/dist/src/debug/query_collector.d.ts +1 -1
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +13 -14
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.d.ts +1 -1
- package/dist/src/debug/route_inspector.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.js +12 -12
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +6 -5
- package/dist/src/edge/client/dashboard.css +516 -171
- package/dist/src/edge/client/dashboard.js +2756 -1662
- package/dist/src/edge/client/debug-panel.css +476 -133
- package/dist/src/edge/client/debug-panel.js +1496 -1043
- package/dist/src/edge/client/stats-bar.css +64 -30
- package/dist/src/edge/client/stats-bar.js +598 -319
- package/dist/src/edge/plugin.d.ts +1 -1
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +41 -59
- package/dist/src/edge/views/stats-bar.edge +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +7 -6
- package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
- package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts +1 -1
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +31 -31
- package/dist/src/utils/json_helpers.d.ts +8 -0
- package/dist/src/utils/json_helpers.d.ts.map +1 -0
- package/dist/src/utils/json_helpers.js +21 -0
- package/dist/src/utils/mail_helpers.d.ts +13 -0
- package/dist/src/utils/mail_helpers.d.ts.map +1 -0
- package/dist/src/utils/mail_helpers.js +26 -0
- package/dist/src/utils/math_helpers.d.ts +8 -0
- package/dist/src/utils/math_helpers.d.ts.map +1 -0
- package/dist/src/utils/math_helpers.js +11 -0
- package/dist/src/utils/time_helpers.d.ts +12 -0
- package/dist/src/utils/time_helpers.d.ts.map +1 -0
- package/dist/src/utils/time_helpers.js +32 -0
- package/dist/src/utils/transmit_client.d.ts +9 -0
- package/dist/src/utils/transmit_client.d.ts.map +1 -0
- package/dist/src/utils/transmit_client.js +20 -0
- package/package.json +35 -29
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
|
|
5
|
+
import { round, clamp } from '../utils/math_helpers.js';
|
|
6
|
+
import { loadTransmitClient } from '../utils/transmit_client.js';
|
|
5
7
|
import { CacheInspector } from './integrations/cache_inspector.js';
|
|
6
|
-
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
7
8
|
import { ConfigInspector } from './integrations/config_inspector.js';
|
|
9
|
+
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
8
10
|
const EDGE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'edge');
|
|
11
|
+
function emptyPage(page, perPage) {
|
|
12
|
+
return { data: [], total: 0, page, perPage };
|
|
13
|
+
}
|
|
9
14
|
/**
|
|
10
15
|
* Controller for the full-page dashboard.
|
|
11
16
|
*
|
|
12
17
|
* Serves the dashboard HTML page and all JSON API endpoints.
|
|
13
|
-
*
|
|
14
|
-
* the in-memory DebugStore, and the AdonisJS application instance.
|
|
18
|
+
* Delegates all data access to the DashboardStore.
|
|
15
19
|
*/
|
|
16
20
|
export default class DashboardController {
|
|
17
21
|
dashboardStore;
|
|
@@ -34,17 +38,10 @@ export default class DashboardController {
|
|
|
34
38
|
// ---------------------------------------------------------------------------
|
|
35
39
|
// Page
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
|
-
/**
|
|
38
|
-
* GET {dashboardPath} — Render the dashboard Edge template.
|
|
39
|
-
*
|
|
40
|
-
* Reads the dashboard CSS/JS assets and passes them as template state
|
|
41
|
-
* along with configuration for tracing and custom panes.
|
|
42
|
-
*/
|
|
43
41
|
async page(ctx) {
|
|
44
42
|
if (!this.checkAccess(ctx)) {
|
|
45
43
|
return ctx.response.forbidden({ error: 'Access denied' });
|
|
46
44
|
}
|
|
47
|
-
// Lazily read and cache the CSS/JS assets
|
|
48
45
|
if (!this.cachedCss) {
|
|
49
46
|
this.cachedCss = readFileSync(join(EDGE_DIR, 'client', 'dashboard.css'), 'utf-8');
|
|
50
47
|
}
|
|
@@ -52,7 +49,13 @@ export default class DashboardController {
|
|
|
52
49
|
this.cachedJs = readFileSync(join(EDGE_DIR, 'client', 'dashboard.js'), 'utf-8');
|
|
53
50
|
}
|
|
54
51
|
if (this.cachedTransmitClient === null) {
|
|
55
|
-
this.cachedTransmitClient = this.
|
|
52
|
+
this.cachedTransmitClient = loadTransmitClient(this.app.makePath('package.json'));
|
|
53
|
+
if (this.cachedTransmitClient) {
|
|
54
|
+
console.log('[server-stats] Transmit client loaded for dashboard');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log('[server-stats] Dashboard will use polling. Install @adonisjs/transmit-client for real-time updates.');
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
const config = this.app.config.get('server_stats');
|
|
58
61
|
const toolbarConfig = config?.devToolbar || {};
|
|
@@ -77,63 +80,21 @@ export default class DashboardController {
|
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
78
81
|
// Overview
|
|
79
82
|
// ---------------------------------------------------------------------------
|
|
80
|
-
/**
|
|
81
|
-
* GET {dashboardPath}/api/overview — Overview metrics cards.
|
|
82
|
-
*/
|
|
83
83
|
async overview({ request, response }) {
|
|
84
|
-
|
|
85
|
-
if (!db)
|
|
86
|
-
return response.json(emptyOverview());
|
|
87
|
-
try {
|
|
84
|
+
return this.withDb(response, emptyOverview(), async () => {
|
|
88
85
|
const range = request.qs().range || '1h';
|
|
89
|
-
|
|
90
|
-
const [overview, widgets] = await Promise.all([
|
|
86
|
+
const [overview, widgets, sparklineData] = await Promise.all([
|
|
91
87
|
this.dashboardStore.getOverviewMetrics(range),
|
|
92
88
|
this.dashboardStore.getOverviewWidgets(range),
|
|
89
|
+
this.dashboardStore.getSparklineData(range),
|
|
93
90
|
]);
|
|
94
91
|
if (!overview)
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const sparklineData = metrics.slice(-15);
|
|
102
|
-
// Fetch cache and queue stats (non-blocking — null if unavailable)
|
|
103
|
-
let cacheStats = null;
|
|
104
|
-
let jobQueueStatus = null;
|
|
105
|
-
try {
|
|
106
|
-
const cacheInspector = await this.getCacheInspector();
|
|
107
|
-
if (cacheInspector) {
|
|
108
|
-
const stats = await cacheInspector.getStats();
|
|
109
|
-
cacheStats = {
|
|
110
|
-
available: true,
|
|
111
|
-
totalKeys: stats.totalKeys,
|
|
112
|
-
hitRate: stats.hitRate,
|
|
113
|
-
memoryUsedHuman: stats.memoryUsedHuman,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// Cache not available
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const queueInspector = await this.getQueueInspector();
|
|
122
|
-
if (queueInspector) {
|
|
123
|
-
const qOverview = await queueInspector.getOverview();
|
|
124
|
-
jobQueueStatus = {
|
|
125
|
-
available: true,
|
|
126
|
-
active: qOverview.active,
|
|
127
|
-
waiting: qOverview.waiting,
|
|
128
|
-
failed: qOverview.failed,
|
|
129
|
-
completed: qOverview.completed,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
// Queue not available
|
|
135
|
-
}
|
|
136
|
-
return response.json({
|
|
92
|
+
return emptyOverview();
|
|
93
|
+
const [cacheStats, jobQueueStatus] = await Promise.all([
|
|
94
|
+
this.fetchCacheOverview(),
|
|
95
|
+
this.fetchQueueOverview(),
|
|
96
|
+
]);
|
|
97
|
+
return {
|
|
137
98
|
...overview,
|
|
138
99
|
sparklines: {
|
|
139
100
|
avgResponseTime: sparklineData.map((m) => m.avg_duration),
|
|
@@ -144,26 +105,14 @@ export default class DashboardController {
|
|
|
144
105
|
...widgets,
|
|
145
106
|
cacheStats,
|
|
146
107
|
jobQueueStatus,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return response.json(emptyOverview());
|
|
151
|
-
}
|
|
108
|
+
};
|
|
109
|
+
});
|
|
152
110
|
}
|
|
153
|
-
/**
|
|
154
|
-
* GET {dashboardPath}/api/overview/chart — Chart data with time range.
|
|
155
|
-
*/
|
|
156
111
|
async overviewChart({ request, response }) {
|
|
157
|
-
const db = this.dashboardStore.getDb();
|
|
158
|
-
if (!db)
|
|
159
|
-
return response.json({ buckets: [] });
|
|
160
112
|
const range = request.qs().range || '1h';
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.where('bucket', '>=', cutoff)
|
|
165
|
-
.orderBy('bucket', 'asc');
|
|
166
|
-
return response.json({
|
|
113
|
+
return this.withDb(response, { range, buckets: [] }, async () => {
|
|
114
|
+
const buckets = await this.dashboardStore.getChartData(range);
|
|
115
|
+
return {
|
|
167
116
|
range,
|
|
168
117
|
buckets: buckets.map((b) => ({
|
|
169
118
|
bucket: b.bucket,
|
|
@@ -173,70 +122,42 @@ export default class DashboardController {
|
|
|
173
122
|
errorCount: b.error_count,
|
|
174
123
|
queryCount: b.query_count,
|
|
175
124
|
})),
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return response.json({ buckets: [] });
|
|
180
|
-
}
|
|
125
|
+
};
|
|
126
|
+
});
|
|
181
127
|
}
|
|
182
128
|
// ---------------------------------------------------------------------------
|
|
183
129
|
// Requests
|
|
184
130
|
// ---------------------------------------------------------------------------
|
|
185
|
-
/**
|
|
186
|
-
* GET {dashboardPath}/api/requests — Paginated request history.
|
|
187
|
-
*/
|
|
188
131
|
async requests({ request, response }) {
|
|
189
|
-
const db = this.dashboardStore.getDb();
|
|
190
|
-
if (!db)
|
|
191
|
-
return response.json({ data: [], total: 0 });
|
|
192
132
|
const qs = request.qs();
|
|
193
133
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
194
134
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
query = query.where('status_code', Number(qs.status));
|
|
201
|
-
if (qs.url)
|
|
202
|
-
query = query.where('url', 'like', `%${qs.url}%`);
|
|
203
|
-
const countResult = await query.clone().count('* as total').first();
|
|
204
|
-
const total = countResult?.total ?? 0;
|
|
205
|
-
const data = await query
|
|
206
|
-
.orderBy('created_at', 'desc')
|
|
207
|
-
.limit(perPage)
|
|
208
|
-
.offset((page - 1) * perPage);
|
|
209
|
-
return response.json({
|
|
210
|
-
data: data.map(formatRequest),
|
|
211
|
-
total,
|
|
212
|
-
page,
|
|
213
|
-
perPage,
|
|
135
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
136
|
+
const result = await this.dashboardStore.getRequests(page, perPage, {
|
|
137
|
+
method: qs.method ? qs.method.toUpperCase() : undefined,
|
|
138
|
+
url: qs.url || undefined,
|
|
139
|
+
status: qs.status ? Number(qs.status) : undefined,
|
|
214
140
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
141
|
+
return {
|
|
142
|
+
data: result.data.map(formatRequest),
|
|
143
|
+
total: result.total,
|
|
144
|
+
page: result.page,
|
|
145
|
+
perPage: result.perPage,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
219
148
|
}
|
|
220
|
-
/**
|
|
221
|
-
* GET {dashboardPath}/api/requests/:id — Single request with trace.
|
|
222
|
-
*/
|
|
223
149
|
async requestDetail({ params, response }) {
|
|
224
|
-
|
|
225
|
-
if (!db)
|
|
150
|
+
if (!this.dashboardStore.isReady()) {
|
|
226
151
|
return response.notFound({ error: 'Not found' });
|
|
152
|
+
}
|
|
227
153
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (!req)
|
|
154
|
+
const detail = await this.dashboardStore.getRequestDetail(Number(params.id));
|
|
155
|
+
if (!detail)
|
|
231
156
|
return response.notFound({ error: 'Request not found' });
|
|
232
|
-
const [queries, trace] = await Promise.all([
|
|
233
|
-
db('server_stats_queries').where('request_id', id).orderBy('created_at', 'asc'),
|
|
234
|
-
db('server_stats_traces').where('request_id', id).first(),
|
|
235
|
-
]);
|
|
236
157
|
return response.json({
|
|
237
|
-
...formatRequest(
|
|
238
|
-
queries: queries.map(formatQuery),
|
|
239
|
-
trace: trace ? formatTrace(trace) : null,
|
|
158
|
+
...formatRequest(detail),
|
|
159
|
+
queries: (detail.queries || []).map(formatQuery),
|
|
160
|
+
trace: detail.trace ? formatTrace(detail.trace) : null,
|
|
240
161
|
});
|
|
241
162
|
}
|
|
242
163
|
catch {
|
|
@@ -246,73 +167,33 @@ export default class DashboardController {
|
|
|
246
167
|
// ---------------------------------------------------------------------------
|
|
247
168
|
// Queries
|
|
248
169
|
// ---------------------------------------------------------------------------
|
|
249
|
-
/**
|
|
250
|
-
* GET {dashboardPath}/api/queries — Paginated query history.
|
|
251
|
-
*/
|
|
252
170
|
async queries({ request, response }) {
|
|
253
|
-
const db = this.dashboardStore.getDb();
|
|
254
|
-
if (!db)
|
|
255
|
-
return response.json({ data: [], total: 0 });
|
|
256
171
|
const qs = request.qs();
|
|
257
172
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
258
173
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (qs.method)
|
|
266
|
-
query = query.where('method', qs.method);
|
|
267
|
-
if (qs.connection)
|
|
268
|
-
query = query.where('connection', qs.connection);
|
|
269
|
-
const countResult = await query.clone().count('* as total').first();
|
|
270
|
-
const total = countResult?.total ?? 0;
|
|
271
|
-
const data = await query
|
|
272
|
-
.orderBy('created_at', 'desc')
|
|
273
|
-
.limit(perPage)
|
|
274
|
-
.offset((page - 1) * perPage);
|
|
275
|
-
return response.json({
|
|
276
|
-
data: data.map(formatQuery),
|
|
277
|
-
total,
|
|
278
|
-
page,
|
|
279
|
-
perPage,
|
|
174
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
175
|
+
const result = await this.dashboardStore.getQueries(page, perPage, {
|
|
176
|
+
durationMin: qs.duration_min ? Number(qs.duration_min) : undefined,
|
|
177
|
+
model: qs.model || undefined,
|
|
178
|
+
method: qs.method || undefined,
|
|
179
|
+
connection: qs.connection || undefined,
|
|
280
180
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
181
|
+
return {
|
|
182
|
+
data: result.data.map(formatQuery),
|
|
183
|
+
total: result.total,
|
|
184
|
+
page: result.page,
|
|
185
|
+
perPage: result.perPage,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
285
188
|
}
|
|
286
|
-
/**
|
|
287
|
-
* GET {dashboardPath}/api/queries/grouped — Grouped by normalized SQL.
|
|
288
|
-
*/
|
|
289
189
|
async queriesGrouped({ request, response }) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const sort = qs.sort || 'total_duration';
|
|
296
|
-
try {
|
|
297
|
-
const validSorts = {
|
|
298
|
-
count: 'count',
|
|
299
|
-
avg_duration: 'avg_duration',
|
|
300
|
-
total_duration: 'total_duration',
|
|
301
|
-
};
|
|
302
|
-
const orderCol = validSorts[sort] || 'total_duration';
|
|
303
|
-
const groups = await db('server_stats_queries')
|
|
304
|
-
.select('sql_normalized')
|
|
305
|
-
.select(db.raw('COUNT(*) as count'))
|
|
306
|
-
.select(db.raw('AVG(duration) as avg_duration'))
|
|
307
|
-
.select(db.raw('MIN(duration) as min_duration'))
|
|
308
|
-
.select(db.raw('MAX(duration) as max_duration'))
|
|
309
|
-
.select(db.raw('SUM(duration) as total_duration'))
|
|
310
|
-
.groupBy('sql_normalized')
|
|
311
|
-
.orderBy(orderCol, 'desc')
|
|
312
|
-
.limit(limit);
|
|
313
|
-
// Calculate total time for percentage
|
|
190
|
+
return this.withDb(response, { groups: [] }, async () => {
|
|
191
|
+
const qs = request.qs();
|
|
192
|
+
const limit = clamp(Number(qs.limit) || 50, 1, 200);
|
|
193
|
+
const sort = qs.sort || 'total_duration';
|
|
194
|
+
const groups = await this.dashboardStore.getQueriesGrouped(limit, sort);
|
|
314
195
|
const totalTime = groups.reduce((sum, g) => sum + (g.total_duration || 0), 0);
|
|
315
|
-
return
|
|
196
|
+
return {
|
|
316
197
|
groups: groups.map((g) => ({
|
|
317
198
|
sqlNormalized: g.sql_normalized,
|
|
318
199
|
count: g.count,
|
|
@@ -322,30 +203,23 @@ export default class DashboardController {
|
|
|
322
203
|
totalDuration: round(g.total_duration),
|
|
323
204
|
percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
|
|
324
205
|
})),
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return response.json({ groups: [] });
|
|
329
|
-
}
|
|
206
|
+
};
|
|
207
|
+
});
|
|
330
208
|
}
|
|
331
|
-
/**
|
|
332
|
-
* GET {dashboardPath}/api/queries/:id/explain — Run EXPLAIN on a query.
|
|
333
|
-
*/
|
|
334
209
|
async queryExplain({ params, response }) {
|
|
335
|
-
|
|
336
|
-
if (!db)
|
|
210
|
+
if (!this.dashboardStore.isReady()) {
|
|
337
211
|
return response.notFound({ error: 'Not found' });
|
|
212
|
+
}
|
|
338
213
|
try {
|
|
214
|
+
const db = this.dashboardStore.getDb();
|
|
339
215
|
const id = Number(params.id);
|
|
340
216
|
const query = await db('server_stats_queries').where('id', id).first();
|
|
341
217
|
if (!query)
|
|
342
218
|
return response.notFound({ error: 'Query not found' });
|
|
343
|
-
// Only allow EXPLAIN on SELECT queries
|
|
344
219
|
const sqlTrimmed = query.sql_text.trim().toUpperCase();
|
|
345
220
|
if (!sqlTrimmed.startsWith('SELECT')) {
|
|
346
221
|
return response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
|
|
347
222
|
}
|
|
348
|
-
// Run EXPLAIN on the app's default database connection (not the stats connection)
|
|
349
223
|
let appDb;
|
|
350
224
|
try {
|
|
351
225
|
const lucid = await this.app.container.make('lucid.db');
|
|
@@ -354,7 +228,6 @@ export default class DashboardController {
|
|
|
354
228
|
catch {
|
|
355
229
|
return response.serviceUnavailable({ error: 'App database connection not available' });
|
|
356
230
|
}
|
|
357
|
-
// Parse stored bindings and pass them to the EXPLAIN query
|
|
358
231
|
let bindings = [];
|
|
359
232
|
if (query.bindings) {
|
|
360
233
|
try {
|
|
@@ -365,22 +238,15 @@ export default class DashboardController {
|
|
|
365
238
|
}
|
|
366
239
|
}
|
|
367
240
|
const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
|
|
368
|
-
// PostgreSQL with Knex: result is { rows: [...] }
|
|
369
|
-
// Each row has a "QUERY PLAN" key containing the JSON plan
|
|
370
241
|
let plan = [];
|
|
371
242
|
const rawRows = explainResult?.rows ?? (Array.isArray(explainResult) ? explainResult : []);
|
|
372
243
|
if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
|
|
373
|
-
// EXPLAIN (FORMAT JSON) returns a single row with a JSON array
|
|
374
244
|
plan = rawRows[0]['QUERY PLAN'];
|
|
375
245
|
}
|
|
376
246
|
else {
|
|
377
247
|
plan = rawRows;
|
|
378
248
|
}
|
|
379
|
-
return response.json({
|
|
380
|
-
queryId: id,
|
|
381
|
-
sql: query.sql_text,
|
|
382
|
-
plan,
|
|
383
|
-
});
|
|
249
|
+
return response.json({ queryId: id, sql: query.sql_text, plan });
|
|
384
250
|
}
|
|
385
251
|
catch (error) {
|
|
386
252
|
return response.internalServerError({
|
|
@@ -392,49 +258,31 @@ export default class DashboardController {
|
|
|
392
258
|
// ---------------------------------------------------------------------------
|
|
393
259
|
// Events
|
|
394
260
|
// ---------------------------------------------------------------------------
|
|
395
|
-
/**
|
|
396
|
-
* GET {dashboardPath}/api/events — Paginated event history.
|
|
397
|
-
*/
|
|
398
261
|
async events({ request, response }) {
|
|
399
|
-
const db = this.dashboardStore.getDb();
|
|
400
|
-
if (!db)
|
|
401
|
-
return response.json({ data: [], total: 0 });
|
|
402
262
|
const qs = request.qs();
|
|
403
263
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
404
264
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const data = await query
|
|
412
|
-
.orderBy('created_at', 'desc')
|
|
413
|
-
.limit(perPage)
|
|
414
|
-
.offset((page - 1) * perPage);
|
|
415
|
-
return response.json({
|
|
416
|
-
data: data.map((e) => ({
|
|
265
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
266
|
+
const result = await this.dashboardStore.getEvents(page, perPage, {
|
|
267
|
+
eventName: qs.event_name || undefined,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
data: result.data.map((e) => ({
|
|
417
271
|
id: e.id,
|
|
418
272
|
requestId: e.request_id,
|
|
419
273
|
eventName: e.event_name,
|
|
420
274
|
data: safeParseJson(e.data),
|
|
421
275
|
createdAt: e.created_at,
|
|
422
276
|
})),
|
|
423
|
-
total,
|
|
424
|
-
page,
|
|
425
|
-
perPage,
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
return response.json({ data: [], total: 0 });
|
|
430
|
-
}
|
|
277
|
+
total: result.total,
|
|
278
|
+
page: result.page,
|
|
279
|
+
perPage: result.perPage,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
431
282
|
}
|
|
432
283
|
// ---------------------------------------------------------------------------
|
|
433
284
|
// Routes
|
|
434
285
|
// ---------------------------------------------------------------------------
|
|
435
|
-
/**
|
|
436
|
-
* GET {dashboardPath}/api/routes — Route table (delegates to DebugStore).
|
|
437
|
-
*/
|
|
438
286
|
async routes({ response }) {
|
|
439
287
|
const routes = this.debugStore.routes.getRoutes();
|
|
440
288
|
return response.json({
|
|
@@ -445,46 +293,34 @@ export default class DashboardController {
|
|
|
445
293
|
// ---------------------------------------------------------------------------
|
|
446
294
|
// Logs
|
|
447
295
|
// ---------------------------------------------------------------------------
|
|
448
|
-
/**
|
|
449
|
-
* GET {dashboardPath}/api/logs — Paginated logs with structured search.
|
|
450
|
-
*/
|
|
451
296
|
async logs({ request, response }) {
|
|
452
|
-
const db = this.dashboardStore.getDb();
|
|
453
|
-
if (!db)
|
|
454
|
-
return response.json({ data: [], total: 0 });
|
|
455
297
|
const qs = request.qs();
|
|
456
298
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
457
299
|
const perPage = clamp(Number(qs.perPage) || 50, 1, 200);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const data = await query
|
|
483
|
-
.orderBy('created_at', 'desc')
|
|
484
|
-
.limit(perPage)
|
|
485
|
-
.offset((page - 1) * perPage);
|
|
486
|
-
return response.json({
|
|
487
|
-
data: data.map((l) => ({
|
|
300
|
+
// Build structured filters from query string
|
|
301
|
+
const structured = [];
|
|
302
|
+
if (qs.field && qs.value !== undefined) {
|
|
303
|
+
const op = qs.operator || 'equals';
|
|
304
|
+
const operatorMap = {
|
|
305
|
+
equals: 'equals',
|
|
306
|
+
contains: 'contains',
|
|
307
|
+
starts_with: 'startsWith',
|
|
308
|
+
};
|
|
309
|
+
structured.push({
|
|
310
|
+
field: qs.field,
|
|
311
|
+
operator: operatorMap[op] || 'equals',
|
|
312
|
+
value: qs.value,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
316
|
+
const result = await this.dashboardStore.getLogs(page, perPage, {
|
|
317
|
+
level: qs.level || undefined,
|
|
318
|
+
search: qs.message || undefined,
|
|
319
|
+
requestId: qs.request_id || undefined,
|
|
320
|
+
structured: structured.length > 0 ? structured : undefined,
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
data: result.data.map((l) => ({
|
|
488
324
|
id: l.id,
|
|
489
325
|
level: l.level,
|
|
490
326
|
message: l.message,
|
|
@@ -492,75 +328,43 @@ export default class DashboardController {
|
|
|
492
328
|
data: safeParseJson(l.data),
|
|
493
329
|
createdAt: l.created_at,
|
|
494
330
|
})),
|
|
495
|
-
total,
|
|
496
|
-
page,
|
|
497
|
-
perPage,
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
return response.json({ data: [], total: 0 });
|
|
502
|
-
}
|
|
331
|
+
total: result.total,
|
|
332
|
+
page: result.page,
|
|
333
|
+
perPage: result.perPage,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
503
336
|
}
|
|
504
337
|
// ---------------------------------------------------------------------------
|
|
505
338
|
// Emails
|
|
506
339
|
// ---------------------------------------------------------------------------
|
|
507
|
-
/**
|
|
508
|
-
* GET {dashboardPath}/api/emails — Paginated email history.
|
|
509
|
-
*/
|
|
510
340
|
async emails({ request, response }) {
|
|
511
|
-
const db = this.dashboardStore.getDb();
|
|
512
|
-
if (!db)
|
|
513
|
-
return response.json({ data: [], total: 0 });
|
|
514
341
|
const qs = request.qs();
|
|
515
342
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
516
343
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const data = await query
|
|
533
|
-
.select('id', 'from_addr', 'to_addr', 'cc', 'bcc', 'subject', 'mailer', 'status', 'message_id', 'attachment_count', 'created_at')
|
|
534
|
-
.orderBy('created_at', 'desc')
|
|
535
|
-
.limit(perPage)
|
|
536
|
-
.offset((page - 1) * perPage);
|
|
537
|
-
return response.json({
|
|
538
|
-
data: data.map(formatEmail),
|
|
539
|
-
total,
|
|
540
|
-
page,
|
|
541
|
-
perPage,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
catch {
|
|
545
|
-
return response.json({ data: [], total: 0 });
|
|
546
|
-
}
|
|
344
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
345
|
+
const result = await this.dashboardStore.getEmails(page, perPage, {
|
|
346
|
+
from: qs.from || undefined,
|
|
347
|
+
to: qs.to || undefined,
|
|
348
|
+
subject: qs.subject || undefined,
|
|
349
|
+
status: qs.status || undefined,
|
|
350
|
+
mailer: qs.mailer || undefined,
|
|
351
|
+
}, true);
|
|
352
|
+
return {
|
|
353
|
+
data: result.data.map(formatEmail),
|
|
354
|
+
total: result.total,
|
|
355
|
+
page: result.page,
|
|
356
|
+
perPage: result.perPage,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
547
359
|
}
|
|
548
|
-
/**
|
|
549
|
-
* GET {dashboardPath}/api/emails/:id/preview — Email HTML preview.
|
|
550
|
-
*/
|
|
551
360
|
async emailPreview({ params, response }) {
|
|
552
|
-
|
|
553
|
-
if (!db)
|
|
361
|
+
if (!this.dashboardStore.isReady()) {
|
|
554
362
|
return response.notFound({ error: 'Not found' });
|
|
363
|
+
}
|
|
555
364
|
try {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
.where('id', id)
|
|
559
|
-
.select('html', 'text_body')
|
|
560
|
-
.first();
|
|
561
|
-
if (!email)
|
|
365
|
+
const html = await this.dashboardStore.getEmailHtml(Number(params.id));
|
|
366
|
+
if (!html)
|
|
562
367
|
return response.notFound({ error: 'Email not found' });
|
|
563
|
-
const html = email.html || email.text_body || '<p>No content</p>';
|
|
564
368
|
return response.header('Content-Type', 'text/html; charset=utf-8').send(html);
|
|
565
369
|
}
|
|
566
370
|
catch {
|
|
@@ -570,27 +374,14 @@ export default class DashboardController {
|
|
|
570
374
|
// ---------------------------------------------------------------------------
|
|
571
375
|
// Traces
|
|
572
376
|
// ---------------------------------------------------------------------------
|
|
573
|
-
/**
|
|
574
|
-
* GET {dashboardPath}/api/traces — Paginated trace list (lightweight).
|
|
575
|
-
*/
|
|
576
377
|
async traces({ request, response }) {
|
|
577
|
-
const db = this.dashboardStore.getDb();
|
|
578
|
-
if (!db)
|
|
579
|
-
return response.json({ data: [], total: 0 });
|
|
580
378
|
const qs = request.qs();
|
|
581
379
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
582
380
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const data = await db('server_stats_traces')
|
|
588
|
-
.select('id', 'request_id', 'method', 'url', 'status_code', 'total_duration', 'span_count', 'warnings', 'created_at')
|
|
589
|
-
.orderBy('created_at', 'desc')
|
|
590
|
-
.limit(perPage)
|
|
591
|
-
.offset((page - 1) * perPage);
|
|
592
|
-
return response.json({
|
|
593
|
-
data: data.map((t) => ({
|
|
381
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
382
|
+
const result = await this.dashboardStore.getTraces(page, perPage);
|
|
383
|
+
return {
|
|
384
|
+
data: result.data.map((t) => ({
|
|
594
385
|
id: t.id,
|
|
595
386
|
requestId: t.request_id,
|
|
596
387
|
method: t.method,
|
|
@@ -601,25 +392,18 @@ export default class DashboardController {
|
|
|
601
392
|
warningCount: safeParseJsonArray(t.warnings).length,
|
|
602
393
|
createdAt: t.created_at,
|
|
603
394
|
})),
|
|
604
|
-
total,
|
|
605
|
-
page,
|
|
606
|
-
perPage,
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch {
|
|
610
|
-
return response.json({ data: [], total: 0 });
|
|
611
|
-
}
|
|
395
|
+
total: result.total,
|
|
396
|
+
page: result.page,
|
|
397
|
+
perPage: result.perPage,
|
|
398
|
+
};
|
|
399
|
+
});
|
|
612
400
|
}
|
|
613
|
-
/**
|
|
614
|
-
* GET {dashboardPath}/api/traces/:id — Single trace with full spans.
|
|
615
|
-
*/
|
|
616
401
|
async traceDetail({ params, response }) {
|
|
617
|
-
|
|
618
|
-
if (!db)
|
|
402
|
+
if (!this.dashboardStore.isReady()) {
|
|
619
403
|
return response.notFound({ error: 'Not found' });
|
|
404
|
+
}
|
|
620
405
|
try {
|
|
621
|
-
const
|
|
622
|
-
const trace = await db('server_stats_traces').where('id', id).first();
|
|
406
|
+
const trace = await this.dashboardStore.getTraceDetail(Number(params.id));
|
|
623
407
|
if (!trace)
|
|
624
408
|
return response.notFound({ error: 'Trace not found' });
|
|
625
409
|
return response.json(formatTrace(trace));
|
|
@@ -631,11 +415,8 @@ export default class DashboardController {
|
|
|
631
415
|
// ---------------------------------------------------------------------------
|
|
632
416
|
// Cache
|
|
633
417
|
// ---------------------------------------------------------------------------
|
|
634
|
-
/**
|
|
635
|
-
* GET {dashboardPath}/api/cache — Cache stats and key list.
|
|
636
|
-
*/
|
|
637
418
|
async cacheStats({ request, response }) {
|
|
638
|
-
const inspector = await this.
|
|
419
|
+
const inspector = await this.getInspector('cache');
|
|
639
420
|
if (!inspector) {
|
|
640
421
|
return response.json({ available: false, stats: null, keys: [] });
|
|
641
422
|
}
|
|
@@ -659,11 +440,8 @@ export default class DashboardController {
|
|
|
659
440
|
return response.json({ available: false, stats: null, keys: [] });
|
|
660
441
|
}
|
|
661
442
|
}
|
|
662
|
-
/**
|
|
663
|
-
* GET {dashboardPath}/api/cache/:key — Single cache key detail.
|
|
664
|
-
*/
|
|
665
443
|
async cacheKey({ params, response }) {
|
|
666
|
-
const inspector = await this.
|
|
444
|
+
const inspector = await this.getInspector('cache');
|
|
667
445
|
if (!inspector) {
|
|
668
446
|
return response.notFound({ error: 'Cache not available' });
|
|
669
447
|
}
|
|
@@ -681,11 +459,8 @@ export default class DashboardController {
|
|
|
681
459
|
// ---------------------------------------------------------------------------
|
|
682
460
|
// Jobs / Queue
|
|
683
461
|
// ---------------------------------------------------------------------------
|
|
684
|
-
/**
|
|
685
|
-
* GET {dashboardPath}/api/jobs — Job list with status filter.
|
|
686
|
-
*/
|
|
687
462
|
async jobs({ request, response }) {
|
|
688
|
-
const inspector = await this.
|
|
463
|
+
const inspector = await this.getInspector('queue');
|
|
689
464
|
if (!inspector) {
|
|
690
465
|
return response.json({ available: false, overview: null, jobs: [], total: 0 });
|
|
691
466
|
}
|
|
@@ -711,11 +486,8 @@ export default class DashboardController {
|
|
|
711
486
|
return response.json({ available: false, overview: null, jobs: [], total: 0 });
|
|
712
487
|
}
|
|
713
488
|
}
|
|
714
|
-
/**
|
|
715
|
-
* GET {dashboardPath}/api/jobs/:id — Single job detail.
|
|
716
|
-
*/
|
|
717
489
|
async jobDetail({ params, response }) {
|
|
718
|
-
const inspector = await this.
|
|
490
|
+
const inspector = await this.getInspector('queue');
|
|
719
491
|
if (!inspector) {
|
|
720
492
|
return response.notFound({ error: 'Queue not available' });
|
|
721
493
|
}
|
|
@@ -729,11 +501,8 @@ export default class DashboardController {
|
|
|
729
501
|
return response.notFound({ error: 'Job not found' });
|
|
730
502
|
}
|
|
731
503
|
}
|
|
732
|
-
/**
|
|
733
|
-
* POST {dashboardPath}/api/jobs/:id/retry — Retry a failed job.
|
|
734
|
-
*/
|
|
735
504
|
async jobRetry({ params, response }) {
|
|
736
|
-
const inspector = await this.
|
|
505
|
+
const inspector = await this.getInspector('queue');
|
|
737
506
|
if (!inspector) {
|
|
738
507
|
return response.notFound({ error: 'Queue not available' });
|
|
739
508
|
}
|
|
@@ -751,9 +520,6 @@ export default class DashboardController {
|
|
|
751
520
|
// ---------------------------------------------------------------------------
|
|
752
521
|
// Config
|
|
753
522
|
// ---------------------------------------------------------------------------
|
|
754
|
-
/**
|
|
755
|
-
* GET {dashboardPath}/api/config — Sanitized app config and env vars.
|
|
756
|
-
*/
|
|
757
523
|
async config({ response }) {
|
|
758
524
|
const configData = this.configInspector.getConfig();
|
|
759
525
|
const envData = this.configInspector.getEnvVars();
|
|
@@ -765,17 +531,10 @@ export default class DashboardController {
|
|
|
765
531
|
// ---------------------------------------------------------------------------
|
|
766
532
|
// Saved Filters
|
|
767
533
|
// ---------------------------------------------------------------------------
|
|
768
|
-
/**
|
|
769
|
-
* GET {dashboardPath}/api/filters — List saved filter presets.
|
|
770
|
-
*/
|
|
771
534
|
async savedFilters({ response }) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return
|
|
775
|
-
try {
|
|
776
|
-
const filters = await db('server_stats_saved_filters')
|
|
777
|
-
.orderBy('created_at', 'desc');
|
|
778
|
-
return response.json({
|
|
535
|
+
return this.withDb(response, { filters: [] }, async () => {
|
|
536
|
+
const filters = await this.dashboardStore.getSavedFilters();
|
|
537
|
+
return {
|
|
779
538
|
filters: filters.map((f) => ({
|
|
780
539
|
id: f.id,
|
|
781
540
|
name: f.name,
|
|
@@ -783,53 +542,38 @@ export default class DashboardController {
|
|
|
783
542
|
filterConfig: safeParseJson(f.filter_config),
|
|
784
543
|
createdAt: f.created_at,
|
|
785
544
|
})),
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
return response.json({ filters: [] });
|
|
790
|
-
}
|
|
545
|
+
};
|
|
546
|
+
});
|
|
791
547
|
}
|
|
792
|
-
/**
|
|
793
|
-
* POST {dashboardPath}/api/filters — Create a saved filter preset.
|
|
794
|
-
*/
|
|
795
548
|
async createSavedFilter({ request, response }) {
|
|
796
|
-
|
|
797
|
-
if (!db)
|
|
549
|
+
if (!this.dashboardStore.isReady()) {
|
|
798
550
|
return response.serviceUnavailable({ error: 'Database not available' });
|
|
551
|
+
}
|
|
799
552
|
try {
|
|
800
553
|
const body = request.body();
|
|
801
|
-
const name = body
|
|
802
|
-
const section = body.section;
|
|
803
|
-
const filterConfig = body.filterConfig;
|
|
554
|
+
const { name, section, filterConfig } = body;
|
|
804
555
|
if (!name || !section || !filterConfig) {
|
|
805
|
-
return response.badRequest({
|
|
556
|
+
return response.badRequest({
|
|
557
|
+
error: 'Missing required fields: name, section, filterConfig',
|
|
558
|
+
});
|
|
806
559
|
}
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
return response.json(
|
|
813
|
-
id,
|
|
814
|
-
name,
|
|
815
|
-
section,
|
|
816
|
-
filterConfig: typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig,
|
|
817
|
-
});
|
|
560
|
+
const configObj = typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig;
|
|
561
|
+
const result = await this.dashboardStore.createSavedFilter(name, section, configObj);
|
|
562
|
+
if (!result) {
|
|
563
|
+
return response.serviceUnavailable({ error: 'Database not available' });
|
|
564
|
+
}
|
|
565
|
+
return response.json(result);
|
|
818
566
|
}
|
|
819
567
|
catch {
|
|
820
568
|
return response.internalServerError({ error: 'Failed to create filter' });
|
|
821
569
|
}
|
|
822
570
|
}
|
|
823
|
-
/**
|
|
824
|
-
* DELETE {dashboardPath}/api/filters/:id — Delete a saved filter preset.
|
|
825
|
-
*/
|
|
826
571
|
async deleteSavedFilter({ params, response }) {
|
|
827
|
-
|
|
828
|
-
if (!db)
|
|
572
|
+
if (!this.dashboardStore.isReady()) {
|
|
829
573
|
return response.serviceUnavailable({ error: 'Database not available' });
|
|
574
|
+
}
|
|
830
575
|
try {
|
|
831
|
-
const
|
|
832
|
-
const deleted = await db('server_stats_saved_filters').where('id', id).del();
|
|
576
|
+
const deleted = await this.dashboardStore.deleteSavedFilter(Number(params.id));
|
|
833
577
|
if (!deleted)
|
|
834
578
|
return response.notFound({ error: 'Filter not found' });
|
|
835
579
|
return response.json({ success: true });
|
|
@@ -842,8 +586,19 @@ export default class DashboardController {
|
|
|
842
586
|
// Private helpers
|
|
843
587
|
// ---------------------------------------------------------------------------
|
|
844
588
|
/**
|
|
845
|
-
*
|
|
589
|
+
* Wraps a store call with null-guard + try/catch boilerplate.
|
|
590
|
+
* Returns emptyValue if the store is not ready or the fn throws.
|
|
846
591
|
*/
|
|
592
|
+
async withDb(response, emptyValue, fn) {
|
|
593
|
+
if (!this.dashboardStore.isReady())
|
|
594
|
+
return response.json(emptyValue);
|
|
595
|
+
try {
|
|
596
|
+
return response.json(await fn());
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return response.json(emptyValue);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
847
602
|
checkAccess(ctx) {
|
|
848
603
|
const config = this.app.config.get('server_stats');
|
|
849
604
|
if (!config?.shouldShow)
|
|
@@ -855,81 +610,90 @@ export default class DashboardController {
|
|
|
855
610
|
return false;
|
|
856
611
|
}
|
|
857
612
|
}
|
|
858
|
-
/**
|
|
859
|
-
* Get the configured dashboard path.
|
|
860
|
-
*/
|
|
861
613
|
getDashboardPath() {
|
|
862
614
|
const config = this.app.config.get('server_stats');
|
|
863
615
|
return config?.devToolbar?.dashboardPath ?? '/__stats';
|
|
864
616
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
617
|
+
async getInspector(type) {
|
|
618
|
+
if (type === 'cache') {
|
|
619
|
+
if (this.cacheAvailable === false)
|
|
620
|
+
return null;
|
|
621
|
+
if (this.cacheInspector)
|
|
622
|
+
return this.cacheInspector;
|
|
623
|
+
try {
|
|
624
|
+
const available = await CacheInspector.isAvailable(this.app);
|
|
625
|
+
this.cacheAvailable = available;
|
|
626
|
+
if (!available)
|
|
627
|
+
return null;
|
|
628
|
+
const redis = await this.app.container.make('redis');
|
|
629
|
+
this.cacheInspector = new CacheInspector(redis);
|
|
630
|
+
return this.cacheInspector;
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
this.cacheAvailable = false;
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
879
636
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
637
|
+
else {
|
|
638
|
+
if (this.queueAvailable === false)
|
|
639
|
+
return null;
|
|
640
|
+
if (this.queueInspector)
|
|
641
|
+
return this.queueInspector;
|
|
642
|
+
try {
|
|
643
|
+
const available = await QueueInspector.isAvailable(this.app);
|
|
644
|
+
this.queueAvailable = available;
|
|
645
|
+
if (!available)
|
|
646
|
+
return null;
|
|
647
|
+
const queue = await this.app.container.make('queue');
|
|
648
|
+
this.queueInspector = new QueueInspector(queue);
|
|
649
|
+
return this.queueInspector;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
this.queueAvailable = false;
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
884
655
|
}
|
|
885
656
|
}
|
|
886
|
-
/**
|
|
887
|
-
|
|
888
|
-
*/
|
|
889
|
-
async getCacheInspector() {
|
|
890
|
-
if (this.cacheAvailable === false)
|
|
891
|
-
return null;
|
|
892
|
-
if (this.cacheInspector)
|
|
893
|
-
return this.cacheInspector;
|
|
657
|
+
/** Fetch cache overview stats for the overview page. */
|
|
658
|
+
async fetchCacheOverview() {
|
|
894
659
|
try {
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
if (!available)
|
|
660
|
+
const inspector = await this.getInspector('cache');
|
|
661
|
+
if (!inspector)
|
|
898
662
|
return null;
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
663
|
+
const stats = await inspector.getStats();
|
|
664
|
+
return {
|
|
665
|
+
available: true,
|
|
666
|
+
totalKeys: stats.totalKeys,
|
|
667
|
+
hitRate: stats.hitRate,
|
|
668
|
+
memoryUsedHuman: stats.memoryUsedHuman,
|
|
669
|
+
};
|
|
902
670
|
}
|
|
903
671
|
catch {
|
|
904
|
-
this.cacheAvailable = false;
|
|
905
672
|
return null;
|
|
906
673
|
}
|
|
907
674
|
}
|
|
908
|
-
/**
|
|
909
|
-
|
|
910
|
-
*/
|
|
911
|
-
async getQueueInspector() {
|
|
912
|
-
if (this.queueAvailable === false)
|
|
913
|
-
return null;
|
|
914
|
-
if (this.queueInspector)
|
|
915
|
-
return this.queueInspector;
|
|
675
|
+
/** Fetch queue overview stats for the overview page. */
|
|
676
|
+
async fetchQueueOverview() {
|
|
916
677
|
try {
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
if (!available)
|
|
678
|
+
const inspector = await this.getInspector('queue');
|
|
679
|
+
if (!inspector)
|
|
920
680
|
return null;
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
681
|
+
const overview = await inspector.getOverview();
|
|
682
|
+
return {
|
|
683
|
+
available: true,
|
|
684
|
+
active: overview.active,
|
|
685
|
+
waiting: overview.waiting,
|
|
686
|
+
failed: overview.failed,
|
|
687
|
+
completed: overview.completed,
|
|
688
|
+
};
|
|
924
689
|
}
|
|
925
690
|
catch {
|
|
926
|
-
this.queueAvailable = false;
|
|
927
691
|
return null;
|
|
928
692
|
}
|
|
929
693
|
}
|
|
930
694
|
}
|
|
931
695
|
// ---------------------------------------------------------------------------
|
|
932
|
-
// Formatting helpers
|
|
696
|
+
// Formatting helpers (snake_case DB rows → camelCase API response)
|
|
933
697
|
// ---------------------------------------------------------------------------
|
|
934
698
|
function formatRequest(r) {
|
|
935
699
|
return {
|
|
@@ -1011,45 +775,3 @@ function emptyOverview() {
|
|
|
1011
775
|
jobQueueStatus: null,
|
|
1012
776
|
};
|
|
1013
777
|
}
|
|
1014
|
-
// ---------------------------------------------------------------------------
|
|
1015
|
-
// Utility functions
|
|
1016
|
-
// ---------------------------------------------------------------------------
|
|
1017
|
-
function round(n) {
|
|
1018
|
-
return Math.round(n * 100) / 100;
|
|
1019
|
-
}
|
|
1020
|
-
function clamp(value, min, max) {
|
|
1021
|
-
return Math.max(min, Math.min(max, value));
|
|
1022
|
-
}
|
|
1023
|
-
function minutesAgo(minutes) {
|
|
1024
|
-
const d = new Date(Date.now() - minutes * 60_000);
|
|
1025
|
-
return d.toISOString().replace('T', ' ').slice(0, 19);
|
|
1026
|
-
}
|
|
1027
|
-
function parseRange(range) {
|
|
1028
|
-
const rangeMap = {
|
|
1029
|
-
'5m': 5,
|
|
1030
|
-
'15m': 15,
|
|
1031
|
-
'30m': 30,
|
|
1032
|
-
'1h': 60,
|
|
1033
|
-
'6h': 360,
|
|
1034
|
-
'24h': 1440,
|
|
1035
|
-
'7d': 10080,
|
|
1036
|
-
};
|
|
1037
|
-
const minutes = rangeMap[range] ?? 60;
|
|
1038
|
-
return { cutoff: minutesAgo(minutes) };
|
|
1039
|
-
}
|
|
1040
|
-
function safeParseJson(value) {
|
|
1041
|
-
if (value === null || value === undefined)
|
|
1042
|
-
return null;
|
|
1043
|
-
if (typeof value !== 'string')
|
|
1044
|
-
return value;
|
|
1045
|
-
try {
|
|
1046
|
-
return JSON.parse(value);
|
|
1047
|
-
}
|
|
1048
|
-
catch {
|
|
1049
|
-
return value;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
function safeParseJsonArray(value) {
|
|
1053
|
-
const parsed = safeParseJson(value);
|
|
1054
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1055
|
-
}
|