adonisjs-server-stats 1.6.1 → 1.6.2

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 (53) hide show
  1. package/dist/src/collectors/app_collector.d.ts.map +1 -1
  2. package/dist/src/collectors/app_collector.js +2 -1
  3. package/dist/src/collectors/auto_detect.d.ts.map +1 -1
  4. package/dist/src/collectors/auto_detect.js +7 -3
  5. package/dist/src/collectors/db_pool_collector.d.ts.map +1 -1
  6. package/dist/src/collectors/db_pool_collector.js +2 -1
  7. package/dist/src/collectors/http_collector.d.ts +4 -3
  8. package/dist/src/collectors/http_collector.d.ts.map +1 -1
  9. package/dist/src/collectors/http_collector.js +3 -5
  10. package/dist/src/collectors/redis_collector.d.ts.map +1 -1
  11. package/dist/src/collectors/redis_collector.js +2 -1
  12. package/dist/src/controller/debug_controller.d.ts +5 -0
  13. package/dist/src/controller/debug_controller.d.ts.map +1 -1
  14. package/dist/src/controller/debug_controller.js +24 -1
  15. package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
  16. package/dist/src/dashboard/chart_aggregator.js +3 -2
  17. package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
  18. package/dist/src/dashboard/dashboard_store.js +47 -11
  19. package/dist/src/dashboard/migrator.d.ts +5 -0
  20. package/dist/src/dashboard/migrator.d.ts.map +1 -1
  21. package/dist/src/dashboard/migrator.js +44 -9
  22. package/dist/src/data/data_access.d.ts +4 -2
  23. package/dist/src/data/data_access.d.ts.map +1 -1
  24. package/dist/src/data/data_access.js +8 -3
  25. package/dist/src/define_config.d.ts.map +1 -1
  26. package/dist/src/define_config.js +7 -6
  27. package/dist/src/edge/client-vue/dashboard.js +1 -1
  28. package/dist/src/edge/client-vue/debug-panel-deferred.js +1 -1
  29. package/dist/src/edge/client-vue/debug-panel.js +1 -1
  30. package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
  31. package/dist/src/middleware/request_tracking_middleware.js +7 -0
  32. package/dist/src/provider/server_stats_provider.d.ts +9 -0
  33. package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
  34. package/dist/src/provider/server_stats_provider.js +196 -59
  35. package/dist/src/utils/app_import.d.ts +23 -0
  36. package/dist/src/utils/app_import.d.ts.map +1 -0
  37. package/dist/src/utils/app_import.js +44 -0
  38. package/dist/vue/{CacheSection-CkrIB4-j.js → CacheSection-C788Yfai.js} +1 -1
  39. package/dist/vue/{ConfigSection-gulpOiq1.js → ConfigSection-CRzYxqW2.js} +1 -1
  40. package/dist/vue/{CustomPaneTab-J57ED_bh.js → CustomPaneTab-BJxT5Dp7.js} +33 -33
  41. package/dist/vue/{EmailsSection-BlKvQDx8.js → EmailsSection-C8JFMtW7.js} +1 -1
  42. package/dist/vue/{EventsSection-BdzQvIVJ.js → EventsSection-C4wXUgxG.js} +1 -1
  43. package/dist/vue/{JobsSection-DOzuMrG3.js → JobsSection-CsKWTjgN.js} +1 -1
  44. package/dist/vue/{LogsSection-CNN4y92u.js → LogsSection-BFVjSZ24.js} +12 -12
  45. package/dist/vue/{LogsTab-CJerb22r.js → LogsTab-DpEQ7euu.js} +17 -17
  46. package/dist/vue/{OverviewSection-SITNR_dA.js → OverviewSection-CbMdAido.js} +1 -1
  47. package/dist/vue/{QueriesSection-BAebAHkD.js → QueriesSection-BPiv7u3r.js} +1 -1
  48. package/dist/vue/{RequestsSection-CIR0IX39.js → RequestsSection-LtImH4rD.js} +1 -1
  49. package/dist/vue/{RoutesSection-j1U2oa0g.js → RoutesSection-CrxOxmzx.js} +1 -1
  50. package/dist/vue/{TimelineSection-Dw980UPg.js → TimelineSection-DLxMW2J_.js} +1 -1
  51. package/dist/vue/{index-COgsk_nv.js → index-qCQpBftQ.js} +2 -2
  52. package/dist/vue/index.js +1 -1
  53. package/package.json +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"request_tracking_middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/request_tracking_middleware.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAcvD;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAYD,wBAAgB,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,IAAI,QAEvE;AAOD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,QAEjE;AAQD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,QAEnD;AASD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAErD;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAQD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC,GAAG,IAAI,QAEpF;AAED,MAAM,CAAC,OAAO,OAAO,yBAAyB;IACtC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM;CA4E5C"}
