adonisjs-server-stats 1.10.0 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +23 -14
  2. package/dist/core/config-utils.d.ts +8 -0
  3. package/dist/core/constants.d.ts +4 -0
  4. package/dist/core/dashboard-data-controller.d.ts +16 -0
  5. package/dist/core/dashboard-data-helpers.d.ts +12 -0
  6. package/dist/core/debug-data-controller.d.ts +4 -0
  7. package/dist/core/define-config-helpers.d.ts +25 -0
  8. package/dist/core/feature-detect-helpers.d.ts +36 -0
  9. package/dist/core/formatters-helpers.d.ts +23 -0
  10. package/dist/core/index.js +594 -509
  11. package/dist/core/log-utils-helpers.d.ts +13 -0
  12. package/dist/core/metrics.d.ts +3 -28
  13. package/dist/core/pagination.d.ts +0 -9
  14. package/dist/core/server-stats-controller.d.ts +6 -0
  15. package/dist/core/transmit-helpers.d.ts +7 -0
  16. package/dist/core/types-dashboard.d.ts +178 -0
  17. package/dist/core/types-diagnostics.d.ts +85 -0
  18. package/dist/core/types.d.ts +10 -442
  19. package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
  20. package/dist/react/CacheTab-2cw_rMzj.js +117 -0
  21. package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
  22. package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
  23. package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
  24. package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
  25. package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
  26. package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
  27. package/dist/react/EventsTab-CfVr7AiM.js +57 -0
  28. package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
  29. package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
  30. package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
  31. package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
  32. package/dist/react/LogsTab-D8unMV5P.js +108 -0
  33. package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
  34. package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
  35. package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
  36. package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
  37. package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
  38. package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
  39. package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
  40. package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
  41. package/dist/react/index-CsImORX6.js +1121 -0
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
  44. package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
  45. package/dist/react/style.css +1 -1
  46. package/dist/src/collectors/app_collector.d.ts +0 -8
  47. package/dist/src/collectors/app_collector.js +45 -52
  48. package/dist/src/collectors/auto_detect.d.ts +0 -23
  49. package/dist/src/collectors/auto_detect.js +33 -55
  50. package/dist/src/collectors/db_pool_collector.d.ts +14 -16
  51. package/dist/src/collectors/db_pool_collector.js +72 -57
  52. package/dist/src/collectors/log_collector.d.ts +0 -47
  53. package/dist/src/collectors/log_collector.js +36 -65
  54. package/dist/src/collectors/queue_collector.d.ts +0 -20
  55. package/dist/src/collectors/queue_collector.js +60 -76
  56. package/dist/src/collectors/redis_collector.d.ts +10 -10
  57. package/dist/src/collectors/redis_collector.js +69 -66
  58. package/dist/src/config/deprecation_migration.d.ts +7 -0
  59. package/dist/src/config/deprecation_migration.js +201 -0
  60. package/dist/src/controller/debug_controller.d.ts +1 -1
  61. package/dist/src/controller/debug_controller.js +87 -81
  62. package/dist/src/dashboard/cache_handlers.d.ts +14 -0
  63. package/dist/src/dashboard/cache_handlers.js +52 -0
  64. package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
  65. package/dist/src/dashboard/chart_aggregator.js +68 -50
  66. package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
  67. package/dist/src/dashboard/coalesce_cache.js +47 -0
  68. package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
  69. package/dist/src/dashboard/dashboard_controller.js +51 -544
  70. package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
  71. package/dist/src/dashboard/dashboard_page_assets.js +51 -0
  72. package/dist/src/dashboard/dashboard_store.d.ts +19 -218
  73. package/dist/src/dashboard/dashboard_store.js +115 -1116
  74. package/dist/src/dashboard/dashboard_types.d.ts +83 -0
  75. package/dist/src/dashboard/dashboard_types.js +4 -0
  76. package/dist/src/dashboard/detail_queries.d.ts +19 -0
  77. package/dist/src/dashboard/detail_queries.js +98 -0
  78. package/dist/src/dashboard/email_event_builder.d.ts +8 -0
  79. package/dist/src/dashboard/email_event_builder.js +65 -0
  80. package/dist/src/dashboard/explain_query.d.ts +8 -0
  81. package/dist/src/dashboard/explain_query.js +22 -0
  82. package/dist/src/dashboard/filter_handlers.d.ts +23 -0
  83. package/dist/src/dashboard/filter_handlers.js +56 -0
  84. package/dist/src/dashboard/filtered_queries.d.ts +15 -0
  85. package/dist/src/dashboard/filtered_queries.js +155 -0
  86. package/dist/src/dashboard/flush_manager.d.ts +25 -0
  87. package/dist/src/dashboard/flush_manager.js +107 -0
  88. package/dist/src/dashboard/format_helpers.d.ts +126 -0
  89. package/dist/src/dashboard/format_helpers.js +140 -0
  90. package/dist/src/dashboard/inspector_manager.d.ts +36 -0
  91. package/dist/src/dashboard/inspector_manager.js +102 -0
  92. package/dist/src/dashboard/integrations/config_inspector.js +11 -13
  93. package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
  94. package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
  95. package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
  96. package/dist/src/dashboard/jobs_handlers.js +61 -0
  97. package/dist/src/dashboard/knex_factory.d.ts +18 -0
  98. package/dist/src/dashboard/knex_factory.js +91 -0
  99. package/dist/src/dashboard/migrator.js +30 -159
  100. package/dist/src/dashboard/migrator_tables.d.ts +19 -0
  101. package/dist/src/dashboard/migrator_tables.js +153 -0
  102. package/dist/src/dashboard/overview_queries.d.ts +66 -0
  103. package/dist/src/dashboard/overview_queries.js +155 -0
  104. package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
  105. package/dist/src/dashboard/overview_query_runners.js +84 -0
  106. package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
  107. package/dist/src/dashboard/overview_store_queries.js +69 -0
  108. package/dist/src/dashboard/paginate_helper.d.ts +12 -0
  109. package/dist/src/dashboard/paginate_helper.js +33 -0
  110. package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
  111. package/dist/src/dashboard/query_explain_handler.js +80 -0
  112. package/dist/src/dashboard/read_queries.d.ts +32 -0
  113. package/dist/src/dashboard/read_queries.js +107 -0
  114. package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
  115. package/dist/src/dashboard/saved_filter_queries.js +24 -0
  116. package/dist/src/dashboard/storage_stats.d.ts +41 -0
  117. package/dist/src/dashboard/storage_stats.js +81 -0
  118. package/dist/src/dashboard/write_queue.d.ts +106 -0
  119. package/dist/src/dashboard/write_queue.js +225 -0
  120. package/dist/src/data/data_access.d.ts +2 -39
  121. package/dist/src/data/data_access.js +17 -193
  122. package/dist/src/data/data_access_helpers.d.ts +130 -0
  123. package/dist/src/data/data_access_helpers.js +212 -0
  124. package/dist/src/debug/debug_store.js +37 -32
  125. package/dist/src/debug/email_collector.d.ts +1 -10
  126. package/dist/src/debug/email_collector.js +78 -81
  127. package/dist/src/debug/event_collector.d.ts +0 -9
  128. package/dist/src/debug/event_collector.js +79 -62
  129. package/dist/src/debug/query_collector.js +23 -19
  130. package/dist/src/debug/route_inspector.d.ts +1 -5
  131. package/dist/src/debug/route_inspector.js +50 -51
  132. package/dist/src/debug/trace_collector.d.ts +9 -1
  133. package/dist/src/debug/trace_collector.js +21 -15
  134. package/dist/src/debug/types.d.ts +1 -1
  135. package/dist/src/define_config.d.ts +0 -65
  136. package/dist/src/define_config.js +93 -333
  137. package/dist/src/edge/client/dashboard.js +2 -2
  138. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  139. package/dist/src/edge/client/stats-bar.js +1 -1
  140. package/dist/src/edge/client-vue/dashboard.js +5 -5
  141. package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
  142. package/dist/src/edge/client-vue/stats-bar.js +3 -3
  143. package/dist/src/edge/plugin.d.ts +0 -16
  144. package/dist/src/edge/plugin.js +57 -64
  145. package/dist/src/engine/request_metrics.d.ts +1 -0
  146. package/dist/src/engine/request_metrics.js +32 -42
  147. package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
  148. package/dist/src/middleware/request_tracking_middleware.js +65 -93
  149. package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
  150. package/dist/src/provider/auth_middleware_detector.js +97 -0
  151. package/dist/src/provider/boot_helpers.d.ts +20 -0
  152. package/dist/src/provider/boot_helpers.js +91 -0
  153. package/dist/src/provider/boot_initializer.d.ts +28 -0
  154. package/dist/src/provider/boot_initializer.js +35 -0
  155. package/dist/src/provider/dashboard_init.d.ts +30 -0
  156. package/dist/src/provider/dashboard_init.js +138 -0
  157. package/dist/src/provider/dashboard_setup.d.ts +25 -0
  158. package/dist/src/provider/dashboard_setup.js +78 -0
  159. package/dist/src/provider/diagnostics.d.ts +134 -0
  160. package/dist/src/provider/diagnostics.js +127 -0
  161. package/dist/src/provider/email_bridge.d.ts +43 -0
  162. package/dist/src/provider/email_bridge.js +80 -0
  163. package/dist/src/provider/email_helpers.d.ts +13 -0
  164. package/dist/src/provider/email_helpers.js +68 -0
  165. package/dist/src/provider/pino_hook.d.ts +17 -0
  166. package/dist/src/provider/pino_hook.js +35 -0
  167. package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
  168. package/dist/src/provider/provider_helpers_extra.js +177 -0
  169. package/dist/src/provider/server_stats_provider.d.ts +39 -85
  170. package/dist/src/provider/server_stats_provider.js +132 -951
  171. package/dist/src/provider/shutdown_helpers.d.ts +43 -0
  172. package/dist/src/provider/shutdown_helpers.js +70 -0
  173. package/dist/src/provider/toolbar_setup.d.ts +57 -0
  174. package/dist/src/provider/toolbar_setup.js +141 -0
  175. package/dist/src/routes/dashboard_routes.d.ts +14 -0
  176. package/dist/src/routes/dashboard_routes.js +197 -0
  177. package/dist/src/routes/debug_routes.d.ts +14 -0
  178. package/dist/src/routes/debug_routes.js +101 -0
  179. package/dist/src/routes/register_routes.d.ts +0 -78
  180. package/dist/src/routes/register_routes.js +22 -352
  181. package/dist/src/routes/stats_routes.d.ts +5 -0
  182. package/dist/src/routes/stats_routes.js +14 -0
  183. package/dist/src/styles/components.css +96 -0
  184. package/dist/src/styles/dashboard.css +8 -90
  185. package/dist/src/styles/debug-panel.css +1 -31
  186. package/dist/src/types.d.ts +305 -14
  187. package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
  188. package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
  189. package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
  190. package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
  191. package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
  192. package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
  193. package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
  194. package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
  195. package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
  196. package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
  197. package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
  198. package/dist/vue/composables/useDashboardData.d.ts +12 -23
  199. package/dist/vue/index-C8MxnS7Q.js +1232 -0
  200. package/dist/vue/index.js +1 -1
  201. package/dist/vue/style.css +1 -1
  202. package/package.json +1 -1
  203. package/dist/react/CacheTab-CA8LB1J5.js +0 -123
  204. package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
  205. package/dist/react/EmailsTab-BDhEiomM.js +0 -153
  206. package/dist/react/EventsTab-CMfY98Rl.js +0 -63
  207. package/dist/react/LogsTab-CicucmVk.js +0 -103
  208. package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
  209. package/dist/react/index-Cflz9Ebj.js +0 -1069
  210. package/dist/vue/index-Dtgysd26.js +0 -1229
