adonisjs-server-stats 1.6.14 → 1.9.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 (69) hide show
  1. package/README.md +24 -8
  2. package/dist/core/index.js +248 -245
  3. package/dist/core/log-utils.d.ts +14 -0
  4. package/dist/react/{CacheSection-DGxMDlWK.js → CacheSection-xH75hwXu.js} +1 -1
  5. package/dist/react/{CacheTab-CnVW5PLs.js → CacheTab-DYmsZJJ1.js} +1 -1
  6. package/dist/react/{ConfigSection-DPcrfqXY.js → ConfigSection-D8BO1Ry9.js} +1 -1
  7. package/dist/react/{ConfigTab-BSWq_o2p.js → ConfigTab-CcN-tfjv.js} +1 -1
  8. package/dist/react/{CustomPaneTab-xjkYwTvH.js → CustomPaneTab-D7_o3Ec6.js} +1 -1
  9. package/dist/react/{EmailsSection-CSyTg1aX.js → EmailsSection-BzlsTdPs.js} +1 -1
  10. package/dist/react/{EmailsTab-Dh2YSa_f.js → EmailsTab-Uh2CQY3o.js} +44 -22
  11. package/dist/react/{EventsSection-C1pbJDfW.js → EventsSection-CGQWiIdV.js} +1 -1
  12. package/dist/react/{EventsTab-eCh02cdd.js → EventsTab-CC6DQzEm.js} +1 -1
  13. package/dist/react/{JobsSection-CLAin5vU.js → JobsSection-D7AHQmZi.js} +1 -1
  14. package/dist/react/{JobsTab-Dl5nrj2z.js → JobsTab-B3Lfdqed.js} +1 -1
  15. package/dist/react/LogsSection-Cly1dpvS.js +227 -0
  16. package/dist/react/LogsTab-BbYK-iyh.js +103 -0
  17. package/dist/react/{OverviewSection-nm3xdACz.js → OverviewSection-CkBGFEWq.js} +1 -1
  18. package/dist/react/{QueriesSection-DB12HMfQ.js → QueriesSection-CfCpnNUD.js} +1 -1
  19. package/dist/react/{QueriesTab-fyBB1u_Y.js → QueriesTab-DbBmAqzO.js} +1 -1
  20. package/dist/react/{RequestsSection-DTqB81ac.js → RequestsSection-Cb5a6MlT.js} +1 -1
  21. package/dist/react/{RoutesSection-DJWa4NPV.js → RoutesSection-CRqF-cNM.js} +1 -1
  22. package/dist/react/{RoutesTab-D3l8TOpu.js → RoutesTab-Bwreij3e.js} +1 -1
  23. package/dist/react/{TimelineSection-C4d-jRX1.js → TimelineSection-B2y06kRE.js} +1 -1
  24. package/dist/react/{TimelineTab-C5TFaSmQ.js → TimelineTab-6hthfdBB.js} +1 -1
  25. package/dist/react/{index-UdTfSvtO.js → index-CecA4IdQ.js} +394 -393
  26. package/dist/react/index.js +1 -1
  27. package/dist/react/react/components/shared/JsonViewer.d.ts +2 -1
  28. package/dist/react/style.css +1 -1
  29. package/dist/src/controller/debug_controller.js +10 -9
  30. package/dist/src/dashboard/dashboard_store.js +10 -6
  31. package/dist/src/data/data_access.js +12 -1
  32. package/dist/src/debug/email_collector.d.ts +11 -1
  33. package/dist/src/debug/email_collector.js +31 -1
  34. package/dist/src/debug/types.d.ts +1 -1
  35. package/dist/src/edge/client/dashboard.js +2 -2
  36. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  37. package/dist/src/edge/client-vue/dashboard.js +4 -4
  38. package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
  39. package/dist/src/provider/server_stats_provider.d.ts +31 -0
  40. package/dist/src/provider/server_stats_provider.js +244 -5
  41. package/dist/src/routes/register_routes.js +2 -2
  42. package/dist/src/styles/components.css +84 -0
  43. package/dist/vue/{CacheSection-C788Yfai.js → CacheSection-Cx-hj09X.js} +2 -2
  44. package/dist/vue/{ConfigSection-CRzYxqW2.js → ConfigSection-CMXyryf6.js} +1 -1
  45. package/dist/vue/{EmailsSection-C8JFMtW7.js → EmailsSection-DgKl9xGT.js} +1 -1
  46. package/dist/vue/EmailsTab-CNyEODVB.js +177 -0
  47. package/dist/vue/{EventsSection-C4wXUgxG.js → EventsSection-BNMCAim1.js} +2 -2
  48. package/dist/vue/{EventsTab-DQ4Nd6AK.js → EventsTab-BBM7olXF.js} +1 -1
  49. package/dist/vue/{JobsSection-CsKWTjgN.js → JobsSection-CCMgMlxd.js} +2 -2
  50. package/dist/vue/{JobsTab-BCvhOARO.js → JobsTab-WFnxPdN7.js} +1 -1
  51. package/dist/vue/{JsonViewer.vue_vue_type_script_setup_true_lang-Vsqar1zx.js → JsonViewer.vue_vue_type_script_setup_true_lang-Bid05zpm.js} +25 -23
  52. package/dist/vue/LogsSection-CvOnTxUu.js +252 -0
  53. package/dist/vue/LogsTab-Bg3o0Mm6.js +147 -0
  54. package/dist/vue/{OverviewSection-CbMdAido.js → OverviewSection-CHgaKtUR.js} +1 -1
  55. package/dist/vue/{QueriesSection-BPiv7u3r.js → QueriesSection-BnHRD98z.js} +1 -1
  56. package/dist/vue/{RequestsSection-LtImH4rD.js → RequestsSection-B-uSlM0f.js} +1 -1
  57. package/dist/vue/{RoutesSection-CrxOxmzx.js → RoutesSection-BrceOcKQ.js} +1 -1
  58. package/dist/vue/{TimelineSection-DLxMW2J_.js → TimelineSection-CfvnA2Oo.js} +1 -1
  59. package/dist/vue/components/DebugPanel/tabs/EmailsTab.vue.d.ts +2 -0
  60. package/dist/vue/components/shared/JsonViewer.vue.d.ts +3 -0
  61. package/dist/vue/{index-qCQpBftQ.js → index-oLxS08vN.js} +56 -54
  62. package/dist/vue/index.js +1 -1
  63. package/dist/vue/style.css +1 -1
  64. package/package.json +1 -1
  65. package/dist/react/LogsSection-C1p81fXO.js +0 -212
  66. package/dist/react/LogsTab-D-kR7PjX.js +0 -88
  67. package/dist/vue/EmailsTab-DhFhoNmU.js +0 -157
  68. package/dist/vue/LogsSection-BFVjSZ24.js +0 -227
  69. package/dist/vue/LogsTab-DpEQ7euu.js +0 -122
