adonisjs-server-stats 1.11.6 → 1.12.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 (82) hide show
  1. package/README.md +13 -0
  2. package/dist/core/explain-utils.d.ts +77 -0
  3. package/dist/core/index.d.ts +8 -2
  4. package/dist/core/index.js +1089 -877
  5. package/dist/core/queries-columns.d.ts +42 -0
  6. package/dist/core/queries-controller.d.ts +94 -0
  7. package/dist/core/query-utils.d.ts +49 -0
  8. package/dist/core/types-diagnostics.d.ts +2 -0
  9. package/dist/react/{CacheSection-BYN53kYO.js → CacheSection-DZFOiOYH.js} +1 -1
  10. package/dist/react/{CacheTab-DOhuK05d.js → CacheTab-D8js5o69.js} +1 -1
  11. package/dist/react/{ConfigSection-B9EHh4Rp.js → ConfigSection-DoJmvNkF.js} +1 -1
  12. package/dist/react/{ConfigTab-C8kriE2b.js → ConfigTab-CDvljuBF.js} +1 -1
  13. package/dist/react/{CustomPaneTab-CvzQS_Wh.js → CustomPaneTab-Db9eIQ-9.js} +1 -1
  14. package/dist/react/{EmailsSection-BJyFJf7A.js → EmailsSection-D7oxvAjA.js} +1 -1
  15. package/dist/react/{EmailsTab-Ch8jp10B.js → EmailsTab-BITuGTQm.js} +1 -1
  16. package/dist/react/{EventsSection-DJPwHeT8.js → EventsSection-7mDZv70I.js} +1 -1
  17. package/dist/react/{EventsTab-B-FoehXC.js → EventsTab-DF13BChE.js} +1 -1
  18. package/dist/react/InternalsContent-BNOnSoi9.js +393 -0
  19. package/dist/react/{InternalsSection-B6VlVx5f.js → InternalsSection-BwrTfpjA.js} +1 -1
  20. package/dist/react/{InternalsTab-CkEKpRMU.js → InternalsTab--RD-L1dX.js} +1 -1
  21. package/dist/react/{JobsSection-DWF4i1t_.js → JobsSection-B_VJFWy6.js} +1 -1
  22. package/dist/react/{JobsTab-DqnifQXV.js → JobsTab-DK8Fl9nz.js} +1 -1
  23. package/dist/react/{LogEntryRow-CMMkqA9M.js → LogEntryRow-Bdie9SFe.js} +1 -1
  24. package/dist/react/{LogsSection-C1xC5aP4.js → LogsSection-CrWqxWRr.js} +2 -2
  25. package/dist/react/{LogsTab-CS4sLfLw.js → LogsTab-DYSpL637.js} +2 -2
  26. package/dist/react/{OverviewSection-CxvfOR0v.js → OverviewSection-CuaccMZt.js} +1 -1
  27. package/dist/react/QueriesContent-BDIx8zFn.js +288 -0
  28. package/dist/react/QueriesSection-CvGGh5Ij.js +117 -0
  29. package/dist/react/QueriesTab-C7CCwhVc.js +87 -0
  30. package/dist/react/{RequestsSection-DETN9oZb.js → RequestsSection-mENyeNsk.js} +2 -2
  31. package/dist/react/{RoutesSection-CmorkJeC.js → RoutesSection-DjovlaNS.js} +1 -1
  32. package/dist/react/{RoutesTab-CbzBOzpc.js → RoutesTab-CbQECiUc.js} +1 -1
  33. package/dist/react/{SplitPaneWrapper-BiIgT4ND.js → SplitPaneWrapper-DyRyH5Re.js} +1 -1
  34. package/dist/react/{TimelineTab-Ue9tUD_n.js → TimelineTab-BKTFbsPe.js} +2 -2
  35. package/dist/react/components/Dashboard/shared/DataTable.d.ts +1 -1
  36. package/dist/react/components/shared/QueriesContent.d.ts +33 -0
  37. package/dist/react/{index-DwDK-4oX.js → index-CAUHa8QX.js} +2 -2
  38. package/dist/react/index.js +1 -1
  39. package/dist/react/style.css +1 -1
  40. package/dist/src/collectors/db_pool_collector.js +17 -23
  41. package/dist/src/dashboard/query_explain_handler.d.ts +11 -0
  42. package/dist/src/dashboard/query_explain_handler.js +64 -11
  43. package/dist/src/debug/query_collector.d.ts +1 -0
  44. package/dist/src/debug/query_collector.js +3 -0
  45. package/dist/src/edge/client/dashboard.js +2 -2
  46. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  47. package/dist/src/edge/client-vue/dashboard.js +4 -4
  48. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  49. package/dist/src/provider/diagnostics.d.ts +3 -0
  50. package/dist/src/provider/diagnostics.js +1 -0
  51. package/dist/src/provider/server_stats_provider.d.ts +7 -0
  52. package/dist/src/provider/server_stats_provider.js +38 -0
  53. package/dist/src/routes/debug_routes.d.ts +4 -0
  54. package/dist/src/routes/debug_routes.js +45 -0
  55. package/dist/src/routes/register_routes.d.ts +4 -0
  56. package/dist/src/routes/register_routes.js +2 -0
  57. package/dist/src/styles/components.css +254 -0
  58. package/dist/src/styles/dashboard.css +26 -185
  59. package/dist/src/styles/debug-panel.css +2 -24
  60. package/dist/vue/{CacheSection-DT2Mwf_s.js → CacheSection-Bx41lpfK.js} +1 -1
  61. package/dist/vue/{ConfigSection-BwKwS9lh.js → ConfigSection-fkfUdCmx.js} +1 -1
  62. package/dist/vue/{EmailsSection-B65g0FVS.js → EmailsSection-CbcDKF9Q.js} +1 -1
  63. package/dist/vue/{EventsSection-CxqtVF-o.js → EventsSection-CWbTYOBi.js} +1 -1
  64. package/dist/vue/{InternalsSection-BJUXE-5F.js → InternalsSection-jhTCuK3j.js} +149 -130
  65. package/dist/vue/{InternalsTab-DEMjqtlw.js → InternalsTab-521fxYYj.js} +152 -133
  66. package/dist/vue/{JobsSection-rMIyMb-g.js → JobsSection-CGGO6rtS.js} +1 -1
  67. package/dist/vue/{LogsSection-DmmZVJ7D.js → LogsSection-BPB-C-XA.js} +1 -1
  68. package/dist/vue/{OverviewSection-BMabyqw-.js → OverviewSection-BTcnPeoM.js} +1 -1
  69. package/dist/vue/QueriesSection-2SE3igQJ.js +411 -0
  70. package/dist/vue/QueriesTab-DO5XwJ--.js +112 -0
  71. package/dist/vue/{RequestsSection-CTu4jPZ_.js → RequestsSection-DXcG5Zok.js} +1 -1
  72. package/dist/vue/{RoutesSection-zQZDedL7.js → RoutesSection-BkNGWdoP.js} +1 -1
  73. package/dist/vue/components/Dashboard/sections/QueriesSection.vue.d.ts +1 -41
  74. package/dist/vue/{index-CM3yNVUR.js → index-CptF6Qbp.js} +2 -2
  75. package/dist/vue/index.js +1 -1
  76. package/dist/vue/style.css +1 -1
  77. package/package.json +1 -1
  78. package/dist/react/InternalsContent-O8ino9oM.js +0 -370
  79. package/dist/react/QueriesSection-CrMdU5Ax.js +0 -458
  80. package/dist/react/QueriesTab-x85PjkyS.js +0 -88
  81. package/dist/vue/QueriesSection-BfDFwGqH.js +0 -428
  82. package/dist/vue/QueriesTab-DuTG7cpC.js +0 -106
