adonisjs-server-stats 1.12.3 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +11 -6
  2. package/dist/configure.js +6 -1
  3. package/dist/core/collectors/collector.d.ts +83 -0
  4. package/dist/core/core/types.d.ts +186 -0
  5. package/dist/core/debug/types.d.ts +467 -0
  6. package/dist/core/index.js +184 -169
  7. package/dist/core/types.d.ts +855 -167
  8. package/dist/react/{CacheSection-CX2duJuc.js → CacheSection-DkLtukWz.js} +1 -1
  9. package/dist/react/{CacheTab-CgT4t0oZ.js → CacheTab-DWeXnEBI.js} +51 -46
  10. package/dist/react/ConfigContent-Bwvfl_G7.js +350 -0
  11. package/dist/react/{ConfigSection-Dt0FyeaW.js → ConfigSection-DaVbFPDg.js} +2 -2
  12. package/dist/react/{ConfigTab-KtiTtsrj.js → ConfigTab-D4640WJZ.js} +2 -2
  13. package/dist/react/{CustomPaneTab-15QoEplP.js → CustomPaneTab-DglmAVMC.js} +1 -1
  14. package/dist/react/{EmailsSection-YcJYR5eA.js → EmailsSection-vYx6ExTb.js} +1 -1
  15. package/dist/react/{EmailsTab-CUjubln_.js → EmailsTab-DKDrRmRH.js} +54 -49
  16. package/dist/react/{EventsSection-GGPoYSNv.js → EventsSection-CapH5xut.js} +1 -1
  17. package/dist/react/{EventsTab-Wdwr0_ny.js → EventsTab-BNVEySAm.js} +1 -1
  18. package/dist/react/{InternalsContent-BNOnSoi9.js → InternalsContent-C9lIA92C.js} +1 -1
  19. package/dist/react/{InternalsSection-BwrTfpjA.js → InternalsSection-lOv_UcSV.js} +1 -1
  20. package/dist/react/{InternalsTab--RD-L1dX.js → InternalsTab-BygoSorC.js} +1 -1
  21. package/dist/react/{JobsSection-BqqIh_DR.js → JobsSection-ZIQsJWid.js} +1 -1
  22. package/dist/react/{JobsTab-CBrU-ryL.js → JobsTab-DXQzXrDt.js} +1 -1
  23. package/dist/react/{LogEntryRow-BOrRkhRU.js → LogEntryRow-BWkHE51-.js} +1 -1
  24. package/dist/react/{LogsSection-Cm_lphM6.js → LogsSection-ConXdBkL.js} +2 -2
  25. package/dist/react/LogsTab-CJM47LPn.js +82 -0
  26. package/dist/react/{OverviewSection-XF7bakyM.js → OverviewSection-DOMu2qvl.js} +131 -131
  27. package/dist/react/{QueriesSection-DsQBKrNK.js → QueriesSection-DFsaOSJI.js} +1 -1
  28. package/dist/react/{QueriesTab-CxCC1GVq.js → QueriesTab-CY9CG_7L.js} +1 -1
  29. package/dist/react/RequestsSection-CC-eVHsl.js +326 -0
  30. package/dist/react/{RoutesSection-D0y5JQP4.js → RoutesSection-p1DMq41y.js} +1 -1
  31. package/dist/react/{RoutesTab-Y_alJVMV.js → RoutesTab-Cnqy8UcK.js} +1 -1
  32. package/dist/react/{SplitPaneWrapper-CZl1ouIT.js → SplitPaneWrapper-XgkA0QxE.js} +1 -1
  33. package/dist/react/{TimelineTab-HyqZpfbp.js → TimelineTab-1YOERxe5.js} +3 -3
  34. package/dist/react/index-DOSlCpZ9.js +1159 -0
  35. package/dist/react/index.js +1 -1
  36. package/dist/react/useApiClient-CtEG7rHs.js +11 -0
  37. package/dist/src/collectors/adonisjs_queue_collector.d.ts +21 -0
  38. package/dist/src/collectors/adonisjs_queue_collector.js +61 -0
  39. package/dist/src/collectors/auto_detect.js +8 -2
  40. package/dist/src/collectors/index.d.ts +2 -0
  41. package/dist/src/collectors/index.js +1 -0
  42. package/dist/src/collectors/queue_collector.js +22 -6
  43. package/dist/src/config/deprecation_migration.js +11 -0
  44. package/dist/src/dashboard/cache_handlers.js +22 -2
  45. package/dist/src/dashboard/flush_manager.d.ts +2 -0
  46. package/dist/src/dashboard/flush_manager.js +17 -0
  47. package/dist/src/dashboard/inspector_manager.d.ts +4 -4
  48. package/dist/src/dashboard/inspector_manager.js +20 -10
  49. package/dist/src/dashboard/integrations/adonisjs_queue_inspector.d.ts +42 -0
  50. package/dist/src/dashboard/integrations/adonisjs_queue_inspector.js +135 -0
  51. package/dist/src/dashboard/integrations/adonisjs_queue_store.d.ts +166 -0
  52. package/dist/src/dashboard/integrations/adonisjs_queue_store.js +629 -0
  53. package/dist/src/dashboard/integrations/config_inspector.d.ts +0 -1
  54. package/dist/src/dashboard/integrations/config_inspector.js +4 -2
  55. package/dist/src/dashboard/integrations/index.d.ts +4 -1
  56. package/dist/src/dashboard/integrations/index.js +2 -0
  57. package/dist/src/dashboard/integrations/queue_inspector.d.ts +5 -61
  58. package/dist/src/dashboard/integrations/queue_inspector.js +5 -1
  59. package/dist/src/dashboard/integrations/queue_inspector_contract.d.ts +73 -0
  60. package/dist/src/dashboard/integrations/queue_inspector_contract.js +15 -0
  61. package/dist/src/dashboard/paginate_helper.d.ts +5 -0
  62. package/dist/src/dashboard/paginate_helper.js +16 -4
  63. package/dist/src/dashboard/query_explain_handler.d.ts +6 -1
  64. package/dist/src/dashboard/query_explain_handler.js +12 -2
  65. package/dist/src/dashboard/write_queue.d.ts +13 -0
  66. package/dist/src/dashboard/write_queue.js +34 -2
  67. package/dist/src/debug/debug_store.js +8 -1
  68. package/dist/src/debug/event_collector.js +4 -0
  69. package/dist/src/debug/query_collector.js +2 -0
  70. package/dist/src/debug/ring_buffer.js +8 -1
  71. package/dist/src/debug/trace_collector.js +34 -10
  72. package/dist/src/debug/types.d.ts +2 -0
  73. package/dist/src/define_config.js +1 -0
  74. package/dist/src/edge/client/dashboard.js +2 -2
  75. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  76. package/dist/src/edge/client/stats-bar.js +1 -1
  77. package/dist/src/edge/client-vue/dashboard.js +5 -5
  78. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  79. package/dist/src/engine/request_metrics.js +8 -10
  80. package/dist/src/log_stream/log_stream_service.d.ts +1 -0
  81. package/dist/src/log_stream/log_stream_service.js +7 -5
  82. package/dist/src/prometheus/prometheus_collector.d.ts +9 -0
  83. package/dist/src/prometheus/prometheus_collector.js +11 -0
  84. package/dist/src/provider/dashboard_init.js +15 -0
  85. package/dist/src/provider/dashboard_setup.js +9 -2
  86. package/dist/src/provider/email_bridge.d.ts +14 -3
  87. package/dist/src/provider/email_bridge.js +25 -3
  88. package/dist/src/provider/email_helpers.d.ts +9 -1
  89. package/dist/src/provider/email_helpers.js +32 -4
  90. package/dist/src/provider/pino_hook.d.ts +8 -0
  91. package/dist/src/provider/pino_hook.js +19 -1
  92. package/dist/src/provider/provider_helpers_extra.d.ts +2 -0
  93. package/dist/src/provider/provider_helpers_extra.js +17 -2
  94. package/dist/src/provider/server_stats_provider.js +19 -1
  95. package/dist/src/provider/shutdown_helpers.js +2 -0
  96. package/dist/src/provider/toolbar_setup.js +32 -17
  97. package/dist/src/routes/dashboard_routes.js +96 -62
  98. package/dist/src/routes/debug_routes.js +45 -35
  99. package/dist/src/routes/no_session_middleware.d.ts +6 -3
  100. package/dist/src/routes/no_session_middleware.js +7 -3
  101. package/dist/src/routes/register_routes.d.ts +8 -0
  102. package/dist/src/routes/register_routes.js +19 -0
  103. package/dist/src/types.d.ts +31 -0
  104. package/dist/src/utils/app_import.js +12 -4
  105. package/dist/vue/CacheSection-CxEBVVkF.js +156 -0
  106. package/dist/vue/ConfigSection-BiRAiaHj.js +415 -0
  107. package/dist/vue/EmailsSection-Dl44qyqY.js +208 -0
  108. package/dist/vue/{EmailsTab-CwIF1fik.js → EmailsTab-Diya54CP.js} +6 -5
  109. package/dist/vue/{EventsSection-BpgkWIwM.js → EventsSection-CWjeitjU.js} +1 -1
  110. package/dist/vue/{InternalsTab-521fxYYj.js → InternalsTab-Z3c82glB.js} +15 -15
  111. package/dist/vue/{JobsSection-DrghFEKL.js → JobsSection-DOBb4LjZ.js} +1 -1
  112. package/dist/vue/LogsSection-CXx-HOWJ.js +260 -0
  113. package/dist/vue/{OverviewSection-BAgZTPjY.js → OverviewSection-CyfNQ8uV.js} +318 -302
  114. package/dist/vue/{QueriesSection-CUpwhp7u.js → QueriesSection-CXBsFp-y.js} +1 -1
  115. package/dist/vue/{RequestsSection-D8P2xpF2.js → RequestsSection-YTIaZGZd.js} +151 -145
  116. package/dist/vue/{RoutesSection-0qB81hTT.js → RoutesSection-CDKMey49.js} +1 -1
  117. package/dist/vue/composables/useApiClient.d.ts +8 -1
  118. package/dist/vue/index-14x39RI_.js +1239 -0
  119. package/dist/vue/index.js +1 -1
  120. package/dist/vue/style.css +1 -1
  121. package/package.json +16 -3
  122. package/dist/react/ConfigContent-CnsEI4j3.js +0 -397
  123. package/dist/react/LogsTab-jKwv9G7Q.js +0 -79
  124. package/dist/react/RequestsSection-D8cMbZU0.js +0 -321
  125. package/dist/react/index-CsprmgzI.js +0 -1121
  126. package/dist/react/useApiClient-BVtNCmnL.js +0 -9
  127. package/dist/vue/CacheSection-DRqV3YX2.js +0 -149
  128. package/dist/vue/ConfigSection-C6pQCHAL.js +0 -576
  129. package/dist/vue/EmailsSection-BTNw3ZU2.js +0 -206
  130. package/dist/vue/LogsSection-BDxx9Bfi.js +0 -258
  131. package/dist/vue/index-30jLw-_w.js +0 -1232
  132. /package/dist/core/{api-client.d.ts → core/api-client.d.ts} +0 -0
  133. /package/dist/core/{config-utils.d.ts → core/config-utils.d.ts} +0 -0
  134. /package/dist/core/{constants.d.ts → core/constants.d.ts} +0 -0
  135. /package/dist/core/{dashboard-api.d.ts → core/dashboard-api.d.ts} +0 -0
  136. /package/dist/core/{dashboard-data-controller.d.ts → core/dashboard-data-controller.d.ts} +0 -0
  137. /package/dist/core/{dashboard-data-helpers.d.ts → core/dashboard-data-helpers.d.ts} +0 -0
  138. /package/dist/core/{debug-data-controller.d.ts → core/debug-data-controller.d.ts} +0 -0
  139. /package/dist/core/{define-config-helpers.d.ts → core/define-config-helpers.d.ts} +0 -0
  140. /package/dist/core/{explain-utils.d.ts → core/explain-utils.d.ts} +0 -0
  141. /package/dist/core/{feature-detect-helpers.d.ts → core/feature-detect-helpers.d.ts} +0 -0
  142. /package/dist/core/{feature-detect.d.ts → core/feature-detect.d.ts} +0 -0
  143. /package/dist/core/{field-resolvers.d.ts → core/field-resolvers.d.ts} +0 -0
  144. /package/dist/core/{formatters-helpers.d.ts → core/formatters-helpers.d.ts} +0 -0
  145. /package/dist/core/{formatters.d.ts → core/formatters.d.ts} +0 -0
  146. /package/dist/core/{history-buffer.d.ts → core/history-buffer.d.ts} +0 -0
  147. /package/dist/core/{icons.d.ts → core/icons.d.ts} +0 -0
  148. /package/dist/core/{index.d.ts → core/index.d.ts} +0 -0
  149. /package/dist/core/{internals-utils.d.ts → core/internals-utils.d.ts} +0 -0
  150. /package/dist/core/{job-utils.d.ts → core/job-utils.d.ts} +0 -0
  151. /package/dist/core/{log-utils-helpers.d.ts → core/log-utils-helpers.d.ts} +0 -0
  152. /package/dist/core/{log-utils.d.ts → core/log-utils.d.ts} +0 -0
  153. /package/dist/core/{metrics.d.ts → core/metrics.d.ts} +0 -0
  154. /package/dist/core/{pagination.d.ts → core/pagination.d.ts} +0 -0
  155. /package/dist/core/{queries-columns.d.ts → core/queries-columns.d.ts} +0 -0
  156. /package/dist/core/{queries-controller.d.ts → core/queries-controller.d.ts} +0 -0
  157. /package/dist/core/{query-utils.d.ts → core/query-utils.d.ts} +0 -0
  158. /package/dist/core/{resizable-columns.d.ts → core/resizable-columns.d.ts} +0 -0
  159. /package/dist/core/{routes.d.ts → core/routes.d.ts} +0 -0
  160. /package/dist/core/{server-stats-controller.d.ts → core/server-stats-controller.d.ts} +0 -0
  161. /package/dist/core/{sparkline.d.ts → core/sparkline.d.ts} +0 -0
  162. /package/dist/core/{split-pane.d.ts → core/split-pane.d.ts} +0 -0
  163. /package/dist/core/{theme.d.ts → core/theme.d.ts} +0 -0
  164. /package/dist/core/{trace-utils.d.ts → core/trace-utils.d.ts} +0 -0
  165. /package/dist/core/{transmit-adapter.d.ts → core/transmit-adapter.d.ts} +0 -0
  166. /package/dist/core/{transmit-helpers.d.ts → core/transmit-helpers.d.ts} +0 -0
  167. /package/dist/core/{types-dashboard.d.ts → core/types-dashboard.d.ts} +0 -0
  168. /package/dist/core/{types-diagnostics.d.ts → core/types-diagnostics.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { B as s, D as t, c as r, J as u, M as g, d as o, d, S as D, T as S, e as h, a as B, u as T, f as b, g as l, h as m } from "./index-CsprmgzI.js";
1
+ import { B as s, D as t, c as r, J as u, M as g, d as o, d, S as D, T as S, e as h, a as B, u as T, f as b, g as l, h as m } from "./index-DOSlCpZ9.js";
2
2
  export {
3
3
  s as Badge,
4
4
  t as DashboardPage,
@@ -0,0 +1,11 @@
1
+ import { useRef as t, useEffect as u, useCallback as c } from "react";
2
+ import { ApiClient as i } from "adonisjs-server-stats/core";
3
+ function o(e = "", n) {
4
+ const r = t(null);
5
+ return u(() => {
6
+ r.current = null;
7
+ }, [e, n]), c(() => (r.current || (r.current = new i({ baseUrl: e, authToken: n })), r.current), [e, n]);
8
+ }
9
+ export {
10
+ o as u
11
+ };
@@ -0,0 +1,21 @@
1
+ import type { MetricCollector } from './collector.js';
2
+ import type { QueueStoreReader } from '../dashboard/integrations/adonisjs_queue_store.js';
3
+ /**
4
+ * Options for {@link adonisQueueCollector}.
5
+ */
6
+ export interface AdonisQueueCollectorOptions {
7
+ }
8
+ /** Internal deps seam — NOT part of the public API; for unit tests only. */
9
+ interface AdonisQueueCollectorDeps {
10
+ resolveReader?: () => Promise<QueueStoreReader | null>;
11
+ }
12
+ /**
13
+ * Monitors an @adonisjs/queue job queue for active, waiting, delayed, and failed jobs.
14
+ *
15
+ * Auto-detects the driver (database or redis) from the host app's queue config.
16
+ * Returns zeros if @adonisjs/queue is unavailable or the store cannot be reached.
17
+ *
18
+ * **Peer dependencies:** `@adonisjs/queue`
19
+ */
20
+ export declare function adonisQueueCollector(_opts?: AdonisQueueCollectorOptions, deps?: AdonisQueueCollectorDeps): MetricCollector;
21
+ export {};
@@ -0,0 +1,61 @@
1
+ import { resolveFromAppImport } from '../dashboard/integrations/adonisjs_queue_store.js';
2
+ import { log, dim, bold } from '../utils/logger.js';
3
+ /** Default metrics returned when queue data is unavailable. */
4
+ const QUEUE_DEFAULTS = {
5
+ queueActive: 0,
6
+ queueWaiting: 0,
7
+ queueDelayed: 0,
8
+ queueFailed: 0,
9
+ queueWorkerCount: 0,
10
+ };
11
+ /**
12
+ * Monitors an @adonisjs/queue job queue for active, waiting, delayed, and failed jobs.
13
+ *
14
+ * Auto-detects the driver (database or redis) from the host app's queue config.
15
+ * Returns zeros if @adonisjs/queue is unavailable or the store cannot be reached.
16
+ *
17
+ * **Peer dependencies:** `@adonisjs/queue`
18
+ */
19
+ export function adonisQueueCollector(_opts, deps) {
20
+ let warnedNotInstalled = false;
21
+ return {
22
+ name: 'queue',
23
+ label: 'queue — @adonisjs/queue (auto-detected driver)',
24
+ getConfig() {
25
+ return {
26
+ driver: 'auto',
27
+ source: '@adonisjs/queue',
28
+ };
29
+ },
30
+ async collect() {
31
+ try {
32
+ const resolve = deps?.resolveReader ?? resolveFromAppImport;
33
+ const reader = await resolve();
34
+ if (!reader) {
35
+ if (!warnedNotInstalled) {
36
+ warnedNotInstalled = true;
37
+ log.block(`Queue collector ${bold('skipped')} — @adonisjs/queue is not installed or not reachable`, [
38
+ dim('Queue metrics will return zeros until the package is available.'),
39
+ `Run ${bold('node ace add @adonisjs/queue')} to install it.`,
40
+ ]);
41
+ }
42
+ return QUEUE_DEFAULTS;
43
+ }
44
+ const [counts, workerCount] = await Promise.all([
45
+ reader.getCounts(),
46
+ reader.getWorkerCount(),
47
+ ]);
48
+ return {
49
+ queueActive: counts.active,
50
+ queueWaiting: counts.waiting,
51
+ queueDelayed: counts.delayed,
52
+ queueFailed: counts.failed,
53
+ queueWorkerCount: workerCount,
54
+ };
55
+ }
56
+ catch {
57
+ return QUEUE_DEFAULTS;
58
+ }
59
+ },
60
+ };
61
+ }
@@ -64,15 +64,21 @@ async function registerOptionalCollectors(collectors, entries) {
64
64
  pkg: '@adonisjs/redis',
65
65
  });
66
66
  const hasBullMQ = await isInstalled('bullmq');
67
+ const hasAdonisQueue = hasBullMQ ? false : await isInstalled('@adonisjs/queue');
67
68
  if (hasBullMQ) {
68
69
  const { queueCollector } = await import('./queue_collector.js');
69
70
  collectors.push(queueCollector({ queueName: 'default', connection: { host: '127.0.0.1', port: 6379 } }));
70
71
  }
72
+ else if (hasAdonisQueue) {
73
+ const { adonisQueueCollector } = await import('./adonisjs_queue_collector.js');
74
+ collectors.push(adonisQueueCollector());
75
+ }
76
+ const queueEnabled = hasBullMQ || hasAdonisQueue;
71
77
  pushOptionalEntry(entries, {
72
78
  name: 'queue',
73
79
  description: 'jobs, wait time, throughput',
74
- enabled: hasBullMQ,
75
- pkg: 'bullmq',
80
+ enabled: queueEnabled,
81
+ pkg: hasBullMQ ? 'bullmq' : hasAdonisQueue ? '@adonisjs/queue' : 'bullmq',
76
82
  });
77
83
  }
