adonisjs-server-stats 1.10.0 → 1.10.3
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 +23 -14
- package/dist/core/config-utils.d.ts +8 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/dashboard-data-controller.d.ts +16 -0
- package/dist/core/dashboard-data-helpers.d.ts +12 -0
- package/dist/core/debug-data-controller.d.ts +4 -0
- package/dist/core/define-config-helpers.d.ts +25 -0
- package/dist/core/feature-detect-helpers.d.ts +36 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/index.js +594 -509
- package/dist/core/log-utils-helpers.d.ts +13 -0
- package/dist/core/metrics.d.ts +3 -28
- package/dist/core/pagination.d.ts +0 -9
- package/dist/core/server-stats-controller.d.ts +6 -0
- package/dist/core/transmit-helpers.d.ts +7 -0
- package/dist/core/types-dashboard.d.ts +178 -0
- package/dist/core/types-diagnostics.d.ts +85 -0
- package/dist/core/types.d.ts +10 -442
- package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
- package/dist/react/CacheTab-2cw_rMzj.js +117 -0
- package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
- package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
- package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
- package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
- package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
- package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
- package/dist/react/EventsTab-CfVr7AiM.js +57 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
- package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
- package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
- package/dist/react/LogsTab-D8unMV5P.js +108 -0
- package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
- package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
- package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
- package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
- package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
- package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
- package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
- package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
- package/dist/react/index-CsImORX6.js +1121 -0
- package/dist/react/index.js +1 -1
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/app_collector.d.ts +0 -8
- package/dist/src/collectors/app_collector.js +45 -52
- package/dist/src/collectors/auto_detect.d.ts +0 -23
- package/dist/src/collectors/auto_detect.js +33 -55
- package/dist/src/collectors/db_pool_collector.d.ts +14 -16
- package/dist/src/collectors/db_pool_collector.js +72 -57
- package/dist/src/collectors/log_collector.d.ts +0 -47
- package/dist/src/collectors/log_collector.js +36 -65
- package/dist/src/collectors/queue_collector.d.ts +0 -20
- package/dist/src/collectors/queue_collector.js +60 -76
- package/dist/src/collectors/redis_collector.d.ts +10 -10
- package/dist/src/collectors/redis_collector.js +69 -66
- package/dist/src/config/deprecation_migration.d.ts +7 -0
- package/dist/src/config/deprecation_migration.js +201 -0
- package/dist/src/controller/debug_controller.d.ts +1 -1
- package/dist/src/controller/debug_controller.js +87 -81
- package/dist/src/dashboard/cache_handlers.d.ts +14 -0
- package/dist/src/dashboard/cache_handlers.js +52 -0
- package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
- package/dist/src/dashboard/chart_aggregator.js +68 -50
- package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
- package/dist/src/dashboard/coalesce_cache.js +47 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
- package/dist/src/dashboard/dashboard_controller.js +51 -544
- package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
- package/dist/src/dashboard/dashboard_page_assets.js +51 -0
- package/dist/src/dashboard/dashboard_store.d.ts +19 -218
- package/dist/src/dashboard/dashboard_store.js +115 -1116
- package/dist/src/dashboard/dashboard_types.d.ts +83 -0
- package/dist/src/dashboard/dashboard_types.js +4 -0
- package/dist/src/dashboard/detail_queries.d.ts +19 -0
- package/dist/src/dashboard/detail_queries.js +98 -0
- package/dist/src/dashboard/email_event_builder.d.ts +8 -0
- package/dist/src/dashboard/email_event_builder.js +65 -0
- package/dist/src/dashboard/explain_query.d.ts +8 -0
- package/dist/src/dashboard/explain_query.js +22 -0
- package/dist/src/dashboard/filter_handlers.d.ts +23 -0
- package/dist/src/dashboard/filter_handlers.js +56 -0
- package/dist/src/dashboard/filtered_queries.d.ts +15 -0
- package/dist/src/dashboard/filtered_queries.js +155 -0
- package/dist/src/dashboard/flush_manager.d.ts +25 -0
- package/dist/src/dashboard/flush_manager.js +107 -0
- package/dist/src/dashboard/format_helpers.d.ts +126 -0
- package/dist/src/dashboard/format_helpers.js +140 -0
- package/dist/src/dashboard/inspector_manager.d.ts +36 -0
- package/dist/src/dashboard/inspector_manager.js +102 -0
- package/dist/src/dashboard/integrations/config_inspector.js +11 -13
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
- package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
- package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
- package/dist/src/dashboard/jobs_handlers.js +61 -0
- package/dist/src/dashboard/knex_factory.d.ts +18 -0
- package/dist/src/dashboard/knex_factory.js +91 -0
- package/dist/src/dashboard/migrator.js +30 -159
- package/dist/src/dashboard/migrator_tables.d.ts +19 -0
- package/dist/src/dashboard/migrator_tables.js +153 -0
- package/dist/src/dashboard/overview_queries.d.ts +66 -0
- package/dist/src/dashboard/overview_queries.js +155 -0
- package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
- package/dist/src/dashboard/overview_query_runners.js +84 -0
- package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
- package/dist/src/dashboard/overview_store_queries.js +69 -0
- package/dist/src/dashboard/paginate_helper.d.ts +12 -0
- package/dist/src/dashboard/paginate_helper.js +33 -0
- package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
- package/dist/src/dashboard/query_explain_handler.js +80 -0
- package/dist/src/dashboard/read_queries.d.ts +32 -0
- package/dist/src/dashboard/read_queries.js +107 -0
- package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
- package/dist/src/dashboard/saved_filter_queries.js +24 -0
- package/dist/src/dashboard/storage_stats.d.ts +41 -0
- package/dist/src/dashboard/storage_stats.js +81 -0
- package/dist/src/dashboard/write_queue.d.ts +106 -0
- package/dist/src/dashboard/write_queue.js +225 -0
- package/dist/src/data/data_access.d.ts +2 -39
- package/dist/src/data/data_access.js +17 -193
- package/dist/src/data/data_access_helpers.d.ts +130 -0
- package/dist/src/data/data_access_helpers.js +212 -0
- package/dist/src/debug/debug_store.js +37 -32
- package/dist/src/debug/email_collector.d.ts +1 -10
- package/dist/src/debug/email_collector.js +78 -81
- package/dist/src/debug/event_collector.d.ts +0 -9
- package/dist/src/debug/event_collector.js +79 -62
- package/dist/src/debug/query_collector.js +23 -19
- package/dist/src/debug/route_inspector.d.ts +1 -5
- package/dist/src/debug/route_inspector.js +50 -51
- package/dist/src/debug/trace_collector.d.ts +9 -1
- package/dist/src/debug/trace_collector.js +21 -15
- package/dist/src/debug/types.d.ts +1 -1
- package/dist/src/define_config.d.ts +0 -65
- package/dist/src/define_config.js +93 -333
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/edge/plugin.d.ts +0 -16
- package/dist/src/edge/plugin.js +57 -64
- package/dist/src/engine/request_metrics.d.ts +1 -0
- package/dist/src/engine/request_metrics.js +32 -42
- package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -93
- package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
- package/dist/src/provider/auth_middleware_detector.js +97 -0
- package/dist/src/provider/boot_helpers.d.ts +20 -0
- package/dist/src/provider/boot_helpers.js +91 -0
- package/dist/src/provider/boot_initializer.d.ts +28 -0
- package/dist/src/provider/boot_initializer.js +35 -0
- package/dist/src/provider/dashboard_init.d.ts +30 -0
- package/dist/src/provider/dashboard_init.js +138 -0
- package/dist/src/provider/dashboard_setup.d.ts +25 -0
- package/dist/src/provider/dashboard_setup.js +78 -0
- package/dist/src/provider/diagnostics.d.ts +134 -0
- package/dist/src/provider/diagnostics.js +127 -0
- package/dist/src/provider/email_bridge.d.ts +43 -0
- package/dist/src/provider/email_bridge.js +80 -0
- package/dist/src/provider/email_helpers.d.ts +13 -0
- package/dist/src/provider/email_helpers.js +68 -0
- package/dist/src/provider/pino_hook.d.ts +17 -0
- package/dist/src/provider/pino_hook.js +35 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
- package/dist/src/provider/provider_helpers_extra.js +177 -0
- package/dist/src/provider/server_stats_provider.d.ts +39 -85
- package/dist/src/provider/server_stats_provider.js +132 -951
- package/dist/src/provider/shutdown_helpers.d.ts +43 -0
- package/dist/src/provider/shutdown_helpers.js +70 -0
- package/dist/src/provider/toolbar_setup.d.ts +57 -0
- package/dist/src/provider/toolbar_setup.js +141 -0
- package/dist/src/routes/dashboard_routes.d.ts +14 -0
- package/dist/src/routes/dashboard_routes.js +197 -0
- package/dist/src/routes/debug_routes.d.ts +14 -0
- package/dist/src/routes/debug_routes.js +101 -0
- package/dist/src/routes/register_routes.d.ts +0 -78
- package/dist/src/routes/register_routes.js +22 -352
- package/dist/src/routes/stats_routes.d.ts +5 -0
- package/dist/src/routes/stats_routes.js +14 -0
- package/dist/src/styles/components.css +96 -0
- package/dist/src/styles/dashboard.css +8 -90
- package/dist/src/styles/debug-panel.css +1 -31
- package/dist/src/types.d.ts +305 -14
- package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
- package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
- package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
- package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
- package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
- package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
- package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
- package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
- package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-C8MxnS7Q.js +1232 -0
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/CacheTab-CA8LB1J5.js +0 -123
- package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
- package/dist/react/EmailsTab-BDhEiomM.js +0 -153
- package/dist/react/EventsTab-CMfY98Rl.js +0 -63
- package/dist/react/LogsTab-CicucmVk.js +0 -103
- package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
- package/dist/react/index-Cflz9Ebj.js +0 -1069
- package/dist/vue/index-Dtgysd26.js +0 -1229
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { log } from '../utils/logger.js';
|
|
2
|
+
import { prepareRequestRows, prepareLogRows, flushRequests, flushEvents, flushEmails, flushLogs, hasWarned, markWarned, } from './write_queue.js';
|
|
3
|
+
const FLUSH_MS = 500;
|
|
4
|
+
const MAX_Q = 200;
|
|
5
|
+
export class FlushManager {
|
|
6
|
+
writeQueue = [];
|
|
7
|
+
pendingEvents = [];
|
|
8
|
+
pendingLogs = [];
|
|
9
|
+
pendingEmails = [];
|
|
10
|
+
flushTimer = null;
|
|
11
|
+
flushing = false;
|
|
12
|
+
db;
|
|
13
|
+
constructor(getDb) {
|
|
14
|
+
this.db = getDb;
|
|
15
|
+
}
|
|
16
|
+
persistRequest(input, dashboardPath) {
|
|
17
|
+
if (!this.db() || input.url.startsWith(dashboardPath))
|
|
18
|
+
return;
|
|
19
|
+
this.backpressure(this.writeQueue);
|
|
20
|
+
this.writeQueue.push(input);
|
|
21
|
+
this.scheduleFlush();
|
|
22
|
+
}
|
|
23
|
+
queueEvents(requestIndex, events) {
|
|
24
|
+
if (events.length > 0)
|
|
25
|
+
this.pendingEvents.push({ requestIndex, events });
|
|
26
|
+
}
|
|
27
|
+
recordLog(entry) {
|
|
28
|
+
if (!this.db())
|
|
29
|
+
return;
|
|
30
|
+
this.backpressure(this.pendingLogs);
|
|
31
|
+
this.pendingLogs.push(entry);
|
|
32
|
+
this.scheduleFlush();
|
|
33
|
+
}
|
|
34
|
+
recordEmail(record) {
|
|
35
|
+
if (!this.db())
|
|
36
|
+
return;
|
|
37
|
+
this.backpressure(this.pendingEmails);
|
|
38
|
+
this.pendingEmails.push(record);
|
|
39
|
+
this.scheduleFlush();
|
|
40
|
+
}
|
|
41
|
+
async stop() {
|
|
42
|
+
if (this.flushTimer) {
|
|
43
|
+
clearTimeout(this.flushTimer);
|
|
44
|
+
this.flushTimer = null;
|
|
45
|
+
}
|
|
46
|
+
await this.flush().catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
backpressure(q) {
|
|
49
|
+
if (q.length >= MAX_Q)
|
|
50
|
+
q.splice(0, Math.floor(MAX_Q / 4));
|
|
51
|
+
}
|
|
52
|
+
scheduleFlush() {
|
|
53
|
+
if (this.flushTimer)
|
|
54
|
+
return;
|
|
55
|
+
this.flushTimer = setTimeout(() => {
|
|
56
|
+
this.flushTimer = null;
|
|
57
|
+
this.flush().catch((err) => {
|
|
58
|
+
if (!hasWarned('flush')) {
|
|
59
|
+
markWarned('flush');
|
|
60
|
+
log.warn(`dashboard: flush failed — ${err?.message}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}, FLUSH_MS);
|
|
64
|
+
}
|
|
65
|
+
async flush() {
|
|
66
|
+
const db = this.db();
|
|
67
|
+
if (this.flushing || !db)
|
|
68
|
+
return;
|
|
69
|
+
this.flushing = true;
|
|
70
|
+
const snap = this.takeSnapshot();
|
|
71
|
+
if (!snap) {
|
|
72
|
+
this.flushing = false;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const pr = prepareRequestRows(snap.requests);
|
|
77
|
+
const pl = prepareLogRows(snap.logs);
|
|
78
|
+
await db.transaction(async (trx) => {
|
|
79
|
+
await flushRequests(trx, pr);
|
|
80
|
+
await flushEvents(trx, snap.events);
|
|
81
|
+
await flushEmails(trx, snap.emails);
|
|
82
|
+
await flushLogs(trx, pl);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (!hasWarned('flush')) {
|
|
87
|
+
markWarned('flush');
|
|
88
|
+
log.warn(`dashboard: flush transaction failed — ${err?.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
this.flushing = false;
|
|
93
|
+
}
|
|
94
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
95
|
+
if (this.writeQueue.length > 0 || this.pendingLogs.length > 0 || this.pendingEmails.length > 0)
|
|
96
|
+
this.scheduleFlush();
|
|
97
|
+
}
|
|
98
|
+
takeSnapshot() {
|
|
99
|
+
const requests = this.writeQueue.splice(0);
|
|
100
|
+
const logs = this.pendingLogs.splice(0);
|
|
101
|
+
const events = this.pendingEvents.splice(0);
|
|
102
|
+
const emails = this.pendingEmails.splice(0);
|
|
103
|
+
if (requests.length === 0 && logs.length === 0 && events.length === 0 && emails.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
return { requests, logs, events, emails };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export interface PaginatedResponse<T> {
|
|
2
|
+
data: T[];
|
|
3
|
+
meta: {
|
|
4
|
+
total: number;
|
|
5
|
+
page: number;
|
|
6
|
+
perPage: number;
|
|
7
|
+
lastPage: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface ChartBucket {
|
|
11
|
+
bucket: string;
|
|
12
|
+
requestCount: number;
|
|
13
|
+
avgDuration: number;
|
|
14
|
+
p95Duration: number;
|
|
15
|
+
errorCount: number;
|
|
16
|
+
queryCount: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function paginatedResponse<T>(data: T[], total: number, page: number, perPage: number): PaginatedResponse<T>;
|
|
19
|
+
export declare function emptyPage<T>(page: number, perPage: number): PaginatedResponse<T>;
|
|
20
|
+
export declare function formatRequest(r: Record<string, unknown>): {
|
|
21
|
+
httpRequestId?: {} | undefined;
|
|
22
|
+
id: unknown;
|
|
23
|
+
method: unknown;
|
|
24
|
+
url: unknown;
|
|
25
|
+
statusCode: unknown;
|
|
26
|
+
duration: unknown;
|
|
27
|
+
spanCount: unknown;
|
|
28
|
+
warningCount: unknown;
|
|
29
|
+
createdAt: unknown;
|
|
30
|
+
};
|
|
31
|
+
export declare function formatQuery(q: Record<string, unknown>): {
|
|
32
|
+
id: unknown;
|
|
33
|
+
requestId: unknown;
|
|
34
|
+
sql: unknown;
|
|
35
|
+
sqlNormalized: unknown;
|
|
36
|
+
bindings: unknown;
|
|
37
|
+
duration: unknown;
|
|
38
|
+
method: unknown;
|
|
39
|
+
model: unknown;
|
|
40
|
+
connection: unknown;
|
|
41
|
+
inTransaction: boolean;
|
|
42
|
+
createdAt: unknown;
|
|
43
|
+
};
|
|
44
|
+
export declare function formatTrace(t: Record<string, unknown>): {
|
|
45
|
+
httpRequestId?: {} | undefined;
|
|
46
|
+
id: unknown;
|
|
47
|
+
requestId: unknown;
|
|
48
|
+
method: unknown;
|
|
49
|
+
url: unknown;
|
|
50
|
+
statusCode: unknown;
|
|
51
|
+
totalDuration: unknown;
|
|
52
|
+
spanCount: unknown;
|
|
53
|
+
spans: {};
|
|
54
|
+
warnings: unknown[];
|
|
55
|
+
createdAt: unknown;
|
|
56
|
+
};
|
|
57
|
+
export declare function formatLog(l: Record<string, unknown>): {
|
|
58
|
+
id: unknown;
|
|
59
|
+
level: unknown;
|
|
60
|
+
message: unknown;
|
|
61
|
+
requestId: unknown;
|
|
62
|
+
data: unknown;
|
|
63
|
+
createdAt: unknown;
|
|
64
|
+
};
|
|
65
|
+
export declare function mapChartBucket(b: Record<string, unknown>): ChartBucket;
|
|
66
|
+
export declare function emptyOverview(): {
|
|
67
|
+
avgResponseTime: number;
|
|
68
|
+
p95ResponseTime: number;
|
|
69
|
+
requestsPerMinute: number;
|
|
70
|
+
errorRate: number;
|
|
71
|
+
sparklines: {
|
|
72
|
+
avgResponseTime: never[];
|
|
73
|
+
p95ResponseTime: never[];
|
|
74
|
+
requestsPerMinute: never[];
|
|
75
|
+
errorRate: never[];
|
|
76
|
+
};
|
|
77
|
+
slowestEndpoints: never[];
|
|
78
|
+
queryStats: {
|
|
79
|
+
total: number;
|
|
80
|
+
avgDuration: number;
|
|
81
|
+
perRequest: number;
|
|
82
|
+
};
|
|
83
|
+
recentErrors: never[];
|
|
84
|
+
topEvents: never[];
|
|
85
|
+
emailActivity: {
|
|
86
|
+
sent: number;
|
|
87
|
+
queued: number;
|
|
88
|
+
failed: number;
|
|
89
|
+
};
|
|
90
|
+
logLevelBreakdown: {
|
|
91
|
+
error: number;
|
|
92
|
+
warn: number;
|
|
93
|
+
info: number;
|
|
94
|
+
debug: number;
|
|
95
|
+
};
|
|
96
|
+
statusDistribution: {
|
|
97
|
+
'2xx': number;
|
|
98
|
+
'3xx': number;
|
|
99
|
+
'4xx': number;
|
|
100
|
+
'5xx': number;
|
|
101
|
+
};
|
|
102
|
+
slowestQueries: never[];
|
|
103
|
+
cacheStats: null;
|
|
104
|
+
jobQueueStatus: null;
|
|
105
|
+
};
|
|
106
|
+
/** Build sparklines from raw sparkline data. */
|
|
107
|
+
export declare function buildSparklines(data: Record<string, unknown>[]): {
|
|
108
|
+
avgResponseTime: unknown[];
|
|
109
|
+
p95ResponseTime: unknown[];
|
|
110
|
+
requestsPerMinute: unknown[];
|
|
111
|
+
errorRate: number[];
|
|
112
|
+
};
|
|
113
|
+
/** Format grouped query results. */
|
|
114
|
+
export declare function formatGroupedQuery(g: Record<string, unknown>, totalTime: number): {
|
|
115
|
+
sqlNormalized: unknown;
|
|
116
|
+
count: unknown;
|
|
117
|
+
avgDuration: number;
|
|
118
|
+
minDuration: number;
|
|
119
|
+
maxDuration: number;
|
|
120
|
+
totalDuration: number;
|
|
121
|
+
percentOfTotal: number;
|
|
122
|
+
};
|
|
123
|
+
/** Run EXPLAIN on a query, returning the plan. */
|
|
124
|
+
export declare function runExplain(appDb: {
|
|
125
|
+
raw: (sql: string, bindings: unknown[]) => Promise<Record<string, unknown>>;
|
|
126
|
+
}, query: Record<string, unknown>): Promise<unknown[]>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
|
|
2
|
+
export function paginatedResponse(data, total, page, perPage) {
|
|
3
|
+
return {
|
|
4
|
+
data,
|
|
5
|
+
meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function emptyPage(page, perPage) {
|
|
9
|
+
return { data: [], meta: { total: 0, page, perPage, lastPage: 1 } };
|
|
10
|
+
}
|
|
11
|
+
export function formatRequest(r) {
|
|
12
|
+
return {
|
|
13
|
+
id: r.id,
|
|
14
|
+
method: r.method,
|
|
15
|
+
url: r.url,
|
|
16
|
+
statusCode: r.status_code,
|
|
17
|
+
duration: r.duration,
|
|
18
|
+
spanCount: r.span_count,
|
|
19
|
+
warningCount: r.warning_count,
|
|
20
|
+
createdAt: r.created_at,
|
|
21
|
+
...(r.http_request_id ? { httpRequestId: r.http_request_id } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function formatQuery(q) {
|
|
25
|
+
return {
|
|
26
|
+
id: q.id,
|
|
27
|
+
requestId: q.request_id,
|
|
28
|
+
sql: q.sql_text,
|
|
29
|
+
sqlNormalized: q.sql_normalized,
|
|
30
|
+
bindings: safeParseJson(q.bindings),
|
|
31
|
+
duration: q.duration,
|
|
32
|
+
method: q.method,
|
|
33
|
+
model: q.model,
|
|
34
|
+
connection: q.connection,
|
|
35
|
+
inTransaction: !!q.in_transaction,
|
|
36
|
+
createdAt: q.created_at,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function formatTrace(t) {
|
|
40
|
+
return {
|
|
41
|
+
id: t.id,
|
|
42
|
+
requestId: t.request_id,
|
|
43
|
+
method: t.method,
|
|
44
|
+
url: t.url,
|
|
45
|
+
statusCode: t.status_code,
|
|
46
|
+
totalDuration: t.total_duration,
|
|
47
|
+
spanCount: t.span_count,
|
|
48
|
+
spans: safeParseJson(t.spans) ?? [],
|
|
49
|
+
warnings: safeParseJsonArray(t.warnings),
|
|
50
|
+
createdAt: t.created_at,
|
|
51
|
+
...(t.http_request_id ? { httpRequestId: t.http_request_id } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function formatLog(l) {
|
|
55
|
+
return {
|
|
56
|
+
id: l.id,
|
|
57
|
+
level: l.level,
|
|
58
|
+
message: l.message,
|
|
59
|
+
requestId: l.request_id,
|
|
60
|
+
data: l.data,
|
|
61
|
+
createdAt: l.created_at,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function mapChartBucket(b) {
|
|
65
|
+
return {
|
|
66
|
+
bucket: b.bucket,
|
|
67
|
+
requestCount: b.request_count,
|
|
68
|
+
avgDuration: b.avg_duration,
|
|
69
|
+
p95Duration: b.p95_duration,
|
|
70
|
+
errorCount: b.error_count,
|
|
71
|
+
queryCount: b.query_count,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function emptyOverview() {
|
|
75
|
+
return {
|
|
76
|
+
avgResponseTime: 0,
|
|
77
|
+
p95ResponseTime: 0,
|
|
78
|
+
requestsPerMinute: 0,
|
|
79
|
+
errorRate: 0,
|
|
80
|
+
sparklines: {
|
|
81
|
+
avgResponseTime: [],
|
|
82
|
+
p95ResponseTime: [],
|
|
83
|
+
requestsPerMinute: [],
|
|
84
|
+
errorRate: [],
|
|
85
|
+
},
|
|
86
|
+
slowestEndpoints: [],
|
|
87
|
+
queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
|
|
88
|
+
recentErrors: [],
|
|
89
|
+
topEvents: [],
|
|
90
|
+
emailActivity: { sent: 0, queued: 0, failed: 0 },
|
|
91
|
+
logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
|
|
92
|
+
statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
|
|
93
|
+
slowestQueries: [],
|
|
94
|
+
cacheStats: null,
|
|
95
|
+
jobQueueStatus: null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
import { round } from '../utils/math_helpers.js';
|
|
99
|
+
/** Build sparklines from raw sparkline data. */
|
|
100
|
+
export function buildSparklines(data) {
|
|
101
|
+
return {
|
|
102
|
+
avgResponseTime: data.map((m) => m.avg_duration),
|
|
103
|
+
p95ResponseTime: data.map((m) => m.p95_duration),
|
|
104
|
+
requestsPerMinute: data.map((m) => m.request_count),
|
|
105
|
+
errorRate: data.map((m) => m.request_count > 0
|
|
106
|
+
? round((m.error_count / m.request_count) * 100)
|
|
107
|
+
: 0),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Format grouped query results. */
|
|
111
|
+
export function formatGroupedQuery(g, totalTime) {
|
|
112
|
+
return {
|
|
113
|
+
sqlNormalized: g.sql_normalized,
|
|
114
|
+
count: g.count,
|
|
115
|
+
avgDuration: round(g.avg_duration),
|
|
116
|
+
minDuration: round(g.min_duration),
|
|
117
|
+
maxDuration: round(g.max_duration),
|
|
118
|
+
totalDuration: round(g.total_duration),
|
|
119
|
+
percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Run EXPLAIN on a query, returning the plan. */
|
|
123
|
+
export async function runExplain(appDb, query) {
|
|
124
|
+
let bindings = [];
|
|
125
|
+
if (query.bindings) {
|
|
126
|
+
try {
|
|
127
|
+
bindings = JSON.parse(query.bindings);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* skip */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
|
|
134
|
+
const rawRows = explainResult?.rows ??
|
|
135
|
+
(Array.isArray(explainResult) ? explainResult : []);
|
|
136
|
+
if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
|
|
137
|
+
return rawRows[0]['QUERY PLAN'];
|
|
138
|
+
}
|
|
139
|
+
return rawRows;
|
|
140
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CacheInspector } from './integrations/cache_inspector.js';
|
|
2
|
+
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
3
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
4
|
+
/**
|
|
5
|
+
* Manages lazy initialization and availability detection for
|
|
6
|
+
* cache (Redis) and queue (BullMQ) inspectors.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from DashboardController to reduce file size and complexity.
|
|
9
|
+
*/
|
|
10
|
+
export declare class InspectorManager {
|
|
11
|
+
private app;
|
|
12
|
+
private cacheInspector;
|
|
13
|
+
private queueInspector;
|
|
14
|
+
private cacheAvailable;
|
|
15
|
+
private queueAvailable;
|
|
16
|
+
constructor(app: ApplicationService);
|
|
17
|
+
/** Lazy-init the cache inspector. Returns null if Redis is unavailable. */
|
|
18
|
+
getCacheInspector(): Promise<CacheInspector | null>;
|
|
19
|
+
/** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
|
|
20
|
+
getQueueInspector(): Promise<QueueInspector | null>;
|
|
21
|
+
/** Fetch cache overview stats for the overview page. */
|
|
22
|
+
fetchCacheOverview(): Promise<{
|
|
23
|
+
available: boolean;
|
|
24
|
+
totalKeys: number;
|
|
25
|
+
hitRate: number;
|
|
26
|
+
memoryUsedHuman: string;
|
|
27
|
+
} | null>;
|
|
28
|
+
/** Fetch queue overview stats for the overview page. */
|
|
29
|
+
fetchQueueOverview(): Promise<{
|
|
30
|
+
available: boolean;
|
|
31
|
+
active: number;
|
|
32
|
+
waiting: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
completed: number;
|
|
35
|
+
} | null>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { log } from '../utils/logger.js';
|
|
2
|
+
import { CacheInspector } from './integrations/cache_inspector.js';
|
|
3
|
+
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
4
|
+
/**
|
|
5
|
+
* Manages lazy initialization and availability detection for
|
|
6
|
+
* cache (Redis) and queue (BullMQ) inspectors.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from DashboardController to reduce file size and complexity.
|
|
9
|
+
*/
|
|
10
|
+
export class InspectorManager {
|
|
11
|
+
app;
|
|
12
|
+
cacheInspector = null;
|
|
13
|
+
queueInspector = null;
|
|
14
|
+
cacheAvailable = null;
|
|
15
|
+
queueAvailable = null;
|
|
16
|
+
constructor(app) {
|
|
17
|
+
this.app = app;
|
|
18
|
+
}
|
|
19
|
+
/** Lazy-init the cache inspector. Returns null if Redis is unavailable. */
|
|
20
|
+
async getCacheInspector() {
|
|
21
|
+
if (this.cacheAvailable === false)
|
|
22
|
+
return null;
|
|
23
|
+
if (this.cacheInspector)
|
|
24
|
+
return this.cacheInspector;
|
|
25
|
+
try {
|
|
26
|
+
const available = await CacheInspector.isAvailable(this.app);
|
|
27
|
+
this.cacheAvailable = available;
|
|
28
|
+
if (!available) {
|
|
29
|
+
log.info('dashboard: Redis not detected — Cache panel disabled');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const redis = await this.app.container.make('redis');
|
|
33
|
+
this.cacheInspector = new CacheInspector(redis);
|
|
34
|
+
return this.cacheInspector;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
this.cacheAvailable = false;
|
|
38
|
+
log.warn('dashboard: CacheInspector init failed — ' + err?.message);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
|
|
43
|
+
async getQueueInspector() {
|
|
44
|
+
if (this.queueAvailable === false)
|
|
45
|
+
return null;
|
|
46
|
+
if (this.queueInspector)
|
|
47
|
+
return this.queueInspector;
|
|
48
|
+
try {
|
|
49
|
+
const available = await QueueInspector.isAvailable(this.app);
|
|
50
|
+
this.queueAvailable = available;
|
|
51
|
+
if (!available) {
|
|
52
|
+
log.info('dashboard: Queue not detected — Jobs panel disabled');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const queue = await this.app.container.make('rlanz/queue');
|
|
56
|
+
this.queueInspector = new QueueInspector(queue);
|
|
57
|
+
return this.queueInspector;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
this.queueAvailable = false;
|
|
61
|
+
log.warn('dashboard: QueueInspector init failed — ' + err?.message);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Fetch cache overview stats for the overview page. */
|
|
66
|
+
async fetchCacheOverview() {
|
|
67
|
+
try {
|
|
68
|
+
const inspector = await this.getCacheInspector();
|
|
69
|
+
if (!inspector)
|
|
70
|
+
return null;
|
|
71
|
+
const stats = await inspector.getStats();
|
|
72
|
+
return {
|
|
73
|
+
available: true,
|
|
74
|
+
totalKeys: stats.totalKeys,
|
|
75
|
+
hitRate: stats.hitRate,
|
|
76
|
+
memoryUsedHuman: stats.memoryUsedHuman,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Fetch queue overview stats for the overview page. */
|
|
84
|
+
async fetchQueueOverview() {
|
|
85
|
+
try {
|
|
86
|
+
const inspector = await this.getQueueInspector();
|
|
87
|
+
if (!inspector)
|
|
88
|
+
return null;
|
|
89
|
+
const overview = await inspector.getOverview();
|
|
90
|
+
return {
|
|
91
|
+
available: true,
|
|
92
|
+
active: overview.active,
|
|
93
|
+
waiting: overview.waiting,
|
|
94
|
+
failed: overview.failed,
|
|
95
|
+
completed: overview.completed,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -134,6 +134,16 @@ function isSensitiveValue(value) {
|
|
|
134
134
|
return true;
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
|
+
/** Sanitize a single key-value pair, redacting sensitive strings. */
|
|
138
|
+
function sanitizeValue(key, value, seen) {
|
|
139
|
+
if (typeof value === 'string' && (isSensitiveKey(key) || isSensitiveValue(value))) {
|
|
140
|
+
return redact(value);
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === 'object' && value !== null) {
|
|
143
|
+
return sanitizeObject(value, seen);
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
137
147
|
/**
|
|
138
148
|
* Recursively sanitize an object, redacting string values whose keys
|
|
139
149
|
* match sensitive patterns. Booleans and numbers are never redacted.
|
|
@@ -141,10 +151,8 @@ function isSensitiveValue(value) {
|
|
|
141
151
|
function sanitizeObject(obj, seen = new WeakSet()) {
|
|
142
152
|
if (obj === null || obj === undefined)
|
|
143
153
|
return obj;
|
|
144
|
-
// Primitive types pass through
|
|
145
154
|
if (typeof obj !== 'object')
|
|
146
155
|
return obj;
|
|
147
|
-
// Avoid circular references
|
|
148
156
|
if (seen.has(obj))
|
|
149
157
|
return '[Circular]';
|
|
150
158
|
seen.add(obj);
|
|
@@ -154,17 +162,7 @@ function sanitizeObject(obj, seen = new WeakSet()) {
|
|
|
154
162
|
const record = obj;
|
|
155
163
|
const result = {};
|
|
156
164
|
for (const key of Object.keys(record)) {
|
|
157
|
-
|
|
158
|
-
if (typeof value === 'string' && (isSensitiveKey(key) || isSensitiveValue(value))) {
|
|
159
|
-
result[key] = redact(value);
|
|
160
|
-
}
|
|
161
|
-
else if (typeof value === 'object' && value !== null) {
|
|
162
|
-
result[key] = sanitizeObject(value, seen);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
// Booleans, numbers, and non-sensitive strings pass through
|
|
166
|
-
result[key] = value;
|
|
167
|
-
}
|
|
165
|
+
result[key] = sanitizeValue(key, record[key], seen);
|
|
168
166
|
}
|
|
169
167
|
return result;
|
|
170
168
|
}
|
|
@@ -142,9 +142,9 @@ export declare class QueueInspector {
|
|
|
142
142
|
* the class-style name from the filename.
|
|
143
143
|
*/
|
|
144
144
|
private static cleanJobName;
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
/** Extract timing fields from a BullMQ job. */
|
|
146
|
+
private static extractTimings;
|
|
147
|
+
/** Format a Bull job into our summary shape. */
|
|
148
148
|
private formatJobSummary;
|
|
149
149
|
}
|
|
150
150
|
export {};
|
|
@@ -199,14 +199,17 @@ export class QueueInspector {
|
|
|
199
199
|
}
|
|
200
200
|
return raw;
|
|
201
201
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
formatJobSummary(job, status) {
|
|
202
|
+
/** Extract timing fields from a BullMQ job. */
|
|
203
|
+
static extractTimings(job) {
|
|
206
204
|
const processedAt = job.processedOn ?? null;
|
|
207
205
|
const finishedAt = job.finishedOn ?? null;
|
|
208
|
-
const duration = processedAt !== null && finishedAt !== null ? finishedAt - processedAt : null;
|
|
209
206
|
const createdAt = job.timestamp ?? 0;
|
|
207
|
+
const duration = processedAt !== null && finishedAt !== null ? finishedAt - processedAt : null;
|
|
208
|
+
return { processedAt, finishedAt, createdAt, duration };
|
|
209
|
+
}
|
|
210
|
+
/** Format a Bull job into our summary shape. */
|
|
211
|
+
formatJobSummary(job, status) {
|
|
212
|
+
const timings = QueueInspector.extractTimings(job);
|
|
210
213
|
const data = job.data ?? null;
|
|
211
214
|
return {
|
|
212
215
|
id: String(job.id),
|
|
@@ -218,11 +221,11 @@ export class QueueInspector {
|
|
|
218
221
|
maxAttempts: job.opts?.attempts ?? 1,
|
|
219
222
|
progress: job.progress ?? 0,
|
|
220
223
|
failedReason: job.failedReason ?? null,
|
|
221
|
-
createdAt,
|
|
222
|
-
timestamp: createdAt,
|
|
223
|
-
processedAt,
|
|
224
|
-
finishedAt,
|
|
225
|
-
duration,
|
|
224
|
+
createdAt: timings.createdAt,
|
|
225
|
+
timestamp: timings.createdAt,
|
|
226
|
+
processedAt: timings.processedAt,
|
|
227
|
+
finishedAt: timings.finishedAt,
|
|
228
|
+
duration: timings.duration,
|
|
226
229
|
};
|
|
227
230
|
}
|
|
228
231
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InspectorManager } from './inspector_manager.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Handle GET /jobs — list jobs with optional status filter and search.
|
|
5
|
+
*/
|
|
6
|
+
export declare function handleJobs(inspectors: InspectorManager, { request, response }: HttpContext): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Handle GET /jobs/:id — get a single job's details.
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleJobDetail(inspectors: InspectorManager, { params, response }: HttpContext): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Handle POST /jobs/:id/retry — retry a failed job.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleJobRetry(inspectors: InspectorManager, { params, response }: HttpContext): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { clamp } from '../utils/math_helpers.js';
|
|
2
|
+
const EMPTY_JOBS = { available: false, overview: null, stats: null, jobs: [], total: 0 };
|
|
3
|
+
/**
|
|
4
|
+
* Handle GET /jobs — list jobs with optional status filter and search.
|
|
5
|
+
*/
|
|
6
|
+
export async function handleJobs(inspectors, { request, response }) {
|
|
7
|
+
const inspector = await inspectors.getQueueInspector();
|
|
8
|
+
if (!inspector)
|
|
9
|
+
return response.json(EMPTY_JOBS);
|
|
10
|
+
const qs = request.qs();
|
|
11
|
+
const page = Math.max(1, Number(qs.page) || 1);
|
|
12
|
+
const perPage = clamp(Number(qs.perPage) || Number(qs.limit) || 25, 1, 100);
|
|
13
|
+
try {
|
|
14
|
+
const [overview, jobList] = await Promise.all([
|
|
15
|
+
inspector.getOverview(),
|
|
16
|
+
inspector.listJobs(qs.status || 'all', page, perPage),
|
|
17
|
+
]);
|
|
18
|
+
let jobs = jobList.jobs;
|
|
19
|
+
let total = jobList.total;
|
|
20
|
+
if (qs.search) {
|
|
21
|
+
const term = qs.search.toLowerCase();
|
|
22
|
+
jobs = jobs.filter((j) => j.name?.toLowerCase().includes(term) || j.id?.toString().toLowerCase().includes(term));
|
|
23
|
+
total = jobs.length;
|
|
24
|
+
}
|
|
25
|
+
return response.json({ available: true, overview, stats: overview, jobs, total, page, perPage });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return response.json(EMPTY_JOBS);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Handle GET /jobs/:id — get a single job's details.
|
|
33
|
+
*/
|
|
34
|
+
export async function handleJobDetail(inspectors, { params, response }) {
|
|
35
|
+
const inspector = await inspectors.getQueueInspector();
|
|
36
|
+
if (!inspector)
|
|
37
|
+
return response.notFound({ error: 'Queue not available' });
|
|
38
|
+
try {
|
|
39
|
+
const detail = await inspector.getJob(String(params.id));
|
|
40
|
+
return detail ? response.json(detail) : response.notFound({ error: 'Job not found' });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return response.notFound({ error: 'Job not found' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Handle POST /jobs/:id/retry — retry a failed job.
|
|
48
|
+
*/
|
|
49
|
+
export async function handleJobRetry(inspectors, { params, response }) {
|
|
50
|
+
const inspector = await inspectors.getQueueInspector();
|
|
51
|
+
if (!inspector)
|
|
52
|
+
return response.notFound({ error: 'Queue not available' });
|
|
53
|
+
try {
|
|
54
|
+
return (await inspector.retryJob(String(params.id)))
|
|
55
|
+
? response.json({ success: true })
|
|
56
|
+
: response.badRequest({ error: 'Job could not be retried (not in failed state)' });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return response.internalServerError({ error: 'Retry failed' });
|
|
60
|
+
}
|
|
61
|
+
}
|