adonisjs-server-stats 1.16.1 → 1.18.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 (109) hide show
  1. package/README.md +25 -8
  2. package/dist/core/core/dashboard-api.d.ts +6 -4
  3. package/dist/core/core/define-config-helpers.d.ts +1 -0
  4. package/dist/core/core/types-diagnostics.d.ts +2 -0
  5. package/dist/core/debug/types.d.ts +2 -0
  6. package/dist/core/index.js +9 -6
  7. package/dist/core/types.d.ts +55 -4
  8. package/dist/react/{CacheSection-DkLtukWz.js → CacheSection-B-lSIN8Q.js} +48 -44
  9. package/dist/react/{CacheTab-DWeXnEBI.js → CacheTab-BN0QgPXz.js} +1 -1
  10. package/dist/react/{ConfigSection-DaVbFPDg.js → ConfigSection-u_TSfb9f.js} +1 -1
  11. package/dist/react/{ConfigTab-D4640WJZ.js → ConfigTab-au26s-ON.js} +1 -1
  12. package/dist/react/{CustomPaneTab-DglmAVMC.js → CustomPaneTab-qvMnh8ss.js} +1 -1
  13. package/dist/react/{EmailsSection-vYx6ExTb.js → EmailsSection-HfEMeRex.js} +65 -57
  14. package/dist/react/{EmailsTab-DKDrRmRH.js → EmailsTab-BWwq-CxB.js} +1 -1
  15. package/dist/react/{EventsSection-CapH5xut.js → EventsSection-CA9b15dJ.js} +1 -1
  16. package/dist/react/{EventsTab-BNVEySAm.js → EventsTab-CgSdHbKU.js} +1 -1
  17. package/dist/react/{InternalsContent-C9lIA92C.js → InternalsContent-CI7HvBtH.js} +51 -47
  18. package/dist/react/{InternalsSection-lOv_UcSV.js → InternalsSection-IEeWXJqP.js} +1 -1
  19. package/dist/react/{InternalsTab-BygoSorC.js → InternalsTab-CoVLHQbF.js} +1 -1
  20. package/dist/react/{JobsSection-ZIQsJWid.js → JobsSection-DgsS_VwU.js} +1 -1
  21. package/dist/react/{JobsTab-DXQzXrDt.js → JobsTab-CzwLSRUC.js} +1 -1
  22. package/dist/react/{LogEntryRow-BWkHE51-.js → LogEntryRow-VCCzwD1H.js} +1 -1
  23. package/dist/react/{LogsSection-ConXdBkL.js → LogsSection-BujNn_cO.js} +2 -2
  24. package/dist/react/{LogsTab-CJM47LPn.js → LogsTab-BtD1h7DM.js} +2 -2
  25. package/dist/react/{OverviewSection-DOMu2qvl.js → OverviewSection-B28em0gQ.js} +119 -111
  26. package/dist/react/{QueriesSection-DFsaOSJI.js → QueriesSection-B0tFn_Dc.js} +1 -1
  27. package/dist/react/{QueriesTab-CY9CG_7L.js → QueriesTab-DJY7aavR.js} +1 -1
  28. package/dist/react/{RequestsSection-CC-eVHsl.js → RequestsSection-DPmYM7tq.js} +2 -2
  29. package/dist/react/{RoutesSection-p1DMq41y.js → RoutesSection-Ck-RcFVS.js} +1 -1
  30. package/dist/react/{RoutesTab-Cnqy8UcK.js → RoutesTab-Ch3PDLzo.js} +1 -1
  31. package/dist/react/{SplitPaneWrapper-XgkA0QxE.js → SplitPaneWrapper-BZ6VrBr-.js} +1 -1
  32. package/dist/react/{TimelineTab-1YOERxe5.js → TimelineTab-_byxh2mJ.js} +2 -2
  33. package/dist/react/components/Dashboard/overview-data-context.d.ts +15 -0
  34. package/dist/react/components/Dashboard/sections/OverviewSection.d.ts +9 -0
  35. package/dist/react/{index-DOSlCpZ9.js → index-De7nsWBN.js} +148 -141
  36. package/dist/react/index.js +1 -1
  37. package/dist/src/collectors/app_collector.d.ts +11 -0
  38. package/dist/src/collectors/app_collector.js +11 -0
  39. package/dist/src/collectors/redis_collector.js +13 -13
  40. package/dist/src/dashboard/cache_handlers.d.ts +17 -3
  41. package/dist/src/dashboard/cache_handlers.js +14 -19
  42. package/dist/src/dashboard/coalesce_cache.d.ts +2 -8
  43. package/dist/src/dashboard/coalesce_cache.js +25 -0
  44. package/dist/src/dashboard/dashboard_controller.d.ts +1 -0
  45. package/dist/src/dashboard/dashboard_controller.js +8 -3
  46. package/dist/src/dashboard/dashboard_store.d.ts +0 -1
  47. package/dist/src/dashboard/dashboard_store.js +18 -18
  48. package/dist/src/dashboard/filter_handlers.js +26 -4
  49. package/dist/src/dashboard/flush_manager.d.ts +1 -0
  50. package/dist/src/dashboard/flush_manager.js +6 -1
  51. package/dist/src/dashboard/format_helpers.d.ts +0 -4
  52. package/dist/src/dashboard/format_helpers.js +0 -19
  53. package/dist/src/dashboard/integrations/config_inspector.js +6 -3
  54. package/dist/src/dashboard/knex_factory.js +8 -0
  55. package/dist/src/dashboard/migrator.d.ts +7 -2
  56. package/dist/src/dashboard/migrator.js +140 -6
  57. package/dist/src/dashboard/migrator_tables.js +8 -0
  58. package/dist/src/dashboard/paginate_helper.js +21 -11
  59. package/dist/src/dashboard/sensitive_patterns.d.ts +37 -0
  60. package/dist/src/dashboard/sensitive_patterns.js +146 -5
  61. package/dist/src/dashboard/storage_stats.d.ts +5 -0
  62. package/dist/src/dashboard/storage_stats.js +15 -1
  63. package/dist/src/dashboard/write_queue.d.ts +20 -21
  64. package/dist/src/dashboard/write_queue.js +37 -60
  65. package/dist/src/debug/query_collector.js +6 -1
  66. package/dist/src/debug/trace_collector.js +26 -13
  67. package/dist/src/debug/types.d.ts +2 -0
  68. package/dist/src/define_config.js +2 -0
  69. package/dist/src/edge/client/dashboard.js +2 -2
  70. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  71. package/dist/src/edge/client-vue/dashboard.js +5 -5
  72. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  73. package/dist/src/edge/views/dashboard.edge +1 -1
  74. package/dist/src/edge/views/debug-panel.edge +1 -1
  75. package/dist/src/edge/views/stats-bar.edge +1 -1
  76. package/dist/src/middleware/request_tracking_middleware.js +7 -2
  77. package/dist/src/provider/dashboard_init.d.ts +9 -1
  78. package/dist/src/provider/dashboard_init.js +10 -0
  79. package/dist/src/provider/dashboard_setup.js +14 -0
  80. package/dist/src/provider/diagnostics.d.ts +2 -0
  81. package/dist/src/provider/diagnostics.js +1 -0
  82. package/dist/src/provider/email_helpers.js +20 -5
  83. package/dist/src/provider/server_stats_provider.d.ts +10 -0
  84. package/dist/src/provider/server_stats_provider.js +26 -7
  85. package/dist/src/provider/shutdown_helpers.js +3 -1
  86. package/dist/src/provider/toolbar_setup.js +21 -15
  87. package/dist/src/stubs/config.stub +4 -0
  88. package/dist/src/types.d.ts +55 -4
  89. package/dist/vue/CacheSection-z2TQRez9.js +161 -0
  90. package/dist/vue/{ConfigSection-BiRAiaHj.js → ConfigSection-CFEaig69.js} +1 -1
  91. package/dist/vue/{EmailsSection-Dl44qyqY.js → EmailsSection-CYohBo62.js} +89 -85
  92. package/dist/vue/{EventsSection-CWjeitjU.js → EventsSection-B_TuVrGj.js} +1 -1
  93. package/dist/vue/{InternalsSection-jhTCuK3j.js → InternalsSection-B1P2kNT-.js} +92 -84
  94. package/dist/vue/{InternalsTab-Z3c82glB.js → InternalsTab-CRI-IFTo.js} +155 -147
  95. package/dist/vue/{JobsSection-DOBb4LjZ.js → JobsSection-AjkEzyiv.js} +1 -1
  96. package/dist/vue/{LogsSection-CXx-HOWJ.js → LogsSection-DZD0CCDi.js} +1 -1
  97. package/dist/vue/{OverviewSection-CyfNQ8uV.js → OverviewSection-BEc4Gktf.js} +1 -1
  98. package/dist/vue/{QueriesSection-CXBsFp-y.js → QueriesSection-BMImGXTn.js} +1 -1
  99. package/dist/vue/{RequestsSection-YTIaZGZd.js → RequestsSection-DOI2MUFX.js} +1 -1
  100. package/dist/vue/{RoutesSection-CDKMey49.js → RoutesSection-C1sxt-AW.js} +1 -1
  101. package/dist/vue/composables/useDashboardData.d.ts +1 -1
  102. package/dist/vue/{index-14x39RI_.js → index-BfPAAjL5.js} +95 -95
  103. package/dist/vue/index.js +1 -1
  104. package/package.json +12 -3
  105. package/dist/src/dashboard/explain_query.d.ts +0 -8
  106. package/dist/src/dashboard/explain_query.js +0 -22
  107. package/dist/src/provider/boot_initializer.d.ts +0 -28
  108. package/dist/src/provider/boot_initializer.js +0 -35
  109. package/dist/vue/CacheSection-CxEBVVkF.js +0 -156