@@ -1,126 +1,62 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
5
1
  import { log } from '../utils/logger.js';
6
- import { round, clamp } from '../utils/math_helpers.js';
7
- import { loadTransmitClient } from '../utils/transmit_client.js';
8
- import { CacheInspector } from './integrations/cache_inspector.js';
2
+ import { clamp } from '../utils/math_helpers.js';
3
+ import { handleCacheStats, handleCacheKey, handleCacheKeyDelete } from './cache_handlers.js';
4
+ import { DashboardPageAssets } from './dashboard_page_assets.js';
5
+ import { handleSavedFilters, handleCreateSavedFilter, handleDeleteSavedFilter, } from './filter_handlers.js';
6
+ import { paginatedResponse, emptyPage, emptyOverview, formatRequest, formatQuery, formatTrace, formatLog, mapChartBucket, buildSparklines, formatGroupedQuery, } from './format_helpers.js';
7
+ import { InspectorManager } from './inspector_manager.js';
9
8
  import { ConfigInspector } from './integrations/config_inspector.js';
10
- import { QueueInspector } from './integrations/queue_inspector.js';
9
+ import { handleJobs, handleJobDetail, handleJobRetry } from './jobs_handlers.js';
10
+ import { handleQueryExplain } from './query_explain_handler.js';
11
11
  const warnedDbReads = new Set();