78
84
  function printBootLog(entries) {
@@ -8,6 +8,8 @@ export type { DbPoolCollectorOptions } from './db_pool_collector.js';
8
8
  export { redisCollector } from './redis_collector.js';
9
9
  export { queueCollector } from './queue_collector.js';
10
10
  export type { QueueCollectorOptions, QueueRedisConnection } from './queue_collector.js';
11
+ export { adonisQueueCollector } from './adonisjs_queue_collector.js';
12
+ export type { AdonisQueueCollectorOptions } from './adonisjs_queue_collector.js';
11
13
  export { logCollector, getLogStreamService } from './log_collector.js';
12
14
  export type { LogCollectorOptions } from './log_collector.js';
13
15
  export { appCollector } from './app_collector.js';
@@ -4,6 +4,7 @@ export { httpCollector, getRequestMetrics } from './http_collector.js';
4
4
  export { dbPoolCollector } from './db_pool_collector.js';
5
5
  export { redisCollector } from './redis_collector.js';
6
6
  export { queueCollector } from './queue_collector.js';
7
+ export { adonisQueueCollector } from './adonisjs_queue_collector.js';
7
8
  export { logCollector, getLogStreamService } from './log_collector.js';
8
9
  export { appCollector } from './app_collector.js';
9
10
  export { autoDetectCollectors } from './auto_detect.js';
@@ -7,12 +7,9 @@ const QUEUE_DEFAULTS = {
7
7
  queueFailed: 0,
8
8
  queueWorkerCount: 0,
9
9
  };
10
- /** Fetch job counts from a BullMQ queue. */
11
- async function fetchQueueCounts(queueName, connection) {
12
- const { Queue } = await import('bullmq');
13
- const queue = new Queue(queueName, { connection });
10
+ /** Fetch job counts from a long-lived BullMQ queue instance. */
11
+ async function fetchQueueCounts(queue) {
14
12
  const [counts, workers] = await Promise.all([queue.getJobCounts(), queue.getWorkers()]);
15
- await queue.close();
16
13
  return {
17
14
  queueActive: counts.active ?? 0,
18
15
  queueWaiting: counts.waiting ?? 0,
@@ -62,9 +59,28 @@ export function queueCollector(opts) {
62
59
  connectionError: false,
63
60
  missingConnection: false,
64
61
  };
62
+ // The BullMQ Queue owns a Redis connection. Create it once and reuse it
63
+ // across every poll tick so a rejected getJobCounts()/getWorkers() can never
64
+ // leak a connection (the previous per-tick `new Queue(...)` leaked one on
65
+ // every failure because close() was skipped). The queue is created lazily on
66
+ // first collect so a missing `bullmq` peer dependency degrades gracefully.
67
+ let queue = null;
68
+ async function getQueue(connection) {
69
+ if (queue)
70
+ return queue;
71
+ const { Queue } = await import('bullmq');
72
+ queue = new Queue(queueName, { connection });
73
+ return queue;
74
+ }
65
75
  return {
66
76
  name: 'queue',
67
77
  label: `queue — ${queueName} @ ${opts.connection?.host ?? '?'}:${opts.connection?.port ?? '?'}`,
78
+ async stop() {
79
+ if (queue) {
80
+ await queue.close().catch(() => { });
81
+ queue = null;
82
+ }
83
+ },
68
84
  getConfig() {
69
85
  return {
70
86
  queueName,
@@ -84,7 +100,7 @@ export function queueCollector(opts) {
84
100
  return QUEUE_DEFAULTS;
85
101
  }
86
102
  try {
87
- return await fetchQueueCounts(queueName, opts.connection);
103
+ return await fetchQueueCounts(await getQueue(opts.connection));
88
104
  }
89
105
  catch (error) {
90
106
  handleQueueError(error, queueName, opts.connection, warned);
@@ -39,6 +39,10 @@ const SIMPLE_DEPRECATIONS = [
39
39
  newName: 'authorize',
40
40
  before: () => ['shouldShow: (ctx) => ...'],
41
41
  after: () => ['authorize: (ctx) => ...'],
42
+ note: () => [
43
+ 'Your `shouldShow` guard still fully protects the dashboard — nothing breaks.',
44
+ '`authorize` is just the new name for the same access check.',
45
+ ],
42
46
  },
43
47
  {
44
48
  key: 'channelName',
@@ -171,6 +175,7 @@ export function logDeprecationWarnings(config) {
171
175
  new: dep.newName,
172
176
  before: dep.before(config),
173
177
  after: dep.after(config),
178
+ note: dep.note?.(config),
174
179
  });
175
180
  }
176
181
  }
@@ -192,6 +197,12 @@ export function logDeprecationWarnings(config) {
192
197
  for (const line of entry.after) {
193
198
  lines.push(` ${dim('after:')} ${line}`);
194
199
  }
200
+ if (entry.note) {
201
+ lines.push('');
202
+ for (const line of entry.note) {
203
+ lines.push(` ${dim(line)}`);
204
+ }
205
+ }
195
206
  lines.push('');
196
207
  }
197
208
  lines.push(` ${dim('No rush — the old names still work. They will be removed in the next major version.')}`);
@@ -1,4 +1,18 @@
1
1
  import { clamp } from '../utils/math_helpers.js';
2
+ /**
3
+ * Optional allow-list prefix for single-key cache operations (get/delete).
4
+ * Restricts which Redis keys the dashboard may read or delete so an attacker
5
+ * can't target arbitrary keys (sessions, rate-limit counters). Empty/unset
6
+ * means unrestricted (back-compat default for local dev).
7
+ */
8
+ function getCacheKeyPrefix() {
9
+ return (process.env.SERVER_STATS_CACHE_KEY_PREFIX ?? '').trim();
10
+ }
11
+ /** Check whether a cache key is permitted under the configured prefix. */
12
+ function isKeyAllowed(key) {
13
+ const prefix = getCacheKeyPrefix();
14
+ return prefix === '' || key.startsWith(prefix);
15
+ }
2
16
  /**
3
17
  * Handle GET /cache-stats — list cache keys and overall stats.
4
18
  */
@@ -26,8 +40,11 @@ export async function handleCacheKey(inspectors, { params, response }) {
26
40
  const inspector = await inspectors.getCacheInspector();
27
41
  if (!inspector)
28
42
  return response.notFound({ error: 'Cache not available' });
43
+ const key = decodeURIComponent(params.key);
44
+ if (!isKeyAllowed(key))
45
+ return response.forbidden({ error: 'Cache key not permitted' });
29
46
  try {
30
- const detail = await inspector.getKey(decodeURIComponent(params.key));
47
+ const detail = await inspector.getKey(key);
31
48
  return detail ? response.json(detail) : response.notFound({ error: 'Key not found' });
32
49
  }
33
50
  catch {
@@ -41,8 +58,11 @@ export async function handleCacheKeyDelete(inspectors, { params, response }) {
41
58
  const inspector = await inspectors.getCacheInspector();
42
59
  if (!inspector)
43
60
  return response.notFound({ error: 'Cache not available' });
61
+ const key = decodeURIComponent(params.key);
62
+ if (!isKeyAllowed(key))
63
+ return response.forbidden({ error: 'Cache key not permitted' });
44
64
  try {
45
- return (await inspector.deleteKey(decodeURIComponent(params.key)))
65
+ return (await inspector.deleteKey(key))
46
66
  ? response.json({ deleted: true })
47
67
  : response.notFound({ error: 'Key not found' });
48
68
  }
@@ -11,6 +11,7 @@ export declare class FlushManager {
11
11
  pendingEmails: EmailRecord[];
12
12
  private flushTimer;
13
13
  private flushing;
14
+ private inFlight;
14
15
  private db;
15
16
  constructor(getDb: () => Knex | null);
16
17
  persistRequest(input: PersistRequestInput, dashboardPath: string): void;
@@ -21,5 +22,6 @@ export declare class FlushManager {
21
22
  private backpressure;
22
23
  private scheduleFlush;
23
24
  flush(): Promise<void>;
25
+ private runFlush;
24
26
  private takeSnapshot;
25
27
  }
@@ -9,6 +9,7 @@ export class FlushManager {
9
9
  pendingEmails = [];
10
10
  flushTimer = null;
11
11
  flushing = false;
12
+ inFlight = null;
12
13
  db;
13
14
  constructor(getDb) {
14
15
  this.db = getDb;
@@ -43,6 +44,10 @@ export class FlushManager {
43
44
  clearTimeout(this.flushTimer);
44
45
  this.flushTimer = null;
45
46
  }
47
+ // Await any in-flight (timer-triggered) flush first, otherwise the final
48
+ // flush below early-returns and queued data is lost at shutdown.
49
+ if (this.inFlight)
50
+ await this.inFlight.catch(() => { });
46
51
  await this.flush().catch(() => { });
47
52
  }
48
53
  backpressure(q) {
@@ -67,6 +72,18 @@ export class FlushManager {
67
72
  if (this.flushing || !db)
68
73
  return;
69
74
  this.flushing = true;
75
+ // Track the in-flight flush so stop() can await it before issuing a final
76
+ // flush, instead of racing the flushing guard and dropping queued data.
77
+ const run = this.runFlush(db);
78
+ this.inFlight = run;
79
+ try {
80
+ await run;
81
+ }
82
+ finally {
83
+ this.inFlight = null;
84
+ }
85
+ }
86
+ async runFlush(db) {
70
87
  const snap = this.takeSnapshot();
71
88
  if (!snap) {
72
89
  this.flushing = false;
@@ -1,9 +1,9 @@
1
1
  import { CacheInspector } from './integrations/cache_inspector.js';
2
- import { QueueInspector } from './integrations/queue_inspector.js';
3
2
  import type { ApplicationService } from '@adonisjs/core/types';
3
+ import type { QueueInspectorContract } from './integrations/queue_inspector_contract.js';
4
4
  /**
5
5
  * Manages lazy initialization and availability detection for
6
- * cache (Redis) and queue (BullMQ) inspectors.
6
+ * cache (Redis) and queue (BullMQ / @adonisjs/queue) inspectors.
7
7
  *
8
8
  * Extracted from DashboardController to reduce file size and complexity.
9
9
  */
@@ -16,8 +16,8 @@ export declare class InspectorManager {
16
16
  constructor(app: ApplicationService);
17
17
  /** Lazy-init the cache inspector. Returns null if Redis is unavailable. */
18
18
  getCacheInspector(): Promise<CacheInspector | null>;
19
- /** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
20
- getQueueInspector(): Promise<QueueInspector | null>;
19
+ /** Lazy-init the queue inspector. Returns null if neither BullMQ nor @adonisjs/queue is available. */
20
+ getQueueInspector(): Promise<QueueInspectorContract | null>;
21
21
  /** Fetch cache overview stats for the overview page. */
22
22
  fetchCacheOverview(): Promise<{
23
23
  available: boolean;
@@ -1,9 +1,10 @@
1
1
  import { log } from '../utils/logger.js';
2
2
  import { CacheInspector } from './integrations/cache_inspector.js';
3
3
  import { QueueInspector } from './integrations/queue_inspector.js';
4
+ import { AdonisQueueInspector } from './integrations/adonisjs_queue_inspector.js';
4
5
  /**
5
6
  * Manages lazy initialization and availability detection for
6
- * cache (Redis) and queue (BullMQ) inspectors.
7
+ * cache (Redis) and queue (BullMQ / @adonisjs/queue) inspectors.
7
8
  *
8
9
  * Extracted from DashboardController to reduce file size and complexity.
9
10
  */
@@ -39,22 +40,31 @@ export class InspectorManager {
39
40
  return null;
40
41
  }
41
42
  }
42
- /** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
43
+ /** Lazy-init the queue inspector. Returns null if neither BullMQ nor @adonisjs/queue is available. */
43
44
  async getQueueInspector() {
44
45
  if (this.queueAvailable === false)
45
46
  return null;
46
47
  if (this.queueInspector)
47
48
  return this.queueInspector;
48
49
  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;
50
+ // Try BullMQ (@rlanz/bull-queue) first.
51
+ if (await QueueInspector.isAvailable(this.app)) {
52
+ const queue = await this.app.container.make('rlanz/queue');
53
+ this.queueInspector = new QueueInspector(queue);
54
+ this.queueAvailable = true;
55
+ log.info('dashboard: BullMQ detected — Jobs panel enabled');
56
+ return this.queueInspector;
54
57
  }
55
- const queue = await this.app.container.make('rlanz/queue');
56
- this.queueInspector = new QueueInspector(queue);
57
- return this.queueInspector;
58
+ // Fall back to @adonisjs/queue.
59
+ if (await AdonisQueueInspector.isAvailable(this.app)) {
60
+ this.queueInspector = new AdonisQueueInspector(this.app);
61
+ this.queueAvailable = true;
62
+ log.info('dashboard: @adonisjs/queue detected — Jobs panel enabled');
63
+ return this.queueInspector;
64
+ }
65
+ this.queueAvailable = false;
66
+ log.info('dashboard: Queue not detected — Jobs panel disabled');
67
+ return null;
58
68
  }
59
69
  catch (err) {
60
70
  this.queueAvailable = false;
@@ -0,0 +1,42 @@
1
+ import type { ApplicationService } from '@adonisjs/core/types';
2
+ import type { QueueInspectorContract } from './queue_inspector_contract.js';
3
+ import type { QueueOverview, QueueJobDetail, QueueJobListResult, JobStatus } from './queue_inspector_contract.js';
4
+ import type { QueueStoreReader } from './adonisjs_queue_store.js';
5
+ /**
6
+ * Inspects @adonisjs/queue jobs via the shared store reader.
7
+ *
8
+ * Supports both the database (Lucid/Knex) and Redis drivers.
9
+ * All methods catch errors and return safe defaults.
10
+ */
11
+ export declare class AdonisQueueInspector implements QueueInspectorContract {
12
+ #private;
13
+ _resolveReader: (app: ApplicationService) => Promise<QueueStoreReader | null>;
14
+ constructor(app: ApplicationService, ...args: unknown[]);
15
+ /**
16
+ * Detect whether `@adonisjs/queue` is registered in the application container.
17
+ */
18
+ static isAvailable(app: ApplicationService): Promise<boolean>;
19
+ /**
20
+ * Get an overview of job counts by status across all queues.
21
+ * The @adonisjs/queue driver has no concept of paused queues, so `paused` is always 0.
22
+ */
23
+ getOverview(): Promise<QueueOverview>;
24
+ /**
25
+ * List jobs filtered by status with pagination.
26
+ *
27
+ * @param status Job status to filter by, or `'all'` for every status.
28
+ * @param page Page number (1-based).
29
+ * @param perPage Jobs per page.
30
+ */
31
+ listJobs(status?: JobStatus | 'all', page?: number, perPage?: number): Promise<QueueJobListResult>;
32
+ /**
33
+ * Get full detail for a single job by ID.
34
+ */
35
+ getJob(id: string): Promise<QueueJobDetail | null>;
36
+ /**
37
+ * Retry a failed job.
38
+ *
39
+ * @returns `true` if the job was successfully requeued.
40
+ */
41
+ retryJob(id: string): Promise<boolean>;
42
+ }
@@ -0,0 +1,135 @@
1
+ import { resolveFromApplication } from './adonisjs_queue_store.js';
2
+ // ---------------------------------------------------------------------------
3
+ // AdonisQueueInspector
4
+ // ---------------------------------------------------------------------------
5
+ /**
6
+ * Inspects @adonisjs/queue jobs via the shared store reader.
7
+ *
8
+ * Supports both the database (Lucid/Knex) and Redis drivers.
9
+ * All methods catch errors and return safe defaults.
10
+ */
11
+ export class AdonisQueueInspector {
12
+ #app;
13
+ #readerPromise = null;
14
+ constructor(app, ...args) {
15
+ this.#app = app;
16
+ // Allow an optional second arg to inject a fake resolver in tests.
17
+ if (typeof args[0] === 'function') {
18
+ this._resolveReader = args[0];
19
+ }
20
+ else {
21
+ this._resolveReader = (a) => resolveFromApplication(a);
22
+ }
23
+ }
24
+ /**
25
+ * Detect whether `@adonisjs/queue` is registered in the application container.
26
+ */
27
+ static async isAvailable(app) {
28
+ try {
29
+ await app.container.make('queue.manager');
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Private helpers
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Lazily resolve and cache the store reader.
41
+ *
42
+ * Memoizes the *promise* (not a boolean flag) so concurrent callers —
43
+ * e.g. `getOverview()` + `listJobs()` fired together via `Promise.all` —
44
+ * all await the same in-flight resolution instead of racing on a half-set
45
+ * flag and getting a still-null reader.
46
+ */
47
+ async #getReader() {
48
+ if (!this.#readerPromise) {
49
+ this.#readerPromise = this._resolveReader(this.#app);
50
+ }
51
+ return this.#readerPromise;
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // QueueInspectorContract implementation
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Get an overview of job counts by status across all queues.
58
+ * The @adonisjs/queue driver has no concept of paused queues, so `paused` is always 0.
59
+ */
60
+ async getOverview() {
61
+ const defaults = {
62
+ active: 0,
63
+ waiting: 0,
64
+ delayed: 0,
65
+ completed: 0,
66
+ failed: 0,
67
+ paused: 0,
68
+ };
69
+ try {
70
+ const reader = await this.#getReader();
71
+ if (!reader)
72
+ return defaults;
73
+ const counts = await reader.getCounts();
74
+ return {
75
+ active: counts.active,
76
+ waiting: counts.waiting,
77
+ delayed: counts.delayed,
78
+ completed: counts.completed,
79
+ failed: counts.failed,
80
+ paused: 0,
81
+ };
82
+ }
83
+ catch {
84
+ return defaults;
85
+ }
86
+ }
87
+ /**
88
+ * List jobs filtered by status with pagination.
89
+ *
90
+ * @param status Job status to filter by, or `'all'` for every status.
91
+ * @param page Page number (1-based).
92
+ * @param perPage Jobs per page.
93
+ */
94
+ async listJobs(status = 'all', page = 1, perPage = 25) {
95
+ try {
96
+ const reader = await this.#getReader();
97
+ if (!reader)
98
+ return { jobs: [], total: 0 };
99
+ return await reader.listJobs(status === 'paused' ? 'all' : status, page, perPage);
100
+ }
101
+ catch {
102
+ return { jobs: [], total: 0 };
103
+ }
104
+ }
105
+ /**
106
+ * Get full detail for a single job by ID.
107
+ */
108
+ async getJob(id) {
109
+ try {
110
+ const reader = await this.#getReader();
111
+ if (!reader)
112
+ return null;
113
+ return await reader.getJob(id);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Retry a failed job.
121
+ *
122
+ * @returns `true` if the job was successfully requeued.
123
+ */
124
+ async retryJob(id) {
125
+ try {
126
+ const reader = await this.#getReader();
127
+ if (!reader)
128
+ return false;
129
+ return await reader.retryJob(id);
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
135
+ }