package/README.md CHANGED
@@ -13,6 +13,8 @@ Drop a single Edge tag into your layout and get a live stats bar showing CPU, me
13
13
 
14
14
  Zero frontend dependencies. Zero build step. Just `@serverStats()` and go.
15
15
 
16
+ **Zero runtime dependencies.** Everything this package needs at runtime is either built into Node or an optional peer dependency you already chose to install (`@adonisjs/lucid`, `better-sqlite3`, ...). Nothing extra lands in your production `node_modules` supply chain.
17
+
16
18
  **New (alpha):** Native [React & Vue components](#react--vue-inertiajs--alpha) for Inertia.js apps — same features, framework-native.
17
19
 
18
20
  ![adonisjs-server-stats demo](https://raw.githubusercontent.com/simulieren/adonisjs-server-stats/main/screenshots/demo.gif)
@@ -201,10 +203,11 @@ All fields are optional. `defineConfig({})` works with zero configuration.
201
203
 
202
204
  ### `DashboardConfig`
203
205
 
204
- | Option | Type | Default | Description |
205
- | --------------- | -------- | ------------ | -------------------------------------- |
206
- | `path` | `string` | `'/__stats'` | URL path for the dashboard page |
207
- | `retentionDays` | `number` | `7` | Days to keep historical data in SQLite |
206
+ | Option | Type | Default | Description |
207
+ | --------------- | -------- | ------------ | --------------------------------------------------------------------------------- |
208
+ | `path` | `string` | `'/__stats'` | URL path for the dashboard page |
209
+ | `retentionDays` | `number` | `7` | Days to keep historical data in SQLite |
210
+ | `maxDbSizeMb` | `number` | `500` | Size cap for the SQLite database -- oldest rows are pruned when exceeded; `0` disables |
208
211
 
209
212
  ### `ProductionConfig`
210
213
 
@@ -213,6 +216,7 @@ All fields are optional. `defineConfig({})` works with zero configuration.
213
216
  | `enabled` | `boolean` | `false` | Register routes and build the dashboard when `NODE_ENV=production` |
214
217
  | `capture` | `CaptureConfig` | all off | Which capture subsystems to switch on -- opt in one at a time |
215
218
  | `retentionDays` | `number` | `3` | SQLite history to keep in production (the usual default is 7) |
219
+ | `maxDbSizeMb` | `number` | `500` | Size cap for the SQLite database in production; `0` disables |
216
220
 
217
221
  `CaptureConfig` fields, each `boolean` and each defaulting to `false` in production: `queries`, `events`, `emails`, `traces`, `logs`.
218
222
 
@@ -262,9 +266,12 @@ The following field names still work but will show deprecation warnings at boot.
262
266
  | `dashboard` | `boolean` | `false` | Enable the full-page dashboard (requires `better-sqlite3`) |
263
267
  | `dashboardPath` | `string` | `'/__stats'` | URL path for the dashboard page |
264
268
  | `retentionDays` | `number` | `7` | Days to keep historical data in SQLite |
269
+ | `maxDbSizeMb` | `number` | `500` | Size cap for the SQLite database's live data. When exceeded, the cleanup deletes the oldest records regardless of age. `0` disables the cap. |
265
270
  | `dbPath` | `string` | `'.adonisjs/server-stats/dashboard.sqlite3'` | Path to the SQLite database file (relative to app root) |
266
271
  | `debugEndpoint` | `string` | `'/admin/api/debug'` | Base path for the debug toolbar API endpoints |
267
272
  | `excludeFromTracing` | `string[]` | `['/admin/api/debug', '/admin/api/server-stats']` | URL prefixes to exclude from tracing and dashboard persistence. Requests still count toward HTTP metrics but won't appear in the timeline or be stored. The stats endpoint is always excluded automatically. |
273
+ | `cacheKeyPrefix` | `string` | -- | Allow-list prefix for the Cache tab (e.g. `'myapp:cache:'`). Scopes which Redis keys can be listed, read, and deleted. Unset: unrestricted in dev, single-key access refused in production. Env var `SERVER_STATS_CACHE_KEY_PREFIX` works as a fallback. |
274
+ | `emailBridgeChannel` | `string` | `'adonisjs-server-stats:emails'` | Redis pub/sub channel for the cross-process email bridge. Namespace it on a shared Redis so other subscribers cannot read your outbound mail. |
268
275
  | `panes` | `DebugPane[]` | -- | Custom debug panel tabs |
269
276
 
270
277
  ---
@@ -288,6 +295,14 @@ Each collector is a factory function that returns a `MetricCollector`. All colle
288
295
  | `logCollector(opts?)` | Errors/warnings/entries (5m window), entries/minute | optional | -- |
289
296
  | `appCollector()` | Online users, pending webhooks, pending emails | none | `@adonisjs/lucid` |
290
297
 
298
+ > [!WARNING]
299
+ > `appCollector()` polls **your primary database** -- three `COUNT(*)` queries every collection interval (3s by default), forever. Index the columns it filters on or every tick is a full table scan:
300
+ >
301
+ > ```sql
302
+ > CREATE INDEX webhook_events_status_index ON webhook_events (status);
303
+ > CREATE INDEX scheduled_emails_status_index ON scheduled_emails (status);
304
+ > ```
305
+
291
306
  ### Collector Options
292
307
 
293
308
  ```ts
@@ -449,9 +464,11 @@ A disabled subsystem is never subscribed, so it costs nothing -- its dashboard p
449
464
 
450
465
  ### Disk
451
466
 
452
- Retention prunes rows older than `retentionDays` on boot and hourly. It does **not** run `VACUUM`, so the `.sqlite3` file reuses freed pages rather than shrinking -- expect the file to plateau, not shrink. Watch real numbers in the dashboard's storage panel (`GET {dashboardPath}/api/storage`), which reports file size, WAL size, and per-table row counts.
467
+ Retention prunes rows older than `retentionDays` on boot and hourly, and returns freed pages to the OS incrementally, so the `.sqlite3` file genuinely shrinks after cleanup. Watch real numbers in the dashboard's storage panel (`GET {dashboardPath}/api/storage`), which reports file size, WAL size, and per-table row counts.
468
+
469
+ `maxDbSizeMb` (default 500) additionally caps the database's live data size. When write volume outruns what `retentionDays` alone would keep small -- tracing plus persistence on a busy app can write hundreds of MB per day -- the cleanup deletes the globally oldest records, regardless of age, until usage drops below the cap. In other words, the retention window shortens dynamically instead of the file growing without bound. Set `maxDbSizeMb: 0` to disable the cap.
453
470
 
454
- There is no sampling and no hard size cap. On a high-traffic app, enable capture deliberately and keep `retentionDays` short.
471
+ There is no sampling. On a high-traffic app, enable capture deliberately and keep `retentionDays` short.
455
472
 
456
473
  ### Async guards and the toolbar
457
474
 
@@ -464,9 +481,9 @@ An `authorize` callback may be sync or async -- the route guard awaits it, so `a
464
481
 
465
482
  Worth knowing before you expose it, even behind an admin guard:
466
483
 
467
- - **One guard, no tiers.** Anyone who passes `authorize` can also `DELETE /api/cache/:key` and `POST /api/jobs/:id/retry`. Scope cache access with `SERVER_STATS_CACHE_KEY_PREFIX` -- unset means unrestricted.
484
+ - **One guard, no tiers.** Anyone who passes `authorize` can also `DELETE /api/cache/:key` and `POST /api/jobs/:id/retry`. Scope cache access with `advanced.cacheKeyPrefix` (or the `SERVER_STATS_CACHE_KEY_PREFIX` env var) -- unset means unrestricted in dev, and in production single-key cache reads/deletes are refused until a prefix is configured.
468
485
  - **No audit log and no rate limiting** on those mutating endpoints.
469
- - **SQL bindings are truncated at 256 characters but not redacted by key name.** A short secret passed as a bound parameter is stored as-is. Config and env values *are* redacted; query bindings are not.
486
+ - **Redaction is pattern-based, not exhaustive.** SQL bindings, config/env values, stored URLs, and persisted log records are all redacted against a shared rule set (credential-shaped names incl. camelCase, hashes, JWTs, provider key prefixes, long hex/base64 blobs), and anything that survives is truncated at 256 chars. A secret under a name the patterns don't recognise, with an ordinary-looking value, still gets stored.
470
487
 
471
488
  ---
472
489
 
@@ -45,9 +45,10 @@ export declare class DashboardApi {
45
45
  /**
46
46
  * Fetch the value of a specific cache key.
47
47
  *
48
- * @param key - Cache key (will be URI-encoded).
48
+ * @param key - Cache key (will be URI-encoded).
49
+ * @param init - Optional `RequestInit` overrides (e.g. `{ signal }`).
49
50
  */
50
- fetchCacheKey(key: string): Promise<Record<string, unknown>>;
51
+ fetchCacheKey(key: string, init?: RequestInit): Promise<Record<string, unknown>>;
51
52
  /**
52
53
  * Delete a cache entry by key.
53
54
  *
@@ -59,7 +60,8 @@ export declare class DashboardApi {
59
60
  /**
60
61
  * Fetch the HTML preview for an email record.
61
62
  *
62
- * @param id - Email record ID.
63
+ * @param id - Email record ID.
64
+ * @param init - Optional `RequestInit` overrides (e.g. `{ signal }`).
63
65
  */
64
- fetchEmailPreview(id: number): Promise<string>;
66
+ fetchEmailPreview(id: number, init?: RequestInit): Promise<string>;
65
67
  }
@@ -14,6 +14,7 @@ export declare function applyToolbarSetting(toolbar: boolean | ToolbarConfig, re
14
14
  export declare function applyDashboardSetting(dashboard: boolean | {
15
15
  path?: string;
16
16
  retentionDays?: number;
17
+ maxDbSizeMb?: number;
17
18
  }, result: DevToolbarOptions): void;
18
19
  /**
19
20
  * Apply advanced config fields onto the result DevToolbarOptions.
@@ -51,6 +51,7 @@ export interface DiagnosticsResponse {
51
51
  dashboardPath: string;
52
52
  debugEndpoint: string;
53
53
  retentionDays: number;
54
+ maxDbSizeMb: number;
54
55
  dbPath: string;
55
56
  persistDebugData: boolean | string;
56
57
  renderer: string;
@@ -77,6 +78,7 @@ export interface DiagnosticsResponse {
77
78
  fileSizeMb: number;
78
79
  walSizeMb: number;
79
80
  retentionDays: number;
81
+ maxDbSizeMb: number;
80
82
  tables: Array<{
81
83
  name: string;
82
84
  rowCount: number;
@@ -284,6 +284,8 @@ export interface DevToolbarConfig {
284
284
  dashboardPath: string;
285
285
  /** Data retention period in days for historical persistence. */
286
286
  retentionDays: number;
287
+ /** Size cap in MB for the SQLite database's live data; `0` disables it. */
288
+ maxDbSizeMb: number;
287
289
  /** Path to the SQLite database file for historical persistence. */
288
290
  dbPath: string;
289
291
  /** Base path for the debug toolbar API endpoints. */
@@ -1110,11 +1110,13 @@ class At {
1110
1110
  /**
1111
1111
  * Fetch the value of a specific cache key.
1112
1112
  *
1113
- * @param key - Cache key (will be URI-encoded).
1113
+ * @param key - Cache key (will be URI-encoded).
1114
+ * @param init - Optional `RequestInit` overrides (e.g. `{ signal }`).
1114
1115
  */
1115
- async fetchCacheKey(e) {
1116
+ async fetchCacheKey(e, r) {
1116
1117
  return this.client.fetch(
1117
- `${this.basePath}/cache/${encodeURIComponent(e)}`
1118
+ `${this.basePath}/cache/${encodeURIComponent(e)}`,
1119
+ r
1118
1120
  );
1119
1121
  }
1120
1122
  /**
@@ -1133,10 +1135,11 @@ class At {
1133
1135
  /**
1134
1136
  * Fetch the HTML preview for an email record.
1135
1137
  *
1136
- * @param id - Email record ID.
1138
+ * @param id - Email record ID.
1139
+ * @param init - Optional `RequestInit` overrides (e.g. `{ signal }`).
1137
1140
  */
1138
- async fetchEmailPreview(e) {
1139
- return this.client.fetch(`${this.basePath}/emails/${e}/preview`);
1141
+ async fetchEmailPreview(e, r) {
1142
+ return this.client.fetch(`${this.basePath}/emails/${e}/preview`, r);
1140
1143
  }
1141
1144
  }
1142
1145
  function $t(t) {
@@ -263,6 +263,15 @@ export interface DevToolbarOptions {
263
263
  * @default 7
264
264
  */
265
265
  retentionDays?: number;
266
+ /**
267
+ * Size cap in MB for the SQLite database's live data. When exceeded,
268
+ * the hourly cleanup deletes the oldest records — regardless of age —
269
+ * until usage drops below the cap, effectively shortening the retention
270
+ * window when write volume outruns `retentionDays`. Set to `0` to
271
+ * disable the cap.
272
+ * @default 500
273
+ */
274
+ maxDbSizeMb?: number;
266
275
  /**
267
276
  * Path to the SQLite database file for historical persistence.
268
277
  * Relative to app root.
@@ -434,6 +443,17 @@ export interface DashboardConfig {
434
443
  * @default 7
435
444
  */
436
445
  retentionDays?: number;
446
+ /**
447
+ * Size cap in MB for the SQLite database's live data.
448
+ *
449
+ * When exceeded, the cleanup deletes the oldest records — regardless
450
+ * of age — until usage drops below the cap. This bounds disk usage
451
+ * when write volume (tracing, verbose query logging) outruns what
452
+ * `retentionDays` alone would keep small. Set to `0` to disable.
453
+ *
454
+ * @default 500
455
+ */
456
+ maxDbSizeMb?: number;
437
457
  }
438
458
  /**
439
459
  * Access-control callback signature.
@@ -461,7 +481,7 @@ export type AccessGuard = (ctx: import('@adonisjs/core/http').HttpContext) => bo
461
481
  export interface CaptureConfig {
462
482
  /** SQL text, bindings, and timings for every query. */
463
483
  queries?: boolean;
464
- /** Application events (in-memory only never persisted to SQLite). */
484
+ /** Application events, linked to the request that follows them and persisted with it. */
465
485
  events?: boolean;
466
486
  /** Sent mail, including subject and body. */
467
487
  emails?: boolean;
@@ -515,13 +535,21 @@ export interface ProductionConfig {
515
535
  capture?: CaptureConfig;
516
536
  /**
517
537
  * How many days of history to keep in SQLite, overriding the usual default
518
- * of 7. Retention deletes rows hourly but never runs `VACUUM`, so the
519
- * database file reuses pages rather than shrinking — watch actual size via
520
- * the dashboard's storage panel.
538
+ * of 7. Retention deletes rows hourly and returns freed pages to the OS
539
+ * incrementally — watch actual size via the dashboard's storage panel.
521
540
  *
522
541
  * @default 3
523
542
  */
524
543
  retentionDays?: number;
544
+ /**
545
+ * Size cap in MB for the SQLite database's live data in production,
546
+ * overriding the usual default. When exceeded, the hourly cleanup deletes
547
+ * the oldest records — regardless of age — until usage drops below the
548
+ * cap. Set to `0` to disable.
549
+ *
550
+ * @default 500
551
+ */
552
+ maxDbSizeMb?: number;
525
553
  }
526
554
  /**
527
555
  * Advanced options that most users never need to touch.
@@ -622,6 +650,29 @@ export interface AdvancedConfig {
622
650
  * @default 200
623
651
  */
624
652
  maxTraces?: number;
653
+ /**
654
+ * Allow-list prefix for the dashboard's cache inspector.
655
+ *
656
+ * Scopes which Redis keys the Cache tab may list, read, and delete
657
+ * (e.g. `'myapp:cache:'`). Unset keeps full access in development for
658
+ * back-compat, but in production single-key reads and deletes are
659
+ * refused until a prefix is configured — a production dashboard must
660
+ * not be able to read session keys or delete rate-limit counters.
661
+ *
662
+ * The `SERVER_STATS_CACHE_KEY_PREFIX` env var is used as a fallback.
663
+ */
664
+ cacheKeyPrefix?: string;
665
+ /**
666
+ * Redis pub/sub channel for the cross-process email bridge.
667
+ *
668
+ * Queue workers publish captured mail on this channel and the web
669
+ * process persists it. On a Redis instance shared between apps or
670
+ * tenants, namespace this (e.g. `myapp:server-stats:emails`) so
671
+ * other subscribers cannot read your outbound mail.
672
+ *
673
+ * @default 'adonisjs-server-stats:emails'
674
+ */
675
+ emailBridgeChannel?: string;
625
676
  }
626
677
  /**
627
678
  * Top-level configuration for `adonisjs-server-stats`.
@@ -1,46 +1,50 @@
1
- import { jsxs as o, jsx as t } from "react/jsx-runtime";
2
- import { useState as c, useCallback as k } from "react";
3
- import { formatCacheSize as x, formatTtl as D } from "adonisjs-server-stats/core";
4
- import { a as N, J as V } from "./index-DOSlCpZ9.js";
5
- import { C as L } from "./CacheStatsBar-CRodCOeP.js";
6
- import { D as F } from "./DataTable-YyShr5B-.js";
7
- import { F as T } from "./FilterBar-CQ7bD669.js";
8
- function _({ options: v = {} }) {
9
- const [d, b] = c(""), [l, i] = c(null), [w, n] = c(null), [C, h] = c(!1), [y, r] = c(null), { data: u, isLoading: g, mutate: m, getApi: p } = N("cache", {
10
- ...v,
11
- search: d
12
- }), a = u, K = k(
1
+ import { jsxs as u, jsx as t } from "react/jsx-runtime";
2
+ import { useState as d, useRef as N, useCallback as v, useEffect as V } from "react";
3
+ import { formatCacheSize as L, formatTtl as R } from "adonisjs-server-stats/core";
4
+ import { a as E, J as F } from "./index-De7nsWBN.js";
5
+ import { C as T } from "./CacheStatsBar-CRodCOeP.js";
6
+ import { F as A } from "./FilterBar-CQ7bD669.js";
7
+ import { D as z } from "./DataTable-YyShr5B-.js";
8
+ function M({ options: g = {} }) {
9
+ const [y, w] = d(""), [r, h] = d(null), [C, n] = d(null), [K, m] = d(!1), [p, c] = d(null), s = N(null), { data: f, isLoading: S, mutate: k, getApi: b } = E("cache", {
10
+ ...g,
11
+ search: y
12
+ }), a = f, x = v(
13
13
  async (e) => {
14
14
  if (confirm(`Delete cache key "${e}"?`))
15
15
  try {
16
- await m(`cache/${encodeURIComponent(e)}`, "delete"), l === e && (i(null), n(null), r(null));
16
+ await k(`cache/${encodeURIComponent(e)}`, "delete"), r === e && (h(null), n(null), c(null));
17
17
  } catch {
18
18
  }
19
19
  },
20
- [m, l]
21
- ), S = k(
20
+ [k, r]
21
+ ), D = v(
22
22
  async (e) => {
23
- if (l === e) {
24
- i(null), n(null), r(null);
23
+ if (s.current?.abort(), r === e) {
24
+ s.current = null, h(null), n(null), c(null);
25
25
  return;
26
26
  }
27
- i(e), n(null), r(null), h(!0);
27
+ h(e), n(null), c(null), m(!0);
28
+ const l = new AbortController();
29
+ s.current = l;
28
30
  try {
29
- const s = await p().fetchCacheKey(e);
31
+ const i = await b().fetchCacheKey(e, { signal: l.signal });
32
+ if (s.current !== l) return;
30
33
  n(
31
- s.value !== void 0 ? s.value : s.data !== void 0 ? s.data : s
32
- ), r(null);
33
- } catch {
34
- n(null), r("Failed to fetch key value");
34
+ i.value !== void 0 ? i.value : i.data !== void 0 ? i.data : i
35
+ ), c(null);
36
+ } catch (o) {
37
+ if (o instanceof Error && o.name === "AbortError" || s.current !== l) return;
38
+ n(null), c("Failed to fetch key value");
35
39
  } finally {
36
- h(!1);
40
+ s.current === l && m(!1);
37
41
  }
38
42
  },
39
- [l, p]
43
+ [r, b]
40
44
  );
41
- return /* @__PURE__ */ o("div", { children: [
45
+ return V(() => () => s.current?.abort(), []), /* @__PURE__ */ u("div", { children: [
42
46
  a?.available && a?.stats && /* @__PURE__ */ t(
43
- L,
47
+ T,
44
48
  {
45
49
  hitRate: a.stats.hitRate ?? 0,
46
50
  hits: a.stats.hits ?? 0,
@@ -49,16 +53,16 @@ function _({ options: v = {} }) {
49
53
  }
50
54
  ),
51
55
  /* @__PURE__ */ t(
52
- T,
56
+ A,
53
57
  {
54
- search: d,
55
- onSearchChange: b,
58
+ search: y,
59
+ onSearchChange: w,
56
60
  placeholder: "Filter cache keys...",
57
61
  summary: `${(a?.keys || a?.data || []).length} keys`
58
62
  }
59
63
  ),
60
- g && !u ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Loading cache..." }) : !a || !a.available ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Cache inspector not available" }) : /* @__PURE__ */ t("div", { className: "ss-dash-table-wrap", children: /* @__PURE__ */ t(
61
- F,
64
+ S && !f ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Loading cache..." }) : !a || !a.available ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Cache inspector not available" }) : /* @__PURE__ */ t("div", { className: "ss-dash-table-wrap", children: /* @__PURE__ */ t(
65
+ z,
62
66
  {
63
67
  columns: [
64
68
  {
@@ -89,25 +93,25 @@ function _({ options: v = {} }) {
89
93
  key: "size",
90
94
  label: "Size",
91
95
  width: "60px",
92
- render: (e) => e != null && e > 0 ? x(e) : "-"
96
+ render: (e) => e != null && e > 0 ? L(e) : "-"
93
97
  },
94
98
  {
95
99
  key: "ttl",
96
100
  label: "TTL",
97
101
  width: "70px",
98
- render: (e) => e > 0 ? D(e) : "-"
102
+ render: (e) => e > 0 ? R(e) : "-"
99
103
  },
100
104
  {
101
105
  key: "_actions",
102
106
  label: "",
103
107
  width: "60px",
104
- render: (e, f) => /* @__PURE__ */ t(
108
+ render: (e, l) => /* @__PURE__ */ t(
105
109
  "button",
106
110
  {
107
111
  type: "button",
108
112
  className: "ss-dash-retry-btn",
109
- onClick: (s) => {
110
- s.stopPropagation(), K(f.key);
113
+ onClick: (o) => {
114
+ o.stopPropagation(), x(l.key);
111
115
  },
112
116
  children: "Delete"
113
117
  }
@@ -116,20 +120,20 @@ function _({ options: v = {} }) {
116
120
  ],
117
121
  data: a.keys || a.data || [],
118
122
  keyField: "key",
119
- onRowClick: (e) => S(e.key),
123
+ onRowClick: (e) => D(e.key),
120
124
  emptyMessage: "No cache keys found"
121
125
  }
122
126
  ) }),
123
- l && /* @__PURE__ */ o("div", { className: "ss-dash-cache-detail", children: [
124
- /* @__PURE__ */ o("h4", { children: [
127
+ r && /* @__PURE__ */ u("div", { className: "ss-dash-cache-detail", children: [
128
+ /* @__PURE__ */ u("h4", { children: [
125
129
  "Key: ",
126
- l
130
+ r
127
131
  ] }),
128
- C ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Loading value..." }) : y ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", style: { color: "var(--ss-red-fg)" }, children: y }) : /* @__PURE__ */ t(V, { data: w })
132
+ K ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", children: "Loading value..." }) : p ? /* @__PURE__ */ t("div", { className: "ss-dash-empty", style: { color: "var(--ss-red-fg)" }, children: p }) : /* @__PURE__ */ t(F, { data: C })
129
133
  ] })
130
134
  ] });
131
135
  }
132
136
  export {
133
- _ as CacheSection,
134
- _ as default
137
+ M as CacheSection,
138
+ M as default
135
139
  };
@@ -2,7 +2,7 @@ import { jsx as e, jsxs as t } from "react/jsx-runtime";
2
2
  import { useState as p, useRef as z, useMemo as A, useCallback as K, useEffect as L } from "react";
3
3
  import { formatTtl as $, formatCacheSize as j } from "adonisjs-server-stats/core";
4
4
  import { u as F } from "./useDashboardApiBase-Bi36pJ2L.js";
5
- import { u as D, J as E } from "./index-DOSlCpZ9.js";
5
+ import { u as D, J as E } from "./index-De7nsWBN.js";
6
6
  import { u as V } from "./useResizableTable-CNJmACdt.js";
7
7
  import { C as J } from "./CacheStatsBar-CRodCOeP.js";
8
8
  import { F as M } from "./FilterBar-CQ7bD669.js";
@@ -1,5 +1,5 @@
1
1
  import { jsx as t } from "react/jsx-runtime";
2
- import { a as i } from "./index-DOSlCpZ9.js";
2
+ import { a as i } from "./index-De7nsWBN.js";
3
3
  import { C as n } from "./ConfigContent-Bwvfl_G7.js";
4
4
  function m({ options: o = {} }) {
5
5
  const { data: a, isLoading: s } = i("config", o);
@@ -1,6 +1,6 @@
1
1
  import { jsxs as i, jsx as n } from "react/jsx-runtime";
2
2
  import { u as f } from "./useDashboardApiBase-Bi36pJ2L.js";
3
- import { u as m } from "./index-DOSlCpZ9.js";
3
+ import { u as m } from "./index-De7nsWBN.js";
4
4
  import { C as d } from "./ConfigContent-Bwvfl_G7.js";
5
5
  function b({ options: r, dashboardPath: o }) {
6
6
  const { resolvedOptions: a } = f(o, r), { data: e, isLoading: t, error: s } = m("config", a);
@@ -1,7 +1,7 @@
1
1
  import { jsxs as l, jsx as r } from "react/jsx-runtime";
2
2
  import { useState as w, useMemo as f } from "react";
3
3
  import { compactPreview as k, formatDuration as x, durationClassName as M } from "adonisjs-server-stats/core";
4
- import { u as L, B as A, M as B } from "./index-DOSlCpZ9.js";
4
+ import { u as L, B as A, M as B } from "./index-De7nsWBN.js";
5
5
  import { T as b } from "./TimeAgoCell-o3KigGfM.js";
6
6
  import { u as D } from "./useResizableTable-CNJmACdt.js";
7
7
  import { F as P } from "./FilterBar-CQ7bD669.js";