12
- const SRC_DIR = dirname(fileURLToPath(import.meta.url));
13
- const EDGE_DIR = join(SRC_DIR, '..', 'edge');
14
- const STYLES_DIR = join(SRC_DIR, '..', 'styles');
15
- function paginatedResponse(data, total, page, perPage) {
16
- return {
17
- data,
18
- meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
19
- };
20
- }
21
- function emptyPage(page, perPage) {
22
- return { data: [], meta: { total: 0, page, perPage, lastPage: 1 } };
23
- }
24
- /**
25
- * Controller for the full-page dashboard.
26
- *
27
- * Serves the dashboard HTML page and dashboard-specific JSON API
28
- * endpoints (overview, requests, grouped queries, query explain,
29
- * cache, jobs, config, saved filters).
30
- *
31
- * Data resource endpoints (queries, events, emails, traces, routes,
32
- * logs) are handled by the unified ApiController — see
33
- * `src/controller/api_controller.ts`.
34
- */
35
12
  export default class DashboardController {
36
13
  dashboardStore;
37
14
  app;
38
- cacheInspector = null;
39
- queueInspector = null;
40
15
  configInspector;
41
- cacheAvailable = null;
42
- queueAvailable = null;
43
- cachedCss = null;
44
- cachedJs = null;
45
- cachedTransmitClient = null;
16
+ inspectors;
17
+ pageAssets;
46
18
  constructor(dashboardStore, app) {
47
19
  this.dashboardStore = dashboardStore;
48
20
  this.app = app;
49
21
  this.configInspector = new ConfigInspector(app);
22
+ this.inspectors = new InspectorManager(app);
23
+ this.pageAssets = new DashboardPageAssets();
50
24
  }
51
- // ---------------------------------------------------------------------------
52
- // Page
53
- // ---------------------------------------------------------------------------
54
25
  async page(ctx) {
55
- if (!this.checkAccess(ctx)) {
26
+ if (!this.checkAccess(ctx))
56
27
  return ctx.response.forbidden({ error: 'Access denied' });
57
- }
58
28
  const config = this.app.config.get('server_stats');
59
- const toolbarConfig = config?.devToolbar ?? {};
60
- if (!this.cachedCss) {
61
- const tokens = readFileSync(join(STYLES_DIR, 'tokens.css'), 'utf-8');
62
- const components = readFileSync(join(STYLES_DIR, 'components.css'), 'utf-8');
63
- const utilities = readFileSync(join(STYLES_DIR, 'utilities.css'), 'utf-8');
64
- const dashboard = readFileSync(join(STYLES_DIR, 'dashboard.css'), 'utf-8');
65
- this.cachedCss = tokens + '\n' + components + '\n' + utilities + '\n' + dashboard;
66
- }
67
- if (!this.cachedJs) {
68
- const renderer = toolbarConfig.renderer || 'preact';
69
- const clientDir = renderer === 'vue' ? 'client-vue' : 'client';
70
- this.cachedJs = readFileSync(join(EDGE_DIR, clientDir, 'dashboard.js'), 'utf-8');
71
- }
72
- if (this.cachedTransmitClient === null) {
73
- this.cachedTransmitClient = loadTransmitClient(this.app.makePath('package.json'));
74
- if (this.cachedTransmitClient) {
75
- log.info('Transmit client loaded for dashboard');
76
- }
77
- else {
78
- log.info('Dashboard will use polling. Install @adonisjs/transmit-client for real-time updates.');
79
- }
80
- }
81
- const dashPath = this.getDashboardPath();
29
+ const tc = config?.devToolbar ?? {};
30
+ const renderer = tc.renderer || 'preact';
31
+ const dp = this.getDashboardPath();
82
32
  return ctx.view.render('ss::dashboard', {
83
- css: this.cachedCss,
84
- js: this.cachedJs,
85
- transmitClient: this.cachedTransmitClient,
33
+ css: this.pageAssets.getCss(),
34
+ js: this.pageAssets.getJs(renderer),
35
+ transmitClient: this.pageAssets.getTransmitClient(this.app.makePath('package.json')),
86
36
  dashConfig: {
87
37
  baseUrl: '',
88
- dashboardEndpoint: dashPath + '/api',
89
- debugEndpoint: toolbarConfig.debugEndpoint || '/admin/api/debug',
38
+ dashboardEndpoint: dp + '/api',
39
+ debugEndpoint: tc.debugEndpoint || '/admin/api/debug',
90
40
  channelName: 'server-stats/dashboard',
91
41
  backUrl: '/',
92
42
  },
93
43
  });
94
44
  }
95
- // ---------------------------------------------------------------------------
96
- // Overview
97
- // ---------------------------------------------------------------------------
98
45
  async overview({ request, response }) {
99
46
  return this.withDb(response, 'overview', emptyOverview(), async () => {
100
47
  const range = request.qs().range || '1h';
101
- // Sequential awaits — with a single-connection SQLite pool, Promise.all
102
- // creates concurrent pendingAcquires that thrash tarn's scheduler.
103
48
  const overview = await this.dashboardStore.getOverviewMetrics(range);
104
49
  if (!overview)
105
50
  return emptyOverview();
106
51
  const widgets = await this.dashboardStore.getOverviewWidgets(range);
107
52
  const sparklineData = await this.dashboardStore.getSparklineData(range);
108
- // Cache and queue inspectors use Redis/BullMQ (not SQLite), so
109
- // Promise.all is fine here — different connection pools.
110
53
  const [cacheStats, jobQueueStatus] = await Promise.all([
111
- this.fetchCacheOverview(),
112
- this.fetchQueueOverview(),
54
+ this.inspectors.fetchCacheOverview(),
55
+ this.inspectors.fetchQueueOverview(),
113
56
  ]);
114
57
  return {
115
58
  ...overview,
116
- sparklines: {
117
- avgResponseTime: sparklineData.map((m) => m.avg_duration),
118
- p95ResponseTime: sparklineData.map((m) => m.p95_duration),
119
- requestsPerMinute: sparklineData.map((m) => m.request_count),
120
- errorRate: sparklineData.map((m) => m.request_count > 0
121
- ? round((m.error_count / m.request_count) * 100)
122
- : 0),
123
- },
59
+ sparklines: buildSparklines(sparklineData),
124
60
  ...widgets,
125
61
  cacheStats,
126
62
  jobQueueStatus,
@@ -131,22 +67,9 @@ export default class DashboardController {
131
67
  const range = request.qs().range || '1h';
132
68
  return this.withDb(response, 'overviewChart', { range, buckets: [] }, async () => {
133
69
  const buckets = await this.dashboardStore.getChartData(range);
134
- return {
135
- range,
136
- buckets: buckets.map((b) => ({
137
- bucket: b.bucket,
138
- requestCount: b.request_count,
139
- avgDuration: b.avg_duration,
140
- p95Duration: b.p95_duration,
141
- errorCount: b.error_count,
142
- queryCount: b.query_count,
143
- })),
144
- };
70
+ return { range, buckets: buckets.map(mapChartBucket) };
145
71
  });
146
72
  }
147
- // ---------------------------------------------------------------------------
148
- // Requests
149
- // ---------------------------------------------------------------------------
150
73
  async requests({ request, response }) {
151
74
  const qs = request.qs();
152
75
  const page = Math.max(1, Number(qs.page) || 1);
@@ -162,9 +85,8 @@ export default class DashboardController {
162
85
  });
163
86
  }
164
87
  async requestDetail({ params, response }) {
165
- if (!this.dashboardStore.isReady()) {
88
+ if (!this.dashboardStore.isReady())
166
89
  return response.notFound({ error: 'Not found' });
167
- }
168
90
  try {
169
91
  const detail = await this.dashboardStore.getRequestDetail(Number(params.id));
170
92
  if (!detail)
@@ -184,301 +106,49 @@ export default class DashboardController {
184
106
  return this.withDb(response, 'queriesGrouped', { groups: [] }, async () => {
185
107
  const qs = request.qs();
186
108
  const limit = clamp(Number(qs.limit) || 50, 1, 200);
187
- const sort = qs.sort || 'total_duration';
188
- const search = qs.search || undefined;
189
- const groups = await this.dashboardStore.getQueriesGrouped(limit, sort, search);
109
+ const groups = await this.dashboardStore.getQueriesGrouped(limit, qs.sort || 'total_duration', qs.search || undefined);
190
110
  const totalTime = groups.reduce((sum, g) => sum + (g.total_duration || 0), 0);
191
111
  return {
192
- groups: groups.map((g) => ({
193
- sqlNormalized: g.sql_normalized,
194
- count: g.count,
195
- avgDuration: round(g.avg_duration),
196
- minDuration: round(g.min_duration),
197
- maxDuration: round(g.max_duration),
198
- totalDuration: round(g.total_duration),
199
- percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
200
- })),
112
+ groups: groups.map((g) => formatGroupedQuery(g, totalTime)),
201
113
  };
202
114
  });
203
115
  }
204
- async queryExplain({ params, response }) {
205
- if (!this.dashboardStore.isReady()) {
206
- return response.notFound({ error: 'Not found' });
207
- }
208
- try {
209
- const db = this.dashboardStore.getDb();
210
- if (!db)
211
- return response.notFound({ error: 'Not found' });
212
- const id = Number(params.id);
213
- const query = await db('server_stats_queries')
214
- .where('id', id)
215
- .first();
216
- if (!query)
217
- return response.notFound({ error: 'Query not found' });
218
- const sqlTrimmed = query.sql_text.trim().toUpperCase();
219
- if (!sqlTrimmed.startsWith('SELECT')) {
220
- return response.badRequest({
221
- error: 'EXPLAIN is only supported for SELECT queries',
222
- });
223
- }
224
- let appDb;
225
- try {
226
- const lucid = await this.app.container.make('lucid.db');
227
- appDb = lucid
228
- .connection()
229
- .getWriteClient();
230
- }
231
- catch {
232
- return response.serviceUnavailable({
233
- error: 'App database connection not available',
234
- });
235
- }
236
- let bindings = [];
237
- if (query.bindings) {
238
- try {
239
- bindings = JSON.parse(query.bindings);
240
- }
241
- catch {
242
- // If bindings can't be parsed, run without them
243
- }
244
- }
245
- const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
246
- let plan = [];
247
- const rawRows = explainResult?.rows ??
248
- (Array.isArray(explainResult) ? explainResult : []);
249
- if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
250
- plan = rawRows[0]['QUERY PLAN'];
251
- }
252
- else {
253
- plan = rawRows;
254
- }
255
- return response.json({ queryId: id, sql: query.sql_text, plan });
256
- }
257
- catch (error) {
258
- return response.internalServerError({
259
- error: 'EXPLAIN failed',
260
- message: error?.message ?? 'Unknown error',
261
- });
262
- }
116
+ async queryExplain(ctx) {
117
+ return handleQueryExplain(this.dashboardStore, this.app, ctx);
263
118
  }
264
- // ---------------------------------------------------------------------------
265
- // Cache
266
- // ---------------------------------------------------------------------------
267
- async cacheStats({ request, response }) {
268
- const inspector = await this.getInspector('cache');
269
- if (!inspector) {
270
- return response.json({ available: false, stats: null, keys: [] });
271
- }
272
- const qs = request.qs();
273
- const searchTerm = qs.search || qs.pattern || '';
274
- const pattern = searchTerm ? `*${searchTerm}*` : '*';
275
- const cursor = qs.cursor || '0';
276
- const count = clamp(Number(qs.count) || 100, 1, 500);
277
- try {
278
- const [stats, keyList] = await Promise.all([
279
- inspector.getStats(),
280
- inspector.listKeys(pattern, cursor, count),
281
- ]);
282
- return response.json({
283
- available: true,
284
- stats,
285
- keys: keyList.keys,
286
- cursor: keyList.cursor,
287
- });
288
- }
289
- catch {
290
- return response.json({ available: false, stats: null, keys: [] });
291
- }
119
+ async cacheStats(ctx) {
120
+ return handleCacheStats(this.inspectors, ctx);
292
121
  }
293
- async cacheKey({ params, response }) {
294
- const inspector = await this.getInspector('cache');
295
- if (!inspector) {
296
- return response.notFound({ error: 'Cache not available' });
297
- }
298
- try {
299
- const key = decodeURIComponent(params.key);
300
- const detail = await inspector.getKey(key);
301
- if (!detail)
302
- return response.notFound({ error: 'Key not found' });
303
- return response.json(detail);
304
- }
305
- catch {
306
- return response.notFound({ error: 'Key not found' });
307
- }
122
+ async cacheKey(ctx) {
123
+ return handleCacheKey(this.inspectors, ctx);
308
124
  }
309
- async cacheKeyDelete({ params, response }) {
310
- const inspector = await this.getInspector('cache');
311
- if (!inspector) {
312
- return response.notFound({ error: 'Cache not available' });
313
- }
314
- try {
315
- const key = decodeURIComponent(params.key);
316
- const deleted = await inspector.deleteKey(key);
317
- if (!deleted)
318
- return response.notFound({ error: 'Key not found' });
319
- return response.json({ deleted: true });
320
- }
321
- catch {
322
- return response.internalServerError({ error: 'Failed to delete cache key' });
323
- }
125
+ async cacheKeyDelete(ctx) {
126
+ return handleCacheKeyDelete(this.inspectors, ctx);
324
127
  }
325
- // ---------------------------------------------------------------------------
326
- // Jobs / Queue
327
- // ---------------------------------------------------------------------------
328
- async jobs({ request, response }) {
329
- const inspector = await this.getInspector('queue');
330
- if (!inspector) {
331
- return response.json({
332
- available: false,
333
- overview: null,
334
- stats: null,
335
- jobs: [],
336
- total: 0,
337
- });
338
- }
339
- const qs = request.qs();
340
- const status = qs.status || 'all';
341
- const searchTerm = qs.search || '';
342
- const page = Math.max(1, Number(qs.page) || 1);
343
- const perPage = clamp(Number(qs.perPage) || Number(qs.limit) || 25, 1, 100);
344
- try {
345
- const [overview, jobList] = await Promise.all([
346
- inspector.getOverview(),
347
- inspector.listJobs(status, page, perPage),
348
- ]);
349
- // Filter jobs by search term (name match) if provided
350
- let filteredJobs = jobList.jobs;
351
- let filteredTotal = jobList.total;
352
- if (searchTerm) {
353
- const term = searchTerm.toLowerCase();
354
- filteredJobs = jobList.jobs.filter((j) => j.name?.toLowerCase().includes(term) || j.id?.toString().toLowerCase().includes(term));
355
- filteredTotal = filteredJobs.length;
356
- }
357
- return response.json({
358
- available: true,
359
- overview,
360
- stats: overview,
361
- jobs: filteredJobs,
362
- total: filteredTotal,
363
- page,
364
- perPage,
365
- });
366
- }
367
- catch {
368
- return response.json({
369
- available: false,
370
- overview: null,
371
- stats: null,
372
- jobs: [],
373
- total: 0,
374
- });
375
- }
128
+ async jobs(ctx) {
129
+ return handleJobs(this.inspectors, ctx);
376
130
  }
377
- async jobDetail({ params, response }) {
378
- const inspector = await this.getInspector('queue');
379
- if (!inspector) {
380
- return response.notFound({ error: 'Queue not available' });
381
- }
382
- try {
383
- const detail = await inspector.getJob(String(params.id));
384
- if (!detail)
385
- return response.notFound({ error: 'Job not found' });
386
- return response.json(detail);
387
- }
388
- catch {
389
- return response.notFound({ error: 'Job not found' });
390
- }
131
+ async jobDetail(ctx) {
132
+ return handleJobDetail(this.inspectors, ctx);
391
133
  }
392
- async jobRetry({ params, response }) {
393
- const inspector = await this.getInspector('queue');
394
- if (!inspector) {
395
- return response.notFound({ error: 'Queue not available' });
396
- }
397
- try {
398
- const success = await inspector.retryJob(String(params.id));
399
- if (!success) {
400
- return response.badRequest({
401
- error: 'Job could not be retried (not in failed state)',
402
- });
403
- }
404
- return response.json({ success: true });
405
- }
406
- catch {
407
- return response.internalServerError({ error: 'Retry failed' });
408
- }
134
+ async jobRetry(ctx) {
135
+ return handleJobRetry(this.inspectors, ctx);
409
136
  }
410
- // ---------------------------------------------------------------------------
411
- // Config
412
- // ---------------------------------------------------------------------------
413
137
  async config({ response }) {
414
- const configData = this.configInspector.getConfig();
415
- const envData = this.configInspector.getEnvVars();
416
138
  return response.json({
417
- app: configData.config,
418
- env: envData.env,
139
+ app: this.configInspector.getConfig().config,
140
+ env: this.configInspector.getEnvVars().env,
419
141
  });
420
142
  }
421
- // ---------------------------------------------------------------------------
422
- // Saved Filters
423
- // ---------------------------------------------------------------------------
424
143
  async savedFilters({ response }) {
425
- return this.withDb(response, 'savedFilters', { filters: [] }, async () => {
426
- const filters = await this.dashboardStore.getSavedFilters();
427
- return {
428
- filters: filters.map((f) => ({
429
- id: f.id,
430
- name: f.name,
431
- section: f.section,
432
- filterConfig: safeParseJson(f.filter_config),
433
- createdAt: f.created_at,
434
- })),
435
- };
436
- });
144
+ return this.withDb(response, 'savedFilters', { filters: [] }, () => handleSavedFilters(this.dashboardStore));
437
145
  }
438
- async createSavedFilter({ request, response }) {
439
- if (!this.dashboardStore.isReady()) {
440
- return response.serviceUnavailable({ error: 'Database not available' });
441
- }
442
- try {
443
- const body = request.body();
444
- const { name, section, filterConfig } = body;
445
- if (!name || !section || !filterConfig) {
446
- return response.badRequest({
447
- error: 'Missing required fields: name, section, filterConfig',
448
- });
449
- }
450
- const configObj = typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig;
451
- const result = await this.dashboardStore.createSavedFilter(name, section, configObj);
452
- if (!result) {
453
- return response.serviceUnavailable({ error: 'Database not available' });
454
- }
455
- return response.json(result);
456
- }
457
- catch {
458
- return response.internalServerError({ error: 'Failed to create filter' });
459
- }
146
+ async createSavedFilter(ctx) {
147
+ return handleCreateSavedFilter(this.dashboardStore, ctx);
460
148
  }
461
- async deleteSavedFilter({ params, response }) {
462
- if (!this.dashboardStore.isReady()) {
463
- return response.serviceUnavailable({ error: 'Database not available' });
464
- }
465
- try {
466
- const deleted = await this.dashboardStore.deleteSavedFilter(Number(params.id));
467
- if (!deleted)
468
- return response.notFound({ error: 'Filter not found' });
469
- return response.json({ success: true });
470
- }
471
- catch {
472
- return response.internalServerError({ error: 'Failed to delete filter' });
473
- }
149
+ async deleteSavedFilter(ctx) {
150
+ return handleDeleteSavedFilter(this.dashboardStore, ctx);
474
151
  }
475
- // ---------------------------------------------------------------------------
476
- // Private helpers
477
- // ---------------------------------------------------------------------------
478
- /**
479
- * Wraps a store call with null-guard + try/catch boilerplate.
480
- * Returns emptyValue if the store is not ready or the fn throws.
481
- */
482
152
  async withDb(response, label, emptyValue, fn) {
483
153
  if (!this.dashboardStore.isReady())
484
154
  return response.json(emptyValue);
@@ -505,170 +175,7 @@ export default class DashboardController {
505
175
  }
506
176
  }
507
177
  getDashboardPath() {
508
- const config = this.app.config.get('server_stats');
509
- return config?.devToolbar?.dashboardPath ?? '/__stats';
510
- }
511
- async getInspector(type) {
512
- if (type === 'cache') {
513
- if (this.cacheAvailable === false)
514
- return null;
515
- if (this.cacheInspector)
516
- return this.cacheInspector;
517
- try {
518
- const available = await CacheInspector.isAvailable(this.app);
519
- this.cacheAvailable = available;
520
- if (!available) {
521
- log.info('dashboard: Redis not detected — Cache panel disabled');
522
- return null;
523
- }
524
- const redis = await this.app.container.make('redis');
525
- this.cacheInspector = new CacheInspector(redis);
526
- return this.cacheInspector;
527
- }
528
- catch (err) {
529
- this.cacheAvailable = false;
530
- log.warn('dashboard: CacheInspector init failed — ' + err?.message);
531
- return null;
532
- }
533
- }
534
- else {
535
- if (this.queueAvailable === false)
536
- return null;
537
- if (this.queueInspector)
538
- return this.queueInspector;
539
- try {
540
- const available = await QueueInspector.isAvailable(this.app);
541
- this.queueAvailable = available;
542
- if (!available) {
543
- log.info('dashboard: Queue not detected — Jobs panel disabled');
544
- return null;
545
- }
546
- const queue = await this.app.container.make('rlanz/queue');
547
- this.queueInspector = new QueueInspector(queue);
548
- return this.queueInspector;
549
- }
550
- catch (err) {
551
- this.queueAvailable = false;
552
- log.warn('dashboard: QueueInspector init failed — ' + err?.message);
553
- return null;
554
- }
555
- }
556
- }
557
- /** Fetch cache overview stats for the overview page. */
558
- async fetchCacheOverview() {
559
- try {
560
- const inspector = await this.getInspector('cache');
561
- if (!inspector)
562
- return null;
563
- const stats = await inspector.getStats();
564
- return {
565
- available: true,
566
- totalKeys: stats.totalKeys,
567
- hitRate: stats.hitRate,
568
- memoryUsedHuman: stats.memoryUsedHuman,
569
- };
570
- }
571
- catch {
572
- return null;
573
- }
574
- }
575
- /** Fetch queue overview stats for the overview page. */
576
- async fetchQueueOverview() {
577
- try {
578
- const inspector = await this.getInspector('queue');
579
- if (!inspector)
580
- return null;
581
- const overview = await inspector.getOverview();
582
- return {
583
- available: true,
584
- active: overview.active,
585
- waiting: overview.waiting,
586
- failed: overview.failed,
587
- completed: overview.completed,
588
- };
589
- }
590
- catch {
591
- return null;
592
- }
178
+ return (this.app.config.get('server_stats')?.devToolbar?.dashboardPath ??
179
+ '/__stats');
593
180
  }
594
181
  }
595
- // ---------------------------------------------------------------------------
596
- // Formatting helpers (snake_case DB rows → camelCase API response)
597
- // ---------------------------------------------------------------------------
598
- function formatRequest(r) {
599
- return {
600
- id: r.id,
601
- method: r.method,
602
- url: r.url,
603
- statusCode: r.status_code,
604
- duration: r.duration,
605
- spanCount: r.span_count,
606
- warningCount: r.warning_count,
607
- createdAt: r.created_at,
608
- ...(r.http_request_id ? { httpRequestId: r.http_request_id } : {}),
609
- };
610
- }
611
- function formatQuery(q) {
612
- return {
613
- id: q.id,
614
- requestId: q.request_id,
615
- sql: q.sql_text,
616
- sqlNormalized: q.sql_normalized,
617
- bindings: safeParseJson(q.bindings),
618
- duration: q.duration,
619
- method: q.method,
620
- model: q.model,
621
- connection: q.connection,
622
- inTransaction: !!q.in_transaction,
623
- createdAt: q.created_at,
624
- };
625
- }
626
- function formatTrace(t) {
627
- return {
628
- id: t.id,
629
- requestId: t.request_id,
630
- method: t.method,
631
- url: t.url,
632
- statusCode: t.status_code,
633
- totalDuration: t.total_duration,
634
- spanCount: t.span_count,
635
- spans: safeParseJson(t.spans) ?? [],
636
- warnings: safeParseJsonArray(t.warnings),
637
- createdAt: t.created_at,
638
- ...(t.http_request_id ? { httpRequestId: t.http_request_id } : {}),
639
- };
640
- }
641
- function formatLog(l) {
642
- return {
643
- id: l.id,
644
- level: l.level,
645
- message: l.message,
646
- requestId: l.request_id,
647
- data: l.data,
648
- createdAt: l.created_at,
649
- };
650
- }
651
- function emptyOverview() {
652
- return {
653
- avgResponseTime: 0,
654
- p95ResponseTime: 0,
655
- requestsPerMinute: 0,
656
- errorRate: 0,
657
- sparklines: {
658
- avgResponseTime: [],
659
- p95ResponseTime: [],
660
- requestsPerMinute: [],
661
- errorRate: [],
662
- },
663
- slowestEndpoints: [],
664
- queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
665
- recentErrors: [],
666
- topEvents: [],
667
- emailActivity: { sent: 0, queued: 0, failed: 0 },
668
- logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
669
- statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
670
- slowestQueries: [],
671
- cacheStats: null,
672
- jobQueueStatus: null,
673
- };
674
- }