@@ -23,6 +23,8 @@ export interface DiagnosticsInput {
23
23
  hasQueueCollector?: boolean;
24
24
  resolvedCollectors?: CollectorRef[];
25
25
  config: DiagnosticsConfig | null;
26
+ /** Lucid connections that have debug: true (empty = no query events emitted). */
27
+ lucidDebugConnections?: string[];
26
28
  }
27
29
  interface DiagnosticsConfig {
28
30
  intervalMs: number;
@@ -130,5 +132,6 @@ export declare function buildDiagnostics(input: DiagnosticsInput): {
130
132
  slowQueryThresholdMs: number;
131
133
  enabled: boolean;
132
134
  };
135
+ lucidDebugConnections: string[];
133
136
  };
134
137
  export {};
@@ -123,5 +123,6 @@ export function buildDiagnostics(input) {
123
123
  integrations: buildIntegrationsDiagnostics(input),
124
124
  config: buildConfigDiagnostics(input.config),
125
125
  devToolbar: buildDevToolbarDiagnostics(input.config),
126
+ lucidDebugConnections: input.lucidDebugConnections ?? [],
126
127
  };
127
128
  }
@@ -31,6 +31,7 @@ export default class ServerStatsProvider {
31
31
  transmitChannels: string[];
32
32
  private resolvedConfig;
33
33
  private resolvedCollectors;
34
+ private lucidDebugConnections;
34
35
  /** Resolves when initStats completes (or rejects). Null if not yet scheduled. */
35
36
  private initPromise;
36
37
  constructor(app: ApplicationService);
@@ -49,6 +50,11 @@ export default class ServerStatsProvider {
49
50
  private setupDevToolbar;
50
51
  private setupLogBroadcast;
51
52
  private resolve;
53
+ /**
54
+ * Check Lucid database connections for `debug: true` and warn if missing.
55
+ * Without it, Lucid won't emit `db:query` events and no queries will be captured.
56
+ */
57
+ private checkLucidDebugFlag;
52
58
  getDiagnostics(): {
53
59
  timers: {
54
60
  collectionInterval: {
@@ -124,6 +130,7 @@ export default class ServerStatsProvider {
124
130
  slowQueryThresholdMs: number;
125
131
  enabled: boolean;
126
132
  };
133
+ lucidDebugConnections: string[];
127
134
  };
128
135
  shutdown(): Promise<void>;
129
136
  }
@@ -33,6 +33,7 @@ export default class ServerStatsProvider {
33
33
  transmitChannels = [];
34
34
  resolvedConfig = null;
35
35
  resolvedCollectors = [];
36
+ lucidDebugConnections = [];
36
37
  /** Resolves when initStats completes (or rejects). Null if not yet scheduled. */
37
38
  initPromise = null;
38
39
  constructor(app) {
@@ -87,6 +88,8 @@ export default class ServerStatsProvider {
87
88
  getStatsController: () => this.statsController,
88
89
  getDebugController: () => this.debugController,
89
90
  getDashboardController: () => this.dashboardController,
91
+ getDebugStore: () => this.debugStore,
92
+ getApp: () => this.app,
90
93
  statsEndpoint,
91
94
  debugEndpoint,
92
95
  dashboardPath,
@@ -126,6 +129,7 @@ export default class ServerStatsProvider {
126
129
  const SC = (await import('../controller/server_stats_controller.js')).default;
127
130
  this.statsController = new SC(this.engine);
128
131
  if (config.devToolbar?.enabled && !this.app.inProduction) {
132
+ this.checkLucidDebugFlag();
129
133
  await this.setupDevToolbar(config);
130
134
  }
131
135
  const iv = await setupStatsIntervalHelper(this.engine, config, container);
@@ -193,6 +197,39 @@ export default class ServerStatsProvider {
193
197
  return null;
194
198
  }
195
199
  }
200
+ /**
201
+ * Check Lucid database connections for `debug: true` and warn if missing.
202
+ * Without it, Lucid won't emit `db:query` events and no queries will be captured.
203
+ */
204
+ checkLucidDebugFlag() {
205
+ const dbConfig = this.app.config.get('database');
206
+ if (!dbConfig?.connections)
207
+ return;
208
+ const enabled = [];
209
+ const disabled = [];
210
+ for (const [name, conn] of Object.entries(dbConfig.connections)) {
211
+ if (conn?.debug)
212
+ enabled.push(name);
213
+ else
214
+ disabled.push(name);
215
+ }
216
+ this.lucidDebugConnections = enabled;
217
+ if (enabled.length === 0 && disabled.length > 0) {
218
+ log.block('query capture is disabled — no Lucid connections have debug: true', [
219
+ '',
220
+ dim('Lucid only emits db:query events when debug is enabled on a connection.'),
221
+ dim('Add this to your database connection in config/database.ts:'),
222
+ '',
223
+ ` ${disabled[0]}: {`,
224
+ ` client: '...',`,
225
+ ` debug: true, ${dim('// ← enables query capture')}`,
226
+ ` connection: { ... },`,
227
+ ` }`,
228
+ '',
229
+ dim(`Connections without debug: ${disabled.join(', ')}`),
230
+ ]);
231
+ }
232
+ }
196
233
  getDiagnostics() {
197
234
  return buildDiagnostics({
198
235
  intervalId: this.intervalId,
@@ -209,6 +246,7 @@ export default class ServerStatsProvider {
209
246
  hasCacheCollector: this.resolvedCollectors.some((c) => c.name === 'redis'),
210
247
  hasQueueCollector: this.resolvedCollectors.some((c) => c.name === 'queue'),
211
248
  config: this.resolvedConfig,
249
+ lucidDebugConnections: this.lucidDebugConnections,
212
250
  });
213
251
  }
214
252
  async shutdown() {
@@ -1,12 +1,16 @@
1
1
  import type { ApiController } from '../controller/api_controller.js';
2
2
  import type DebugController from '../controller/debug_controller.js';
3
+ import type { DebugStore } from '../debug/debug_store.js';
3
4
  import type { AdonisRouter } from './router_types.js';
4
5
  import type { HttpContext } from '@adonisjs/core/http';
6
+ import type { ApplicationService } from '@adonisjs/core/types';
5
7
  interface DebugRoutesOpts {
6
8
  router: AdonisRouter;
7
9
  debugEndpoint: string;
8
10
  getDebugController: () => DebugController | null;
9
11
  getApiController: () => ApiController | null;
12
+ getDebugStore?: () => DebugStore | null;
13
+ getApp?: () => ApplicationService | null;
10
14
  middleware: Array<(ctx: HttpContext, next: () => Promise<void>) => Promise<void>>;
11
15
  whenReady?: () => Promise<void>;
12
16
  }
@@ -1,3 +1,4 @@
1
+ import { getAppDbClient, buildExplainSql, extractPlan } from '../dashboard/query_explain_handler.js';
1
2
  let _whenReady;
2
3
  function bindDebug(getController, method) {
3
4
  return async (ctx) => {
@@ -89,6 +90,49 @@ function registerDebugTraceRoutes(router, getApiController) {
89
90
  }))
90
91
  .as('server-stats.debug.traceDetail');
91
92
  }
93
+ function registerDebugExplainRoute(router, getDebugStore, getApp) {
94
+ if (!getDebugStore || !getApp)
95
+ return;
96
+ router
97
+ .get('/queries/:id/explain', async (ctx) => {
98
+ if (_whenReady)
99
+ await _whenReady();
100
+ const store = getDebugStore();
101
+ const app = getApp();
102
+ if (!store || !app) {
103
+ return ctx.response.serviceUnavailable({ error: 'Debug store not ready' });
104
+ }
105
+ const query = store.queries.getQueryById(Number(ctx.params.id));
106
+ if (!query)
107
+ return ctx.response.notFound({ error: 'Query not found' });
108
+ if (!query.sql.trim().toUpperCase().startsWith('SELECT')) {
109
+ return ctx.response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
110
+ }
111
+ try {
112
+ const appDb = await getAppDbClient(app, query.connection || undefined);
113
+ if (!appDb) {
114
+ return ctx.response.serviceUnavailable({ error: 'App database connection not available' });
115
+ }
116
+ const bindings = Array.isArray(query.bindings) ? query.bindings : [];
117
+ const explainSql = buildExplainSql(query.sql, appDb.dialect);
118
+ if (!explainSql) {
119
+ return ctx.response.badRequest({
120
+ error: `EXPLAIN is not supported for ${appDb.dialect} databases`,
121
+ });
122
+ }
123
+ const explainResult = await appDb.raw(explainSql, bindings);
124
+ const plan = extractPlan(explainResult, appDb.dialect);
125
+ return ctx.response.json({ queryId: query.id, sql: query.sql, plan });
126
+ }
127
+ catch (error) {
128
+ return ctx.response.internalServerError({
129
+ error: 'EXPLAIN failed',
130
+ message: error?.message ?? 'Unknown error',
131
+ });
132
+ }
133
+ })
134
+ .as('server-stats.debug.queryExplain');
135
+ }
92
136
  /** Register debug panel API routes. */
93
137
  export function registerDebugRoutes(opts) {
94
138
  const { router, debugEndpoint, getDebugController, getApiController, middleware } = opts;
@@ -99,6 +143,7 @@ export function registerDebugRoutes(opts) {
99
143
  .group(() => {
100
144
  registerDebugConfigRoutes(router, getDebugController);
101
145
  registerDebugQueryAndEventRoutes(router, getApiController);
146
+ registerDebugExplainRoute(router, opts.getDebugStore, opts.getApp);
102
147
  registerDebugLogRoutes(router, getApiController);
103
148
  registerDebugEmailRoutes(router, getApiController);
104
149
  registerDebugTraceRoutes(router, getApiController);
@@ -1,9 +1,11 @@
1
1
  import type { ApiController } from '../controller/api_controller.js';
2
2
  import type DebugController from '../controller/debug_controller.js';
3
+ import type { DebugStore } from '../debug/debug_store.js';
3
4
  import type ServerStatsController from '../controller/server_stats_controller.js';
4
5
  import type DashboardController from '../dashboard/dashboard_controller.js';
5
6
  import type { AdonisRouter } from './router_types.js';
6
7
  import type { HttpContext } from '@adonisjs/core/http';
8
+ import type { ApplicationService } from '@adonisjs/core/types';
7
9
  /**
8
10
  * Options for the unified route registration function.
9
11
  */
@@ -13,6 +15,8 @@ export interface RegisterRoutesOptions {
13
15
  getStatsController: () => ServerStatsController | null;
14
16
  getDebugController: () => DebugController | null;
15
17
  getDashboardController: () => DashboardController | null;
18
+ getDebugStore?: () => DebugStore | null;
19
+ getApp?: () => ApplicationService | null;
16
20
  statsEndpoint?: string | false;
17
21
  debugEndpoint?: string;
18
22
  dashboardPath?: string;
@@ -16,6 +16,8 @@ export function registerAllRoutes(options) {
16
16
  debugEndpoint: options.debugEndpoint,
17
17
  getDebugController: options.getDebugController,
18
18
  getApiController: options.getApiController,
19
+ getDebugStore: options.getDebugStore,
20
+ getApp: options.getApp,
19
21
  middleware,
20
22
  whenReady: options.whenReady,
21
23
  });
@@ -94,13 +94,29 @@
94
94
  .ss-dash-empty,
95
95
  .ss-dbg-empty {
96
96
  display: flex;
97
+ flex-direction: column;
97
98
  align-items: center;
98
99
  justify-content: center;
100
+ gap: 6px;
99
101
  height: 100%;
100
102
  min-height: 120px;
101
103
  color: var(--ss-dim);
102
104
  font-size: 12px;
103
105
  }
106
+ .ss-empty-hint {
107
+ font-size: 11px;
108
+ color: var(--ss-dim);
109
+ opacity: 0.7;
110
+ max-width: 420px;
111
+ text-align: center;
112
+ line-height: 1.5;
113
+ }
114
+ .ss-empty-hint code {
115
+ background: var(--ss-surface, rgba(255,255,255,0.06));
116
+ padding: 1px 5px;
117
+ border-radius: 3px;
118
+ font-size: 10px;
119
+ }
104
120
 
105
121
  /* ── Info cards ───────────────────────────────────────────────── */
106
122
  .ss-info-cards,
@@ -435,6 +451,7 @@
435
451
  background: var(--ss-surface);
436
452
  border-bottom: 1px solid var(--ss-border);
437
453
  white-space: nowrap;
454
+ z-index: 2;
438
455
  }
439
456
  .ss-table td,
440
457
  .ss-dash-table td,
@@ -446,6 +463,7 @@
446
463
  text-overflow: ellipsis;
447
464
  white-space: nowrap;
448
465
  color: var(--ss-text);
466
+ max-width: 0;
449
467
  }
450
468
  .ss-table tr:hover td,
451
469
  .ss-dash-table tr:hover td,
@@ -517,6 +535,242 @@
517
535
  background: var(--ss-red-bg);
518
536
  color: var(--ss-red-fg);
519
537
  }
538
+ /* SQL method variants (query tables) */
539
+ .ss-method-select,
540
+ .ss-dash-method-select,
541
+ .ss-dbg-method-select {
542
+ background: var(--ss-green-bg);
543
+ color: var(--ss-green-fg);
544
+ }
545
+ .ss-method-insert,
546
+ .ss-dash-method-insert,
547
+ .ss-dbg-method-insert {
548
+ background: var(--ss-blue-bg);
549
+ color: var(--ss-blue-fg);
550
+ }
551
+ .ss-method-update,
552
+ .ss-dash-method-update,
553
+ .ss-dbg-method-update {
554
+ background: var(--ss-amber-bg);
555
+ color: var(--ss-amber-fg);
556
+ }
557
+ .ss-method-head,
558
+ .ss-method-options,
559
+ .ss-method-raw,
560
+ .ss-dash-method-head,
561
+ .ss-dash-method-options,
562
+ .ss-dash-method-raw,
563
+ .ss-dbg-method-head,
564
+ .ss-dbg-method-options,
565
+ .ss-dbg-method-raw {
566
+ background: var(--ss-surface);
567
+ color: var(--ss-dim);
568
+ }
569
+
570
+ /* ── SQL cell text ───────────────────────────────────────────── */
571
+ .ss-sql,
572
+ .ss-dash-sql,
573
+ .ss-dbg-sql {
574
+ font-size: 11px;
575
+ color: var(--ss-sql-color);
576
+ word-break: break-all;
577
+ cursor: pointer;
578
+ max-width: 600px;
579
+ display: inline-block;
580
+ }
581
+ .ss-sql.ss-expanded,
582
+ .ss-dash-sql.ss-dash-expanded,
583
+ .ss-dbg-sql.ss-dbg-expanded {
584
+ white-space: pre-wrap;
585
+ max-width: none;
586
+ }
587
+
588
+ /* ── Duplicate query badge ───────────────────────────────────── */
589
+ .ss-dup,
590
+ .ss-dash-dup,
591
+ .ss-dbg-dup {
592
+ color: var(--ss-purple-fg);
593
+ font-size: 10px;
594
+ font-weight: 600;
595
+ }
596
+
597
+ /* ── Scrollable table container ──────────────────────────────── */
598
+ .ss-table-wrap,
599
+ .ss-dash-table-wrap,
600
+ .ss-dbg-table-wrap {
601
+ flex: 1;
602
+ overflow-x: auto;
603
+ overflow-y: visible;
604
+ margin: 0 -2px;
605
+ padding: 0 2px;
606
+ scrollbar-width: thin;
607
+ scrollbar-color: var(--ss-border) transparent;
608
+ }
609
+
610
+ /* ── Sortable column headers ─────────────────────────────────── */
611
+ .ss-sortable,
612
+ .ss-dash-sortable,
613
+ .ss-dbg-sortable {
614
+ cursor: pointer;
615
+ user-select: none;
616
+ }
617
+ .ss-sortable:hover,
618
+ .ss-dash-sortable:hover,
619
+ .ss-dbg-sortable:hover {
620
+ color: var(--ss-accent);
621
+ }
622
+ .ss-sort-arrow,
623
+ .ss-dash-sort-arrow,
624
+ .ss-dbg-sort-arrow {
625
+ font-size: 10px;
626
+ color: var(--ss-accent);
627
+ margin-left: 2px;
628
+ }
629
+
630
+ /* ── Clickable table rows ────────────────────────────────────── */
631
+ .ss-table tr.ss-clickable,
632
+ .ss-dash-table tr.ss-dash-clickable,
633
+ .ss-dbg-table tr.ss-dbg-clickable {
634
+ cursor: pointer;
635
+ }
636
+ .ss-table tr.ss-clickable:hover td,
637
+ .ss-dash-table tr.ss-dash-clickable:hover td,
638
+ .ss-dbg-table tr.ss-dbg-clickable:hover td {
639
+ background: var(--ss-hover-accent);
640
+ }
641
+
642
+ /* ── Event time ──────────────────────────────────────────────── */
643
+ .ss-event-time,
644
+ .ss-dash-event-time,
645
+ .ss-dbg-event-time {
646
+ color: var(--ss-dim);
647
+ font-size: 10px;
648
+ }
649
+
650
+ /* ── EXPLAIN panel ───────────────────────────────────────────── */
651
+ .ss-explain-row td,
652
+ .ss-dash-explain-row td,
653
+ .ss-dbg-explain-row td {
654
+ white-space: normal;
655
+ overflow: visible;
656
+ max-width: none;
657
+ }
658
+ .ss-table tr.ss-explain-row:hover td,
659
+ .ss-dash-table tr.ss-dash-explain-row:hover td,
660
+ .ss-dbg-table tr.ss-dbg-explain-row:hover td {
661
+ background: var(--ss-surface-alt);
662
+ }
663
+ .ss-explain,
664
+ .ss-dash-explain,
665
+ .ss-dbg-explain {
666
+ padding: 12px 16px;
667
+ background: var(--ss-surface-alt);
668
+ border-top: 1px solid var(--ss-border-dim);
669
+ font-size: 11px;
670
+ overflow: hidden;
671
+ word-break: break-word;
672
+ }
673
+ .ss-explain-btn,
674
+ .ss-dash-explain-btn,
675
+ .ss-dbg-explain-btn {
676
+ padding: 2px 8px;
677
+ font-size: 9px;
678
+ color: var(--ss-purple-fg);
679
+ background: var(--ss-purple-bg);
680
+ border: none;
681
+ border-radius: 3px;
682
+ cursor: pointer;
683
+ text-transform: uppercase;
684
+ letter-spacing: 0.04em;
685
+ font-weight: 600;
686
+ }
687
+ .ss-explain-btn:hover,
688
+ .ss-dash-explain-btn:hover,
689
+ .ss-dbg-explain-btn:hover {
690
+ opacity: 0.8;
691
+ }
692
+ .ss-explain-btn-active,
693
+ .ss-dash-explain-btn-active,
694
+ .ss-dbg-explain-btn-active {
695
+ background: var(--ss-purple-fg);
696
+ color: #fff;
697
+ }
698
+ .ss-explain-result,
699
+ .ss-dash-explain-result,
700
+ .ss-dbg-explain-result {
701
+ margin-top: 6px;
702
+ font-size: 11px;
703
+ color: var(--ss-text-secondary);
704
+ }
705
+ .ss-explain-error,
706
+ .ss-dash-explain-error,
707
+ .ss-dbg-explain-error {
708
+ color: var(--ss-red-fg);
709
+ padding: 8px;
710
+ background: var(--ss-red-bg);
711
+ border-radius: 4px;
712
+ }
713
+ .ss-explain-node,
714
+ .ss-dash-explain-node,
715
+ .ss-dbg-explain-node {
716
+ padding: 4px 0;
717
+ border-left: 2px solid var(--ss-border-dim);
718
+ padding-left: 10px;
719
+ margin-top: 2px;
720
+ }
721
+ .ss-explain-node:first-child,
722
+ .ss-dash-explain-node:first-child,
723
+ .ss-dbg-explain-node:first-child {
724
+ border-left: 2px solid var(--ss-purple-fg);
725
+ }
726
+ .ss-explain-node-header,
727
+ .ss-dash-explain-node-header,
728
+ .ss-dbg-explain-node-header {
729
+ font-weight: 600;
730
+ color: var(--ss-text-primary);
731
+ font-size: 11px;
732
+ }
733
+ .ss-explain-node-type,
734
+ .ss-dash-explain-node-type,
735
+ .ss-dbg-explain-node-type {
736
+ color: var(--ss-purple-fg);
737
+ font-weight: 700;
738
+ }
739
+ .ss-explain-metrics,
740
+ .ss-dash-explain-metrics,
741
+ .ss-dbg-explain-metrics {
742
+ font-size: 10px;
743
+ color: var(--ss-dim);
744
+ margin-top: 1px;
745
+ font-family: var(--ss-font-mono);
746
+ word-break: break-all;
747
+ overflow-wrap: break-word;
748
+ }
749
+ .ss-explain-result table,
750
+ .ss-dash-explain-result table,
751
+ .ss-dbg-explain-result table {
752
+ width: 100%;
753
+ border-collapse: collapse;
754
+ margin-top: 4px;
755
+ }
756
+ .ss-explain-result th,
757
+ .ss-dash-explain-result th,
758
+ .ss-dbg-explain-result th {
759
+ padding: 3px 8px;
760
+ text-align: left;
761
+ font-weight: 600;
762
+ color: var(--ss-muted);
763
+ border-bottom: 1px solid var(--ss-border);
764
+ font-size: 9px;
765
+ text-transform: uppercase;
766
+ }
767
+ .ss-explain-result td,
768
+ .ss-dash-explain-result td,
769
+ .ss-dbg-explain-result td {
770
+ padding: 3px 8px;
771
+ border-bottom: 1px solid var(--ss-border-faint);
772
+ color: var(--ss-text-secondary);
773
+ }
520
774
 
521
775
  /* ── 3. Log system ───────────────────────────────────────────── */
522
776
  /* Log entry row: dashboard uses 16px horizontal padding, debug uses 12px.