1
+ {"version":3,"file":"request_tracking_middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/request_tracking_middleware.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;AAcvD;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAYD,wBAAgB,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,IAAI,QAEvE;AAOD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,QAEjE;AAQD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,QAEnD;AASD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAErD;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAQD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC,GAAG,IAAI,QAEpF;AAED,MAAM,CAAC,OAAO,OAAO,yBAAyB;IACtC,MAAM,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM;CAoF5C"}
@@ -67,7 +67,14 @@ export default class RequestTrackingMiddleware {
67
67
  await next();
68
68
  return;
69
69
  }
70
+ // Gracefully handle the startup window before collectors initialize.
71
+ // The provider defers initialization via setImmediate, so early
72
+ // requests may arrive before httpCollector() has been called.
70
73
  const metrics = getRequestMetrics();
74
+ if (!metrics) {
75
+ await next();
76
+ return;
77
+ }
71
78
  const start = performance.now();
72
79
  metrics.incrementActiveConnections();
73
80
  // Share a lazy shouldShow evaluator with Edge for @serverStats() tag.
@@ -14,6 +14,7 @@ export default class ServerStatsProvider {
14
14
  private statsController;
15
15
  private debugController;
16
16
  private apiController;
17
+ private dashboardDepsAvailable;
17
18
  private pinoHookActive;
18
19
  private edgePluginActive;
19
20
  private prometheusActive;
@@ -23,6 +24,7 @@ export default class ServerStatsProvider {
23
24
  private resolvedCollectors;
24
25
  constructor(app: ApplicationService);
25
26
  boot(): Promise<void>;
27
+ private initializeBoot;
26
28
  /**
27
29
  * Read start/kernel.ts and detect auth-related middleware in server.use()
28
30
  * or router.use() blocks. Returns import paths of problematic middleware.
@@ -40,6 +42,13 @@ export default class ServerStatsProvider {
40
42
  */
41
43
  private hookPinoLogger;
42
44
  ready(): Promise<void>;
45
+ private initializeServerStats;
46
+ /**
47
+ * Set up the stats collection interval, transmit broadcasting,
48
+ * and Prometheus integration. Extracted from initializeServerStats
49
+ * so the ready log fires promptly.
50
+ */
51
+ private setupStatsInterval;
43
52
  private setupDevToolbar;
44
53
  /**
45
54
  * Initialize the full-page dashboard: SQLite store, controller,
@@ -1 +1 @@
1
- {"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAQ9D,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAwB1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAvB7C,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,kBAAkB,CAAgC;IAC1D,OAAO,CAAC,uBAAuB,CAA8C;IAC7E,OAAO,CAAC,mBAAmB,CAA6C;IACxE,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,kBAAkB,CAAwB;gBAE5B,GAAG,EAAE,kBAAkB;IAEvC,IAAI;IAwGV;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IAmDlC;;;;;;;OAOG;YACW,cAAc;IAqCtB,KAAK;YAkIG,eAAe;IAmG7B;;;;;;OAMG;YACW,cAAc;IA0I5B,2DAA2D;IAC3D,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2ER,QAAQ;CAwCf"}
1
+ {"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAQ9D,MAAM,CAAC,OAAO,OAAO,mBAAmB;IA2B1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;IA1B7C,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,kBAAkB,CAAgC;IAC1D,OAAO,CAAC,uBAAuB,CAA8C;IAC7E,OAAO,CAAC,mBAAmB,CAA6C;IACxE,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,sBAAsB,CAAgB;IAG9C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,gBAAgB,CAAe;IACvC,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,kBAAkB,CAAwB;gBAE5B,GAAG,EAAE,kBAAkB;IAEvC,IAAI;YAcI,cAAc;IA8I5B;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IAmDlC;;;;;;;OAOG;YACW,cAAc;IAqCtB,KAAK;YAuBG,qBAAqB;IA6FnC;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;YA2DZ,eAAe;IA8G7B;;;;;;OAMG;YACW,cAAc;IA0L5B,2DAA2D;IAC3D,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2ER,QAAQ;CAwCf"}
@@ -1,7 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { getLogStreamService } from '../collectors/log_collector.js';
3
- import { DashboardStore } from '../dashboard/dashboard_store.js';
4
- import { DataAccess } from '../data/data_access.js';
5
3
  import { DebugStore } from '../debug/debug_store.js';
6
4
  import { StatsEngine } from '../engine/stats_engine.js';
7
5
  import { LogStreamService } from '../log_stream/log_stream_service.js';
@@ -23,6 +21,8 @@ export default class ServerStatsProvider {
23
21
  statsController = null;
24
22
  debugController = null;
25
23
  apiController = null;
24
+ // Dashboard dependency check (set in boot, read in ready)
25
+ dashboardDepsAvailable = true;
26
26
  // Diagnostics tracking
27
27
  pinoHookActive = false;
28
28
  edgePluginActive = false;
@@ -35,9 +35,24 @@ export default class ServerStatsProvider {
35
35
  this.app = app;
36
36
  }
37
37
  async boot() {
38
+ try {
39
+ await this.initializeBoot();
40
+ }
41
+ catch (err) {
42
+ log.warn(`boot failed: ${err?.message ?? err}\n` +
43
+ ` ${dim('The server will continue without server-stats.')}`);
44
+ if (err?.stack) {
45
+ console.error(err.stack);
46
+ }
47
+ }
48
+ }
49
+ async initializeBoot() {
38
50
  const config = this.app.config.get('server_stats');
39
- if (!config)
51
+ if (!config) {
52
+ log.warn('no config found — is config/server_stats.ts set up?');
40
53
  return;
54
+ }
55
+ log.info('booting...');
41
56
  // Wire up the per-request shouldShow callback
42
57
  if (config.shouldShow) {
43
58
  setShouldShow(config.shouldShow);
@@ -58,7 +73,36 @@ export default class ServerStatsProvider {
58
73
  const debugEndpoint = toolbarConfig?.enabled
59
74
  ? (toolbarConfig.debugEndpoint ?? '/admin/api/debug')
60
75
  : undefined;
61
- const dashboardPath = toolbarConfig?.enabled && toolbarConfig.dashboard
76
+ // Check dashboard dependencies before registering dashboard routes.
77
+ // Must use appImport — bare import() resolves to this package's
78
+ // devDeps when symlinked, not the app's actual dependencies.
79
+ if (toolbarConfig?.enabled && toolbarConfig.dashboard) {
80
+ const { appImport } = await import('../utils/app_import.js');
81
+ const missing = [];
82
+ try {
83
+ await appImport('knex');
84
+ }
85
+ catch {
86
+ missing.push('knex');
87
+ }
88
+ try {
89
+ await appImport('better-sqlite3');
90
+ }
91
+ catch {
92
+ missing.push('better-sqlite3');
93
+ }
94
+ if (missing.length > 0) {
95
+ this.dashboardDepsAvailable = false;
96
+ log.block(`Dashboard requires ${missing.join(' and ')}. Install with:`, [
97
+ '',
98
+ bold(`npm install ${missing.join(' ')}`),
99
+ '',
100
+ dim('Dashboard routes have been skipped for now.'),
101
+ dim('Everything else (stats bar, debug panel) works without it.'),
102
+ ]);
103
+ }
104
+ }
105
+ const dashboardPath = toolbarConfig?.enabled && toolbarConfig.dashboard && this.dashboardDepsAvailable
62
106
  ? (toolbarConfig.dashboardPath ?? '/__stats')
63
107
  : undefined;
64
108
  // ── Register all routes via the unified registrar ──────────
@@ -85,7 +129,7 @@ export default class ServerStatsProvider {
85
129
  }
86
130
  // Log registered routes
87
131
  if (registeredPaths.length > 0) {
88
- log.list('routes registered:', registeredPaths);
132
+ log.list('routes auto-registered (no manual setup needed):', registeredPaths);
89
133
  // Only warn about global auth middleware if:
90
134
  // 1. shouldShow is NOT configured (user hasn't set up access control)
91
135
  // 2. There IS auth middleware in server.use() or router.use()
@@ -118,7 +162,11 @@ export default class ServerStatsProvider {
118
162
  if (!this.app.usingEdgeJS)
119
163
  return;
120
164
  try {
121
- const edge = await import('edge.js');
165
+ // Must use appImport for edge.js — when this package is symlinked,
166
+ // bare import('edge.js') resolves to the package's devDep copy,
167
+ // which is a different singleton than the app's Edge instance.
168
+ const { appImport } = await import('../utils/app_import.js');
169
+ const edge = await appImport('edge.js');
122
170
  const { edgePluginServerStats } = await import('../edge/plugin.js');
123
171
  edge.default.use(edgePluginServerStats(config));
124
172
  this.edgePluginActive = true;
@@ -228,6 +276,21 @@ export default class ServerStatsProvider {
228
276
  return;
229
277
  if (this.app.inTest && config.skipInTest !== false)
230
278
  return;
279
+ // Defer the entire initialization to setImmediate so ready() returns
280
+ // immediately. AdonisJS waits for all provider ready() hooks before
281
+ // processing HTTP requests — blocking here would hang the server.
282
+ // Routes use lazy controller getters that return 503 until init completes.
283
+ setImmediate(() => {
284
+ this.initializeServerStats(config).catch((err) => {
285
+ log.warn(`failed to initialize: ${err?.message ?? err}\n` +
286
+ ` ${dim('The server will continue without server-stats.')}`);
287
+ if (err?.stack) {
288
+ console.error(err.stack);
289
+ }
290
+ });
291
+ });
292
+ }
293
+ async initializeServerStats(config) {
231
294
  this.resolvedConfig = config;
232
295
  let collectors;
233
296
  if (!config.collectors || config.collectors === 'auto') {
@@ -251,21 +314,27 @@ export default class ServerStatsProvider {
251
314
  // Dev toolbar setup
252
315
  const toolbarConfig = config.devToolbar;
253
316
  if (toolbarConfig?.enabled && !this.app.inProduction) {
254
- await this.setupDevToolbar({
255
- enabled: true,
256
- maxQueries: toolbarConfig.maxQueries ?? 500,
257
- maxEvents: toolbarConfig.maxEvents ?? 200,
258
- maxEmails: toolbarConfig.maxEmails ?? 100,
259
- slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
260
- persistDebugData: toolbarConfig.persistDebugData ?? false,
261
- tracing: toolbarConfig.tracing ?? false,
262
- maxTraces: toolbarConfig.maxTraces ?? 200,
263
- dashboard: toolbarConfig.dashboard ?? false,
264
- dashboardPath: toolbarConfig.dashboardPath ?? '/__stats',
265
- retentionDays: toolbarConfig.retentionDays ?? 7,
266
- dbPath: toolbarConfig.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
267
- debugEndpoint: toolbarConfig.debugEndpoint ?? '/admin/api/debug',
268
- });
317
+ try {
318
+ await this.setupDevToolbar({
319
+ enabled: true,
320
+ maxQueries: toolbarConfig.maxQueries ?? 500,
321
+ maxEvents: toolbarConfig.maxEvents ?? 200,
322
+ maxEmails: toolbarConfig.maxEmails ?? 100,
323
+ slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
324
+ persistDebugData: toolbarConfig.persistDebugData ?? false,
325
+ tracing: toolbarConfig.tracing ?? false,
326
+ maxTraces: toolbarConfig.maxTraces ?? 200,
327
+ dashboard: toolbarConfig.dashboard ?? false,
328
+ dashboardPath: toolbarConfig.dashboardPath ?? '/__stats',
329
+ retentionDays: toolbarConfig.retentionDays ?? 7,
330
+ dbPath: toolbarConfig.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
331
+ debugEndpoint: toolbarConfig.debugEndpoint ?? '/admin/api/debug',
332
+ });
333
+ }
334
+ catch (err) {
335
+ log.warn(`dev toolbar setup failed: ${err?.message ?? err}\n` +
336
+ ` ${dim('Stats bar will still work, but debug panel may be unavailable.')}`);
337
+ }
269
338
  // Exclude the stats endpoint and user-specified prefixes from tracing
270
339
  // so the debug panel's own polling doesn't flood the timeline
271
340
  const debugEndpoint = toolbarConfig.debugEndpoint ?? '/admin/api/debug';
@@ -277,41 +346,58 @@ export default class ServerStatsProvider {
277
346
  if (prefixes.length > 0) {
278
347
  setExcludedPrefixes(prefixes);
279
348
  }
280
- // Create the unified ApiController now that both stores are available
349
+ // Create the unified ApiController now that debug store is available.
350
+ // Dashboard store is passed as a getter so it picks up the reference
351
+ // once setupDashboard() completes asynchronously.
281
352
  if (this.debugStore) {
282
353
  const logPath = this.app.makePath('logs', 'adonisjs.log');
283
- const dataAccess = new DataAccess(this.debugStore, this.dashboardStore, logPath);
354
+ const { DataAccess: DataAccessClass } = await import('../data/data_access.js');
355
+ const dataAccess = new DataAccessClass(this.debugStore, () => this.dashboardStore, logPath);
284
356
  const { ApiController: ApiControllerClass } = await import('../controller/api_controller.js');
285
357
  this.apiController = new ApiControllerClass(dataAccess);
286
358
  }
287
359
  }
360
+ // ── Stats collection interval + transmit (lightweight, set up inline) ──
361
+ this.setupStatsInterval(config);
362
+ log.info('ready');
363
+ }
364
+ /**
365
+ * Set up the stats collection interval, transmit broadcasting,
366
+ * and Prometheus integration. Extracted from initializeServerStats
367
+ * so the ready log fires promptly.
368
+ */
369
+ setupStatsInterval(config) {
288
370
  let transmit = null;
289
- if (config.transport === 'transmit') {
290
- try {
291
- transmit = await this.app.container.make('transmit');
292
- if (transmit) {
293
- this.transmitAvailable = true;
294
- if (config.channelName) {
295
- this.transmitChannels.push(config.channelName);
371
+ let prometheusCollector = null;
372
+ // Resolve transmit + prometheus asynchronously but don't block ready()
373
+ const resolveIntegrations = async () => {
374
+ if (config.transport === 'transmit') {
375
+ try {
376
+ transmit = await this.app.container.make('transmit');
377
+ if (transmit) {
378
+ this.transmitAvailable = true;
379
+ if (config.channelName) {
380
+ this.transmitChannels.push(config.channelName);
381
+ }
296
382
  }
297
383
  }
384
+ catch {
385
+ log.info('transport is "transmit" but @adonisjs/transmit is not installed — falling back to polling');
386
+ }
387
+ }
388
+ try {
389
+ const mod = await import('../prometheus/prometheus_collector.js');
390
+ prometheusCollector = mod.ServerStatsCollector.instance;
298
391
  }
299
392
  catch {
300
- log.info('transport is "transmit" but @adonisjs/transmit is not installed — falling back to polling');
393
+ // Prometheus not installed — skip (optional dependency)
301
394
  }
302
- }
303
- let prometheusCollector = null;
304
- try {
305
- const mod = await import('../prometheus/prometheus_collector.js');
306
- prometheusCollector = mod.ServerStatsCollector.instance;
307
- }
308
- catch {
309
- // Prometheus not installed — skip (optional dependency)
310
- }
311
- if (prometheusCollector) {
312
- this.prometheusActive = true;
313
- log.info('Prometheus integration active');
314
- }
395
+ if (prometheusCollector) {
396
+ this.prometheusActive = true;
397
+ log.info('Prometheus integration active');
398
+ }
399
+ };
400
+ resolveIntegrations().catch(() => { });
315
401
  this.intervalId = setInterval(async () => {
316
402
  try {
317
403
  const stats = await this.engine.collect();
@@ -365,6 +451,7 @@ export default class ServerStatsProvider {
365
451
  getEngine: () => this.engine,
366
452
  getDashboardStore: () => this.dashboardStore,
367
453
  getProviderDiagnostics: () => this.getDiagnostics(),
454
+ getApp: () => this.app,
368
455
  });
369
456
  // Wire trace collector into the request tracking middleware
370
457
  if (this.debugStore.traces) {
@@ -415,9 +502,17 @@ export default class ServerStatsProvider {
415
502
  }, 200);
416
503
  });
417
504
  }
418
- // Full-page dashboard setup (routes already registered in boot)
419
- if (toolbarConfig.dashboard) {
420
- await this.setupDashboard(toolbarConfig, emitter);
505
+ // Full-page dashboard setup deferred with setImmediate so it runs
506
+ // AFTER the current event-loop cycle completes. This guarantees
507
+ // ready() returns and AdonisJS can process HTTP requests while the
508
+ // SQLite store initializes in the background.
509
+ if (toolbarConfig.dashboard && this.dashboardDepsAvailable) {
510
+ setImmediate(() => {
511
+ this.setupDashboard(toolbarConfig, emitter).catch((err) => {
512
+ log.warn(`dashboard setup failed: ${err?.message ?? err}\n` +
513
+ ` ${dim('Everything else continues to work.')}`);
514
+ });
515
+ });
421
516
  }
422
517
  }
423
518
  /**
@@ -428,34 +523,76 @@ export default class ServerStatsProvider {
428
523
  * This method creates the controller so those routes become functional.
429
524
  */
430
525
  async setupDashboard(toolbarConfig, emitter) {
431
- // Create and start the DashboardStore
432
- this.dashboardStore = new DashboardStore(toolbarConfig);
526
+ log.info('dashboard: initializing SQLite store...');
527
+ // Dynamically import DashboardStore so knex/better-sqlite3 are truly optional
528
+ const { DashboardStore: DashboardStoreClass } = await import('../dashboard/dashboard_store.js');
529
+ this.dashboardStore = new DashboardStoreClass(toolbarConfig);
433
530
  const appRoot = this.app.makePath('');
434
531
  try {
435
- await this.dashboardStore.start(null, emitter, appRoot);
532
+ // Timeout safety net: if SQLite init hangs (e.g. wrong native binary
533
+ // loaded via symlink), abort after 15s instead of freezing forever.
534
+ const TIMEOUT_MS = 15_000;
535
+ const startPromise = this.dashboardStore.start(null, emitter, appRoot);
536
+ const timeoutPromise = new Promise((_, reject) => {
537
+ setTimeout(() => reject(new Error(`Dashboard SQLite initialization timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
538
+ });
539
+ await Promise.race([startPromise, timeoutPromise]);
540
+ log.info('dashboard: SQLite store ready');
436
541
  }
437
542
  catch (err) {
438
543
  const msg = err?.message || '';
439
- if (msg.includes('better-sqlite3') || msg.includes('Cannot find module')) {
440
- log.warn('Dashboard requires better-sqlite3. Install it with:\n' +
441
- ' npm install better-sqlite3\n' +
442
- ' Dashboard has been disabled for this session.');
443
- this.dashboardStore = null;
444
- return;
544
+ const code = err?.code || '';
545
+ const isMissingDep = msg.includes('better-sqlite3') ||
546
+ msg.includes('knex') ||
547
+ msg.includes('Cannot find module') ||
548
+ msg.includes('Cannot find package') ||
549
+ code === 'ERR_MODULE_NOT_FOUND' ||
550
+ code === 'MODULE_NOT_FOUND';
551
+ const isTimeout = msg.includes('timed out');
552
+ if (isMissingDep) {
553
+ log.block('Dashboard could not start — missing dependencies. Install with:', [
554
+ '',
555
+ bold('npm install knex better-sqlite3'),
556
+ '',
557
+ dim('Dashboard has been disabled for this session.'),
558
+ dim('Everything else (stats bar, debug panel) works without it.'),
559
+ ]);
560
+ }
561
+ else if (isTimeout) {
562
+ log.block('Dashboard initialization timed out', [
563
+ dim('SQLite setup took too long — this usually means a wrong native'),
564
+ dim('binary was loaded (common with symlinked/file: dependencies).'),
565
+ '',
566
+ dim('Try running:'),
567
+ ` ${bold('npm install knex better-sqlite3')}`,
568
+ dim('in your app directory to ensure the correct copies are used.'),
569
+ '',
570
+ dim('Dashboard has been disabled for this session.'),
571
+ dim('Everything else (stats bar, debug panel) works without it.'),
572
+ ]);
445
573
  }
446
- throw err;
574
+ else {
575
+ log.warn(`Dashboard could not start: ${msg}\n` +
576
+ ` ${dim('Dashboard has been disabled for this session.')}`);
577
+ if (err?.stack) {
578
+ console.error(err.stack);
579
+ }
580
+ }
581
+ this.dashboardStore = null;
582
+ return;
447
583
  }
448
- // Bind to container
449
- ;
584
+ log.info('dashboard: binding to container...');
450
585
  this.app.container.singleton('dashboard.store', () => this.dashboardStore);
451
586
  // Set dashboard path in middleware for self-exclusion
452
587
  setDashboardPath(toolbarConfig.dashboardPath);
453
588
  // Create the controller — this makes the routes registered in boot() functional
589
+ log.info('dashboard: creating controller...');
454
590
  const DashboardControllerClass = (await import('../dashboard/dashboard_controller.js')).default;
455
591
  this.dashboardController = new DashboardControllerClass(this.dashboardStore, this.app);
456
592
  // ── Log piping ────────────────────────────────────────────────
457
593
  // If the log collector is already hooked into Pino (zero-config mode),
458
594
  // piggyback on it instead of creating a separate file poller.
595
+ log.info('dashboard: setting up log piping...');
459
596
  const existingLogStream = getLogStreamService();
460
597
  if (existingLogStream && !existingLogStream['logPath']) {
461
598
  // Stream mode — add a listener for dashboard persistence
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Dynamically import a module, resolving from the app root (`process.cwd()`)
3
+ * instead of from this package's location.
4
+ *
5
+ * This is critical when `adonisjs-server-stats` is symlinked into the app
6
+ * (e.g. via `file:../../adonisjs-server-stats` in package.json). Without
7
+ * this, Node.js dereferences the symlink and resolves bare specifiers from
8
+ * the package's *real* directory tree — which may contain devDependency
9
+ * stubs with different module identity than the app's actual packages.
10
+ *
11
+ * Falls back to a normal `import()` when `createRequire` resolution fails
12
+ * (e.g. when the package is installed normally, not symlinked).
13
+ */
14
+ export declare function appImport<T = unknown>(specifier: string): Promise<T>;
15
+ /**
16
+ * Same as {@link appImport} but also returns the resolved file path.
17
+ * Useful for diagnostic logging.
18
+ */
19
+ export declare function appImportWithPath<T = unknown>(specifier: string): Promise<{
20
+ module: T;
21
+ resolvedPath: string;
22
+ }>;
23
+ //# sourceMappingURL=app_import.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app_import.d.ts","sourceRoot":"","sources":["../../../src/utils/app_import.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAS1E;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EACjD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAW9C"}
@@ -0,0 +1,44 @@
1
+ import { createRequire } from 'node:module';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ /**
5
+ * Dynamically import a module, resolving from the app root (`process.cwd()`)
6
+ * instead of from this package's location.
7
+ *
8
+ * This is critical when `adonisjs-server-stats` is symlinked into the app
9
+ * (e.g. via `file:../../adonisjs-server-stats` in package.json). Without
10
+ * this, Node.js dereferences the symlink and resolves bare specifiers from
11
+ * the package's *real* directory tree — which may contain devDependency
12
+ * stubs with different module identity than the app's actual packages.
13
+ *
14
+ * Falls back to a normal `import()` when `createRequire` resolution fails
15
+ * (e.g. when the package is installed normally, not symlinked).
16
+ */
17
+ export async function appImport(specifier) {
18
+ try {
19
+ const appRequire = createRequire(join(process.cwd(), 'package.json'));
20
+ const resolved = appRequire.resolve(specifier);
21
+ return await import(pathToFileURL(resolved).href);
22
+ }
23
+ catch {
24
+ // Fallback: normal import (works when not symlinked)
25
+ return await import(specifier);
26
+ }
27
+ }
28
+ /**
29
+ * Same as {@link appImport} but also returns the resolved file path.
30
+ * Useful for diagnostic logging.
31
+ */
32
+ export async function appImportWithPath(specifier) {
33
+ try {
34
+ const appRequire = createRequire(join(process.cwd(), 'package.json'));
35
+ const resolved = appRequire.resolve(specifier);
36
+ const module = await import(pathToFileURL(resolved).href);
37
+ return { module: module, resolvedPath: resolved };
38
+ }
39
+ catch {
40
+ // Fallback: normal import (works when not symlinked)
41
+ const module = await import(specifier);
42
+ return { module: module, resolvedPath: `(bare import: ${specifier})` };
43
+ }
44
+ }
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as E, inject as v, ref as h, computed as C, openBlock as a, createElementBlock as l, createElementVNode as e, toDisplayString as c, createCommentVNode as g, createVNode as F, unref as y, Fragment as N, renderList as A, withModifiers as B, createBlock as U } from "vue";
2
- import { u as H } from "./index-COgsk_nv.js";
2
+ import { u as H } from "./index-qCQpBftQ.js";
3
3
  import { u as M } from "./useResizableTable-BoivAevK.js";
4
4
  import { DashboardApi as j, formatCacheSize as q, formatTtl as I } from "adonisjs-server-stats/core";
5
5
  import { u as G } from "./useApiClient-BQQ9CF-q.js";
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as re, inject as m, ref as w, computed as x, openBlock as i, createElementBlock as r, createElementVNode as n, normalizeClass as a, createCommentVNode as b, Fragment as _, toDisplayString as u, unref as c, renderList as A, withModifiers as I, normalizeStyle as R } from "vue";
2
- import { u as ue } from "./index-COgsk_nv.js";
2
+ import { u as ue } from "./index-qCQpBftQ.js";
3
3
  import { isRedactedValue as d, flattenConfig as z, TAB_ICONS as g, countLeaves as ce, collectTopLevelObjectKeys as pe, copyWithFeedback as de, formatFlatValue as ve } from "adonisjs-server-stats/core";
4
4
  const fe = { style: { position: "relative", flex: 1 } }, he = ["value"], ge = ["title", "onClick"], ye = ["viewBox", "innerHTML"], be = ["viewBox", "innerHTML"], $e = ["onClick"], we = { key: 0 }, xe = ["title", "onClick"], ke = ["viewBox", "innerHTML"], Ce = ["viewBox", "innerHTML"], _e = ["onClick"], Se = { key: 0 }, Be = { style: { padding: "4px 16px", fontSize: "10px", color: "var(--ss-muted)" } }, Le = ["onClick"], He = ["title"], Te = ["title"], je = ["title", "onClick"], Me = ["viewBox", "innerHTML"], me = ["viewBox", "innerHTML"], Ae = ["onClick"], Ie = ["title", "onClick"], Ee = ["viewBox", "innerHTML"], Ve = ["viewBox", "innerHTML"], Oe = {
5
5
  key: 1,