@@ -15,6 +15,9 @@ export default class ServerStatsProvider {
15
15
  private debugController;
16
16
  private apiController;
17
17
  private dashboardDepsAvailable;
18
+ private emailBridgeRedis;
19
+ private emailBridgeChannel;
20
+ private logStreamService;
18
21
  private pinoHookActive;
19
22
  private edgePluginActive;
20
23
  private prometheusActive;
@@ -58,6 +61,31 @@ export default class ServerStatsProvider {
58
61
  * This method creates the controller so those routes become functional.
59
62
  */
60
63
  private setupDashboard;
64
+ /**
65
+ * Lightweight email bridge publisher for non-web environments
66
+ * (queue workers, scheduler). Listens to local AdonisJS mail events
67
+ * and publishes them to Redis so the web server can ingest them.
68
+ */
69
+ private setupEmailBridgePublisher;
70
+ /**
71
+ * Set up live log streaming via Transmit (web environment only).
72
+ * Merged from the former standalone LogStreamProvider.
73
+ */
74
+ private setupLogStream;
75
+ /**
76
+ * Set up a Redis pub/sub bridge for cross-process email capture.
77
+ *
78
+ * Mail events (`mail:sending`, `mail:sent`, etc.) are process-local.
79
+ * When emails are sent from a Bull queue worker, the web server's
80
+ * {@link EmailCollector} never sees them. This bridge solves that:
81
+ *
82
+ * 1. **Every process** publishes mail events to a Redis channel.
83
+ * 2. **Every process** subscribes and ingests events from *other*
84
+ * processes (identified by a unique process tag).
85
+ *
86
+ * Requires `@adonisjs/redis`. Silently skipped if not installed.
87
+ */
88
+ private setupEmailBridge;
61
89
  /** Return diagnostics state for the Internals endpoint. */
62
90
  getDiagnostics(): {
63
91
  timers: {
@@ -97,6 +125,9 @@ export default class ServerStatsProvider {
97
125
  edgePlugin: {
98
126
  active: boolean;
99
127
  };
128
+ emailBridge: {
129
+ active: boolean;
130
+ };
100
131
  cacheInspector: {
101
132
  available: boolean;
102
133
  };
@@ -6,6 +6,7 @@ import { LogStreamService } from '../log_stream/log_stream_service.js';
6
6
  import { setShouldShow, setTraceCollector, setDashboardPath, setExcludedPrefixes, setOnRequestComplete, } from '../middleware/request_tracking_middleware.js';
7
7
  import { registerAllRoutes } from '../routes/register_routes.js';
8
8
  import { log, dim, bold, setVerbose } from '../utils/logger.js';
9
+ import { extractAddresses } from '../utils/mail_helpers.js';
9
10
  export default class ServerStatsProvider {
10
11
  app;
11
12
  intervalId = null;
@@ -23,6 +24,11 @@ export default class ServerStatsProvider {
23
24
  apiController = null;
24
25
  // Dashboard dependency check (set in boot, read in ready)
25
26
  dashboardDepsAvailable = true;
27
+ // Redis email bridge (cross-process email capture)
28
+ emailBridgeRedis = null;
29
+ emailBridgeChannel = 'adonisjs-server-stats:emails';
30
+ // Log stream (merged from LogStreamProvider)
31
+ logStreamService = null;
26
32
  // Diagnostics tracking
27
33
  pinoHookActive = false;
28
34
  edgePluginActive = false;
@@ -35,6 +41,11 @@ export default class ServerStatsProvider {
35
41
  this.app = app;
36
42
  }
37
43
  async boot() {
44
+ // In non-web environments (queue workers, scheduler, REPL), skip all
45
+ // route registration, Edge plugin, and heavy initialization. The
46
+ // email bridge publisher is set up in ready() instead.
47
+ if (this.app.getEnvironment() !== 'web')
48
+ return;
38
49
  try {
39
50
  await this.initializeBoot();
40
51
  }
@@ -279,10 +290,15 @@ export default class ServerStatsProvider {
279
290
  return;
280
291
  if (this.app.inTest && config.skipInTest !== false)
281
292
  return;
282
- // Defer the entire initialization to setImmediate so ready() returns
283
- // immediately. AdonisJS waits for all provider ready() hooks before
284
- // processing HTTP requests — blocking here would hang the server.
285
- // Routes use lazy controller getters that return 503 until init completes.
293
+ // ── Non-web environments: only start the email bridge publisher ──
294
+ if (this.app.getEnvironment() !== 'web') {
295
+ await this.setupEmailBridgePublisher();
296
+ return;
297
+ }
298
+ // ── Web environment: full initialization ──
299
+ // Defer to setImmediate so ready() returns immediately. AdonisJS waits
300
+ // for all provider ready() hooks before processing HTTP requests —
301
+ // blocking here would hang the server.
286
302
  setImmediate(() => {
287
303
  this.initializeServerStats(config).catch((err) => {
288
304
  log.warn(`failed to initialize: ${err?.message ?? err}\n` +
@@ -362,6 +378,8 @@ export default class ServerStatsProvider {
362
378
  }
363
379
  // ── Stats collection interval + transmit (lightweight, set up inline) ──
364
380
  this.setupStatsInterval(config);
381
+ // ── Live log streaming via Transmit ──
382
+ this.setupLogStream().catch(() => { });
365
383
  log.info('ready');
366
384
  }
367
385
  /**
@@ -447,6 +465,9 @@ export default class ServerStatsProvider {
447
465
  // Router not available
448
466
  }
449
467
  await this.debugStore.start(emitter, router);
468
+ // Set up Redis pub/sub bridge for cross-process email capture
469
+ // (e.g., emails sent from Bull queue workers)
470
+ await this.setupEmailBridge(emitter);
450
471
  // Create the debug controller (makes the debug routes functional)
451
472
  const serverConfig = this.app.config.get('server_stats');
452
473
  const DebugControllerClass = (await import('../controller/debug_controller.js')).default;
@@ -673,6 +694,211 @@ export default class ServerStatsProvider {
673
694
  }, 30_000);
674
695
  }
675
696
  }
697
+ /**
698
+ * Lightweight email bridge publisher for non-web environments
699
+ * (queue workers, scheduler). Listens to local AdonisJS mail events
700
+ * and publishes them to Redis so the web server can ingest them.
701
+ */
702
+ async setupEmailBridgePublisher() {
703
+ let emitter;
704
+ try {
705
+ emitter = (await this.app.container.make('emitter'));
706
+ }
707
+ catch {
708
+ return;
709
+ }
710
+ const { appImport } = await import('../utils/app_import.js');
711
+ let redis;
712
+ try {
713
+ const mod = await appImport('@adonisjs/redis/services/main');
714
+ redis = mod.default;
715
+ }
716
+ catch {
717
+ return; // @adonisjs/redis not installed
718
+ }
719
+ const tag = `${process.pid}-${Date.now()}`;
720
+ const ch = this.emailBridgeChannel;
721
+ const statusMap = [
722
+ ['mail:sending', 'sending'],
723
+ ['mail:sent', 'sent'],
724
+ ['mail:queueing', 'queueing'],
725
+ ['mail:queued', 'queued'],
726
+ ['queued:mail:error', 'failed'],
727
+ ];
728
+ const MAX_HTML = 50_000; // 50 KB cap per email body, same as EmailCollector
729
+ const capSize = (v) => {
730
+ if (!v || typeof v !== 'string')
731
+ return null;
732
+ return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
733
+ };
734
+ for (const [event, status] of statusMap) {
735
+ emitter.on(event, (data) => {
736
+ try {
737
+ const d = data;
738
+ const msg = (d?.message || d);
739
+ const payload = JSON.stringify({
740
+ _t: tag,
741
+ from: extractAddresses(msg?.from) || 'unknown',
742
+ to: extractAddresses(msg?.to) || 'unknown',
743
+ cc: extractAddresses(msg?.cc) || null,
744
+ bcc: extractAddresses(msg?.bcc) || null,
745
+ subject: msg?.subject || '(no subject)',
746
+ html: capSize(msg?.html),
747
+ text: capSize(msg?.text),
748
+ mailer: d?.mailerName || d?.mailer || 'unknown',
749
+ status,
750
+ messageId: d?.response?.messageId ||
751
+ d?.messageId ||
752
+ null,
753
+ attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
754
+ timestamp: Date.now(),
755
+ });
756
+ redis.publish(ch, payload).catch(() => { });
757
+ }
758
+ catch {
759
+ // Silently ignore serialization errors
760
+ }
761
+ });
762
+ }
763
+ log.info('email bridge publisher active (queue worker → Redis)');
764
+ }
765
+ /**
766
+ * Set up live log streaming via Transmit (web environment only).
767
+ * Merged from the former standalone LogStreamProvider.
768
+ */
769
+ async setupLogStream() {
770
+ let transmit;
771
+ try {
772
+ transmit = (await this.app.container.make('transmit'));
773
+ }
774
+ catch {
775
+ return; // @adonisjs/transmit not available
776
+ }
777
+ const channelName = this.app.config.get('server_stats.logChannelName', 'admin/logs');
778
+ const broadcast = (entry) => {
779
+ try {
780
+ transmit.broadcast(channelName, entry);
781
+ }
782
+ catch {
783
+ // Silently ignore broadcast errors
784
+ }
785
+ };
786
+ // If logCollector() is already hooked into Pino (zero-config),
787
+ // piggyback on its stream instead of creating a file poller.
788
+ const existing = getLogStreamService();
789
+ if (existing) {
790
+ const internal = existing;
791
+ const origOnEntry = internal.onEntry;
792
+ internal.onEntry = (entry) => {
793
+ origOnEntry?.(entry);
794
+ broadcast(entry);
795
+ };
796
+ return;
797
+ }
798
+ // Fallback: poll the log file directly
799
+ const logPath = this.app.makePath('logs', 'adonisjs.log');
800
+ this.logStreamService = new LogStreamService(logPath, broadcast);
801
+ await this.logStreamService.start();
802
+ }
803
+ /**
804
+ * Set up a Redis pub/sub bridge for cross-process email capture.
805
+ *
806
+ * Mail events (`mail:sending`, `mail:sent`, etc.) are process-local.
807
+ * When emails are sent from a Bull queue worker, the web server's
808
+ * {@link EmailCollector} never sees them. This bridge solves that:
809
+ *
810
+ * 1. **Every process** publishes mail events to a Redis channel.
811
+ * 2. **Every process** subscribes and ingests events from *other*
812
+ * processes (identified by a unique process tag).
813
+ *
814
+ * Requires `@adonisjs/redis`. Silently skipped if not installed.
815
+ */
816
+ async setupEmailBridge(emitter) {
817
+ if (!emitter)
818
+ return;
819
+ const { appImport } = await import('../utils/app_import.js');
820
+ let redis;
821
+ try {
822
+ const mod = await appImport('@adonisjs/redis/services/main');
823
+ redis = mod.default;
824
+ }
825
+ catch {
826
+ return; // @adonisjs/redis not installed — skip
827
+ }
828
+ const CHANNEL = this.emailBridgeChannel;
829
+ const processTag = `${process.pid}-${Date.now()}`;
830
+ const em = emitter;
831
+ // ── Publish local mail events to Redis ──────────────────────
832
+ const statusMap = [
833
+ ['mail:sending', 'sending'],
834
+ ['mail:sent', 'sent'],
835
+ ['mail:queueing', 'queueing'],
836
+ ['mail:queued', 'queued'],
837
+ ['queued:mail:error', 'failed'],
838
+ ];
839
+ const MAX_HTML = 50_000;
840
+ const capSize = (v) => {
841
+ if (!v || typeof v !== 'string')
842
+ return null;
843
+ return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
844
+ };
845
+ for (const [event, status] of statusMap) {
846
+ em.on(event, (data) => {
847
+ try {
848
+ const d = data;
849
+ const msg = d?.message || d;
850
+ const payload = JSON.stringify({
851
+ _t: processTag,
852
+ from: extractAddresses(msg?.from) || 'unknown',
853
+ to: extractAddresses(msg?.to) || 'unknown',
854
+ cc: extractAddresses(msg?.cc) || null,
855
+ bcc: extractAddresses(msg?.bcc) || null,
856
+ subject: msg?.subject || '(no subject)',
857
+ html: capSize(msg?.html),
858
+ text: capSize(msg?.text),
859
+ mailer: d?.mailerName || d?.mailer || 'unknown',
860
+ status,
861
+ messageId: d?.response?.messageId || d?.messageId || null,
862
+ attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
863
+ timestamp: Date.now(),
864
+ });
865
+ redis.publish(CHANNEL, payload).catch(() => { });
866
+ }
867
+ catch {
868
+ // Silently ignore serialization errors
869
+ }
870
+ });
871
+ }
872
+ // ── Subscribe to receive emails from other processes ─────────
873
+ try {
874
+ await redis.subscribe(CHANNEL, (message) => {
875
+ try {
876
+ const parsed = JSON.parse(message);
877
+ if (parsed._t === processTag)
878
+ return; // Skip own messages
879
+ const { _t: _, ...fields } = parsed;
880
+ const record = {
881
+ ...fields,
882
+ html: fields.html || null,
883
+ text: fields.text || null,
884
+ };
885
+ // Ingest into in-memory ring buffer (debug panel)
886
+ this.debugStore?.emails.ingest(record);
887
+ // Also persist to SQLite dashboard (the sending process has
888
+ // no DashboardStore, so we must record it here)
889
+ this.dashboardStore?.recordEmail({ id: 0, ...record });
890
+ }
891
+ catch {
892
+ // Ignore malformed messages
893
+ }
894
+ });
895
+ this.emailBridgeRedis = redis;
896
+ log.info('email bridge active (cross-process capture via Redis)');
897
+ }
898
+ catch {
899
+ // Subscribe failed — bridge unavailable, local capture still works
900
+ }
901
+ }
676
902
  /** Return diagnostics state for the Internals endpoint. */
677
903
  getDiagnostics() {
678
904
  const config = this.resolvedConfig;
@@ -711,6 +937,7 @@ export default class ServerStatsProvider {
711
937
  mode: this.pinoHookActive ? 'stream' : toolbarConfig?.enabled ? 'none' : 'none',
712
938
  },
713
939
  edgePlugin: { active: this.edgePluginActive },
940
+ emailBridge: { active: this.emailBridgeRedis !== null },
714
941
  cacheInspector: {
715
942
  available: this.resolvedCollectors.some((c) => c.name === 'redis'),
716
943
  },
@@ -773,7 +1000,19 @@ export default class ServerStatsProvider {
773
1000
  log.warn('could not save debug data on shutdown — ' + err?.message);
774
1001
  }
775
1002
  }
776
- // Clean up dashboard resources
1003
+ // Unsubscribe Redis email bridge
1004
+ if (this.emailBridgeRedis) {
1005
+ try {
1006
+ ;
1007
+ this.emailBridgeRedis.unsubscribe(this.emailBridgeChannel);
1008
+ }
1009
+ catch {
1010
+ // Ignore cleanup errors
1011
+ }
1012
+ this.emailBridgeRedis = null;
1013
+ }
1014
+ // Clean up log stream and dashboard resources
1015
+ this.logStreamService?.stop();
777
1016
  this.dashboardLogStream?.stop();
778
1017
  setOnRequestComplete(null);
779
1018
  setDashboardPath(null);
@@ -125,14 +125,14 @@ export function registerAllRoutes(options) {
125
125
  .as('server-stats.debug.logs');
126
126
  router
127
127
  .get('/emails', bindApi(async (api, ctx) => {
128
- const result = await api.getEmails({ source: 'memory' });
128
+ const result = await api.getEmails({});
129
129
  return ctx.response.json({ emails: result.data, total: result.meta.total });
130
130
  }))
131
131
  .as('server-stats.debug.emails');
132
132
  router
133
133
  .get('/emails/:id/preview', bindApi(async (api, ctx) => {
134
134
  const id = Number(ctx.params.id);
135
- const html = await api.getEmailPreview(id, 'memory');
135
+ const html = await api.getEmailPreview(id);
136
136
  if (!html) {
137
137
  return ctx.response.notFound({ error: 'Email not found' });
138
138
  }
@@ -522,6 +522,87 @@
522
522
  word-break: break-word;
523
523
  }
524
524
 
525
+ /* Expandable log row */
526
+ .ss-log-entry-expandable,
527
+ .ss-dash-log-entry-expandable,
528
+ .ss-dbg-log-entry-expandable {
529
+ cursor: pointer;
530
+ }
531
+ .ss-log-entry-expandable:hover,
532
+ .ss-dash-log-entry-expandable:hover,
533
+ .ss-dbg-log-entry-expandable:hover {
534
+ background: var(--ss-surface-alt);
535
+ }
536
+ .ss-log-expand-icon,
537
+ .ss-dash-log-expand-icon,
538
+ .ss-dbg-log-expand-icon {
539
+ flex-shrink: 0;
540
+ font-size: 10px;
541
+ color: var(--ss-dim);
542
+ width: 14px;
543
+ text-align: center;
544
+ transition: transform 0.15s ease;
545
+ user-select: none;
546
+ }
547
+ .ss-log-expand-icon-open,
548
+ .ss-dash-log-expand-icon-open,
549
+ .ss-dbg-log-expand-icon-open {
550
+ transform: rotate(90deg);
551
+ }
552
+ .ss-log-detail,
553
+ .ss-dash-log-detail,
554
+ .ss-dbg-log-detail {
555
+ padding: 6px var(--ss-log-px, 12px) 10px var(--ss-log-px, 12px);
556
+ border-bottom: 1px solid var(--ss-log-border, var(--ss-input-bg));
557
+ background: var(--ss-surface-alt);
558
+ font-size: 11px;
559
+ }
560
+ .ss-log-detail .ss-dash-data-cell,
561
+ .ss-log-detail .ss-dbg-data-cell,
562
+ .ss-dash-log-detail .ss-dash-data-cell,
563
+ .ss-dbg-log-detail .ss-dbg-data-cell {
564
+ width: 100%;
565
+ max-width: none;
566
+ }
567
+ .ss-log-detail .ss-dash-data-full,
568
+ .ss-log-detail .ss-dbg-data-full,
569
+ .ss-dash-log-detail .ss-dash-data-full,
570
+ .ss-dbg-log-detail .ss-dbg-data-full {
571
+ position: relative;
572
+ margin: 0;
573
+ padding: 8px 12px;
574
+ border-radius: 4px;
575
+ background: var(--ss-bg);
576
+ border: 1px solid var(--ss-border-dim);
577
+ width: 100%;
578
+ box-sizing: border-box;
579
+ cursor: pointer;
580
+ }
581
+ .ss-log-detail .ss-dash-data-full pre,
582
+ .ss-log-detail .ss-dbg-data-full pre,
583
+ .ss-dash-log-detail .ss-dash-data-full pre,
584
+ .ss-dbg-log-detail .ss-dbg-data-full pre {
585
+ margin: 0;
586
+ white-space: pre-wrap;
587
+ word-break: break-all;
588
+ font-size: 11px;
589
+ line-height: 1.5;
590
+ color: var(--ss-text-secondary);
591
+ }
592
+ .ss-log-detail .ss-dash-copy-btn,
593
+ .ss-log-detail .ss-dbg-copy-btn,
594
+ .ss-dash-log-detail .ss-dash-copy-btn,
595
+ .ss-dbg-log-detail .ss-dbg-copy-btn {
596
+ position: absolute;
597
+ top: 4px;
598
+ right: 4px;
599
+ padding: 2px 8px;
600
+ font-size: 10px;
601
+ border: 1px solid var(--ss-border-dim);
602
+ border-radius: 3px;
603
+ background: var(--ss-surface-alt);
604
+ }
605
+
525
606
  /* ── 4. Email preview ────────────────────────────────────────── */
526
607
  /* Dashboard uses 16px padding, debug uses 12px — override via
527
608
  * --ss-email-px on a wrapper. */
@@ -602,6 +683,9 @@
602
683
  background: var(--ss-amber-bg);
603
684
  color: var(--ss-amber-fg);
604
685
  }
686
+ .ss-email-status-queueing,
687
+ .ss-dash-email-status-queueing,
688
+ .ss-dbg-email-status-queueing,
605
689
  .ss-email-status-queued,
606
690
  .ss-dash-email-status-queued,
607
691
  .ss-dbg-email-status-queued {
@@ -1,9 +1,9 @@
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-qCQpBftQ.js";
2
+ import { u as H } from "./index-oLxS08vN.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";
6
- import { _ as J } from "./JsonViewer.vue_vue_type_script_setup_true_lang-Vsqar1zx.js";
6
+ import { _ as J } from "./JsonViewer.vue_vue_type_script_setup_true_lang-Bid05zpm.js";
7
7
  import { _ as O } from "./FilterBar.vue_vue_type_script_setup_true_lang-ClJ37hhT.js";
8
8
  const P = {
9
9
  key: 0,
@@ -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-qCQpBftQ.js";
2
+ import { u as ue } from "./index-oLxS08vN.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,
@@ -1,6 +1,6 @@
1
1
  import { defineComponent as V, inject as h, ref as f, computed as A, openBlock as o, createElementBlock as i, createElementVNode as e, Fragment as c, createTextVNode as u, toDisplayString as n, createCommentVNode as y, normalizeClass as _, createVNode as F, unref as r, renderList as B, createBlock as D } from "vue";
2
2
  import { formatTime as R, timeAgo as z } from "adonisjs-server-stats/core";
3
- import { u as L } from "./index-qCQpBftQ.js";
3
+ import { u as L } from "./index-oLxS08vN.js";
4
4
  import { u as M } from "./useResizableTable-BoivAevK.js";
5
5
  import { _ as U } from "./FilterBar.vue_vue_type_script_setup_true_lang-ClJ37hhT.js";
6
6
  import { _ as q } from "./PaginationControls.vue_vue_type_script_setup_true_lang-CuN7g_8Z.js";
@@ -0,0 +1,177 @@
1
+ import { defineComponent as A, ref as v, computed as f, openBlock as o, createElementBlock as i, createElementVNode as t, createTextVNode as b, toDisplayString as a, createCommentVNode as p, normalizeClass as y, Fragment as _, withDirectives as E, vModelText as j, renderList as q, unref as w } from "vue";
2
+ import { formatTime as N, timeAgo as S } from "adonisjs-server-stats/core";
3
+ import { u as $ } from "./useResizableTable-BoivAevK.js";
4
+ const F = { style: { position: "relative", height: "100%" } }, P = {
5
+ key: 0,
6
+ class: "ss-dbg-email-preview"
7
+ }, V = { class: "ss-dbg-email-preview-header" }, z = { class: "ss-dbg-email-preview-meta" }, B = { key: 0 }, M = {
8
+ key: 0,
9
+ class: "ss-dbg-empty"
10
+ }, R = ["srcdoc"], D = {
11
+ key: 2,
12
+ class: "ss-dbg-empty"
13
+ }, H = { class: "ss-dbg-search-bar" }, U = { class: "ss-dbg-summary" }, G = {
14
+ key: 0,
15
+ class: "ss-dbg-empty"
16
+ }, I = ["onClick"], J = {
17
+ class: "ss-dbg-c-dim",
18
+ style: { "white-space": "nowrap" }
19
+ }, K = ["title"], O = ["title"], Q = { class: "ss-dbg-c-sql" }, W = { class: "ss-dbg-c-muted" }, X = {
20
+ class: "ss-dbg-c-dim",
21
+ style: { "text-align": "center" }
22
+ }, Y = ["title"], et = /* @__PURE__ */ A({
23
+ __name: "EmailsTab",
24
+ props: {
25
+ data: {},
26
+ dashboardPath: {},
27
+ debugEndpoint: {},
28
+ authToken: {}
29
+ },
30
+ setup(k) {
31
+ const d = k, c = v(""), n = v(null), r = v(null), m = v(!1), g = f(() => {
32
+ const l = d.data, e = l ? (Array.isArray(l) ? l : l.emails) || [] : [];
33
+ if (!c.value.trim()) return e;
34
+ const s = c.value.toLowerCase();
35
+ return e.filter(
36
+ (u) => u.subject.toLowerCase().includes(s) || u.from.toLowerCase().includes(s) || u.to.toLowerCase().includes(s) || u.mailer && u.mailer.toLowerCase().includes(s)
37
+ );
38
+ }), C = f(() => {
39
+ const l = d.data;
40
+ return `${(l ? (Array.isArray(l) ? l : l.emails) || [] : []).length} emails`;
41
+ });
42
+ function h(l) {
43
+ return {
44
+ sent: "ss-dbg-email-status-sent",
45
+ sending: "ss-dbg-email-status-sending",
46
+ queueing: "ss-dbg-email-status-queued",
47
+ queued: "ss-dbg-email-status-queued",
48
+ failed: "ss-dbg-email-status-failed"
49
+ }[l] || "";
50
+ }
51
+ async function T(l) {
52
+ if (n.value = l, r.value = l.html || null, !r.value && l.id) {
53
+ m.value = !0;
54
+ try {
55
+ const e = d.debugEndpoint || "/admin/api/debug", s = {};
56
+ d.authToken && (s.Authorization = `Bearer ${d.authToken}`);
57
+ const u = await fetch(`${e}/emails/${l.id}/preview`, {
58
+ headers: s,
59
+ credentials: d.authToken ? "omit" : "include"
60
+ });
61
+ u.ok && (r.value = await u.text());
62
+ } catch {
63
+ } finally {
64
+ m.value = !1;
65
+ }
66
+ }
67
+ }
68
+ function L() {
69
+ n.value = null, r.value = null, m.value = !1;
70
+ }
71
+ const { tableRef: x } = $(() => g.value);
72
+ return (l, e) => (o(), i("div", F, [
73
+ n.value ? (o(), i("div", P, [
74
+ t("div", V, [
75
+ t("div", z, [
76
+ t("div", null, [
77
+ e[1] || (e[1] = t("strong", null, "From:", -1)),
78
+ b(" " + a(n.value.from), 1)
79
+ ]),
80
+ t("div", null, [
81
+ e[2] || (e[2] = t("strong", null, "To:", -1)),
82
+ b(" " + a(n.value.to), 1)
83
+ ]),
84
+ n.value.cc ? (o(), i("div", B, [
85
+ e[3] || (e[3] = t("strong", null, "CC:", -1)),
86
+ b(" " + a(n.value.cc), 1)
87
+ ])) : p("", !0),
88
+ t("div", null, [
89
+ e[4] || (e[4] = t("strong", null, "Subject:", -1)),
90
+ b(" " + a(n.value.subject), 1)
91
+ ]),
92
+ t("div", null, [
93
+ e[5] || (e[5] = t("strong", null, "Status:", -1)),
94
+ t("span", {
95
+ class: y(["ss-dbg-email-status", h(n.value.status)])
96
+ }, a(n.value.status), 3)
97
+ ])
98
+ ]),
99
+ t("button", {
100
+ type: "button",
101
+ class: "ss-dbg-btn-clear",
102
+ onClick: L
103
+ }, "×")
104
+ ]),
105
+ m.value ? (o(), i("div", M, "Loading preview...")) : r.value ? (o(), i("iframe", {
106
+ key: 1,
107
+ class: "ss-dbg-email-iframe",
108
+ srcdoc: r.value
109
+ }, null, 8, R)) : (o(), i("div", D, "No HTML content"))
110
+ ])) : p("", !0),
111
+ n.value ? p("", !0) : (o(), i(_, { key: 1 }, [
112
+ t("div", H, [
113
+ E(t("input", {
114
+ "onUpdate:modelValue": e[0] || (e[0] = (s) => c.value = s),
115
+ class: "ss-dbg-search",
116
+ placeholder: "Filter emails...",
117
+ type: "text"
118
+ }, null, 512), [
119
+ [j, c.value]
120
+ ]),
121
+ t("span", U, a(C.value), 1)
122
+ ]),
123
+ g.value.length === 0 ? (o(), i("div", G, "No emails captured")) : (o(), i("table", {
124
+ key: 1,
125
+ ref_key: "tableRef",
126
+ ref: x,
127
+ class: "ss-dbg-table"
128
+ }, [
129
+ e[6] || (e[6] = t("thead", null, [
130
+ t("tr", null, [
131
+ t("th", null, "#"),
132
+ t("th", null, "From"),
133
+ t("th", null, "To"),
134
+ t("th", null, "Subject"),
135
+ t("th", null, "Status"),
136
+ t("th", null, "Mailer"),
137
+ t("th", { title: "Attachments" }, "📎"),
138
+ t("th", null, "Time")
139
+ ])
140
+ ], -1)),
141
+ t("tbody", null, [
142
+ (o(!0), i(_, null, q(g.value, (s) => (o(), i("tr", {
143
+ key: s.id,
144
+ class: "ss-dbg-email-row",
145
+ onClick: (u) => T(s)
146
+ }, [
147
+ t("td", J, a(s.id), 1),
148
+ t("td", {
149
+ class: "ss-dbg-c-secondary",
150
+ title: s.from
151
+ }, a(s.from), 9, K),
152
+ t("td", {
153
+ class: "ss-dbg-c-secondary",
154
+ title: s.to
155
+ }, a(s.to), 9, O),
156
+ t("td", Q, a(s.subject), 1),
157
+ t("td", null, [
158
+ t("span", {
159
+ class: y(["ss-dbg-email-status", h(s.status)])
160
+ }, a(s.status), 3)
161
+ ]),
162
+ t("td", W, a(s.mailer), 1),
163
+ t("td", X, a(s.attachmentCount > 0 ? s.attachmentCount : "-"), 1),
164
+ t("td", {
165
+ class: "ss-dbg-event-time",
166
+ title: w(N)(s.timestamp)
167
+ }, a(w(S)(s.timestamp)), 9, Y)
168
+ ], 8, I))), 128))
169
+ ])
170
+ ], 512))
171
+ ], 64))
172
+ ]));
173
+ }
174
+ });
175
+ export {
176
+ et as default
177
+ };