adonisjs-server-stats 1.0.10 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/dist/src/controller/debug_controller.d.ts +2 -0
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.js +14 -0
- package/dist/src/debug/debug_store.d.ts +6 -0
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +40 -0
- package/dist/src/debug/email_collector.d.ts +36 -0
- package/dist/src/debug/email_collector.d.ts.map +1 -0
- package/dist/src/debug/email_collector.js +138 -0
- package/dist/src/debug/event_collector.d.ts +2 -0
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +11 -2
- package/dist/src/debug/query_collector.d.ts +2 -0
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +6 -0
- package/dist/src/debug/ring_buffer.d.ts +4 -0
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/ring_buffer.js +10 -0
- package/dist/src/debug/types.d.ts +38 -0
- package/dist/src/debug/types.d.ts.map +1 -1
- package/dist/src/edge/client/debug-panel.css +50 -0
- package/dist/src/edge/client/debug-panel.js +117 -0
- package/dist/src/edge/views/debug-panel.edge +16 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts +2 -0
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +33 -0
- package/dist/src/types.d.ts +12 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Zero frontend dependencies. Zero build step. Just `@serverStats()` and go.
|
|
|
34
34
|
## Features
|
|
35
35
|
|
|
36
36
|
- **Live stats bar** -- CPU, memory, event loop lag, HTTP throughput, DB pool, Redis, queues, logs
|
|
37
|
-
- **Debug toolbar** -- SQL queries, events, routes, logs with search and filtering
|
|
37
|
+
- **Debug toolbar** -- SQL queries, events, emails, routes, logs with search and filtering
|
|
38
38
|
- **Custom panes** -- add your own tabs (webhooks, emails, cache, anything) with a simple config
|
|
39
39
|
- **Pluggable collectors** -- use built-in collectors or write your own
|
|
40
40
|
- **Visibility control** -- show only to admins, specific roles, or in dev mode
|
|
@@ -223,13 +223,15 @@ export default class ServerStatsController {
|
|
|
223
223
|
|
|
224
224
|
### `DevToolbarOptions`
|
|
225
225
|
|
|
226
|
-
| Option | Type | Default | Description
|
|
227
|
-
|
|
228
|
-
| `enabled` | `boolean` | `false` | Enable the dev toolbar
|
|
229
|
-
| `maxQueries` | `number` | `500` | Max SQL queries to buffer
|
|
230
|
-
| `maxEvents` | `number` | `200` | Max events to buffer
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
226
|
+
| Option | Type | Default | Description |
|
|
227
|
+
|------------------------|-----------------|---------|------------------------------------------------|
|
|
228
|
+
| `enabled` | `boolean` | `false` | Enable the dev toolbar |
|
|
229
|
+
| `maxQueries` | `number` | `500` | Max SQL queries to buffer |
|
|
230
|
+
| `maxEvents` | `number` | `200` | Max events to buffer |
|
|
231
|
+
| `maxEmails` | `number` | `100` | Max emails to buffer |
|
|
232
|
+
| `slowQueryThresholdMs` | `number` | `100` | Slow query threshold (ms) |
|
|
233
|
+
| `persistDebugData` | `boolean` | `false` | Persist debug data to disk across restarts |
|
|
234
|
+
| `panes` | `DebugPane[]` | -- | Custom debug panel tabs |
|
|
233
235
|
|
|
234
236
|
---
|
|
235
237
|
|
|
@@ -378,7 +380,7 @@ Features:
|
|
|
378
380
|
|
|
379
381
|
## Dev Toolbar
|
|
380
382
|
|
|
381
|
-
Adds a debug panel with SQL query inspection, event tracking, route table, and live logs. Only active in non-production environments.
|
|
383
|
+
Adds a debug panel with SQL query inspection, event tracking, email capture with HTML preview, route table, and live logs. Only active in non-production environments.
|
|
382
384
|
|
|
383
385
|
```ts
|
|
384
386
|
export default defineConfig({
|
|
@@ -386,7 +388,9 @@ export default defineConfig({
|
|
|
386
388
|
enabled: true,
|
|
387
389
|
maxQueries: 500,
|
|
388
390
|
maxEvents: 200,
|
|
391
|
+
maxEmails: 100,
|
|
389
392
|
slowQueryThresholdMs: 100,
|
|
393
|
+
persistDebugData: true, // survive server restarts
|
|
390
394
|
},
|
|
391
395
|
})
|
|
392
396
|
```
|
|
@@ -400,11 +404,24 @@ router
|
|
|
400
404
|
router.get('queries', '#controllers/admin/debug_controller.queries')
|
|
401
405
|
router.get('events', '#controllers/admin/debug_controller.events')
|
|
402
406
|
router.get('routes', '#controllers/admin/debug_controller.routes')
|
|
407
|
+
router.get('emails', '#controllers/admin/debug_controller.emails')
|
|
408
|
+
router.get('emails/:id/preview', '#controllers/admin/debug_controller.emailPreview')
|
|
403
409
|
})
|
|
404
410
|
.prefix('/admin/api/debug')
|
|
405
411
|
.use(middleware.admin())
|
|
406
412
|
```
|
|
407
413
|
|
|
414
|
+
### Built-in Emails Tab
|
|
415
|
+
|
|
416
|
+
The debug toolbar captures all emails sent via AdonisJS mail (`mail:sending`, `mail:sent`, `mail:queued`, `queued:mail:error` events). Click any email row to preview its HTML in an iframe.
|
|
417
|
+
|
|
418
|
+
### Persistent Debug Data
|
|
419
|
+
|
|
420
|
+
Enable `persistDebugData: true` to save queries, events, and emails to `tmp/debug-data.json`. Data is:
|
|
421
|
+
- **Loaded** on server startup (before collectors start)
|
|
422
|
+
- **Flushed** every 30 seconds (handles crashes)
|
|
423
|
+
- **Saved** on graceful shutdown
|
|
424
|
+
|
|
408
425
|
### Custom Debug Panes
|
|
409
426
|
|
|
410
427
|
Add custom tabs to the debug panel:
|
|
@@ -562,6 +579,7 @@ import type {
|
|
|
562
579
|
BadgeColor,
|
|
563
580
|
QueryRecord,
|
|
564
581
|
EventRecord,
|
|
582
|
+
EmailRecord,
|
|
565
583
|
RouteRecord,
|
|
566
584
|
} from 'adonisjs-server-stats'
|
|
567
585
|
|
|
@@ -6,5 +6,7 @@ export default class DebugController {
|
|
|
6
6
|
queries({ response }: HttpContext): Promise<void>;
|
|
7
7
|
events({ response }: HttpContext): Promise<void>;
|
|
8
8
|
routes({ response }: HttpContext): Promise<void>;
|
|
9
|
+
emails({ response }: HttpContext): Promise<void>;
|
|
10
|
+
emailPreview({ params, response }: HttpContext): Promise<void>;
|
|
9
11
|
}
|
|
10
12
|
//# sourceMappingURL=debug_controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,OAAO,OAAO,eAAe;IACtB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE/B,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAMjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;
|
|
1
|
+
{"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,MAAM,CAAC,OAAO,OAAO,eAAe;IACtB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE/B,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAMjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAOhC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;CAQrD"}
|
|
@@ -16,4 +16,18 @@ export default class DebugController {
|
|
|
16
16
|
const routes = this.store.routes.getRoutes();
|
|
17
17
|
return response.json({ routes, total: this.store.routes.getRouteCount() });
|
|
18
18
|
}
|
|
19
|
+
async emails({ response }) {
|
|
20
|
+
const emails = this.store.emails.getLatest(100);
|
|
21
|
+
// Strip html/text from list response to keep it lightweight
|
|
22
|
+
const stripped = emails.map(({ html, text, ...rest }) => rest);
|
|
23
|
+
return response.json({ emails: stripped, total: this.store.emails.getTotalCount() });
|
|
24
|
+
}
|
|
25
|
+
async emailPreview({ params, response }) {
|
|
26
|
+
const id = Number(params.id);
|
|
27
|
+
const html = this.store.emails.getEmailHtml(id);
|
|
28
|
+
if (!html) {
|
|
29
|
+
return response.notFound({ error: 'Email not found' });
|
|
30
|
+
}
|
|
31
|
+
return response.header('Content-Type', 'text/html; charset=utf-8').send(html);
|
|
32
|
+
}
|
|
19
33
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { QueryCollector } from "./query_collector.js";
|
|
2
2
|
import { EventCollector } from "./event_collector.js";
|
|
3
|
+
import { EmailCollector } from "./email_collector.js";
|
|
3
4
|
import { RouteInspector } from "./route_inspector.js";
|
|
4
5
|
import type { DevToolbarConfig } from "./types.js";
|
|
5
6
|
/**
|
|
@@ -9,9 +10,14 @@ import type { DevToolbarConfig } from "./types.js";
|
|
|
9
10
|
export declare class DebugStore {
|
|
10
11
|
readonly queries: QueryCollector;
|
|
11
12
|
readonly events: EventCollector;
|
|
13
|
+
readonly emails: EmailCollector;
|
|
12
14
|
readonly routes: RouteInspector;
|
|
13
15
|
constructor(config: DevToolbarConfig);
|
|
14
16
|
start(emitter: any, router: any): Promise<void>;
|
|
15
17
|
stop(): void;
|
|
18
|
+
/** Serialize all collector data to a JSON file (atomic write). */
|
|
19
|
+
saveToDisk(filePath: string): Promise<void>;
|
|
20
|
+
/** Restore collector data from a JSON file on disk. */
|
|
21
|
+
loadFromDisk(filePath: string): Promise<void>;
|
|
16
22
|
}
|
|
17
23
|
//# sourceMappingURL=debug_store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;gBAEpB,MAAM,EAAE,gBAAgB;IAU9B,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD,IAAI,IAAI,IAAI;IAMZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAajD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBpD"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { writeFile, readFile, rename, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { QueryCollector } from "./query_collector.js";
|
|
2
4
|
import { EventCollector } from "./event_collector.js";
|
|
5
|
+
import { EmailCollector } from "./email_collector.js";
|
|
3
6
|
import { RouteInspector } from "./route_inspector.js";
|
|
4
7
|
/**
|
|
5
8
|
* Singleton store holding all debug data collectors.
|
|
@@ -8,19 +11,56 @@ import { RouteInspector } from "./route_inspector.js";
|
|
|
8
11
|
export class DebugStore {
|
|
9
12
|
queries;
|
|
10
13
|
events;
|
|
14
|
+
emails;
|
|
11
15
|
routes;
|
|
12
16
|
constructor(config) {
|
|
13
17
|
this.queries = new QueryCollector(config.maxQueries, config.slowQueryThresholdMs);
|
|
14
18
|
this.events = new EventCollector(config.maxEvents);
|
|
19
|
+
this.emails = new EmailCollector(config.maxEmails);
|
|
15
20
|
this.routes = new RouteInspector();
|
|
16
21
|
}
|
|
17
22
|
async start(emitter, router) {
|
|
18
23
|
await this.queries.start(emitter);
|
|
19
24
|
this.events.start(emitter);
|
|
25
|
+
await this.emails.start(emitter);
|
|
20
26
|
this.routes.inspect(router);
|
|
21
27
|
}
|
|
22
28
|
stop() {
|
|
23
29
|
this.queries.stop();
|
|
24
30
|
this.events.stop();
|
|
31
|
+
this.emails.stop();
|
|
32
|
+
}
|
|
33
|
+
/** Serialize all collector data to a JSON file (atomic write). */
|
|
34
|
+
async saveToDisk(filePath) {
|
|
35
|
+
const data = {
|
|
36
|
+
queries: this.queries.getQueries(),
|
|
37
|
+
events: this.events.getEvents(),
|
|
38
|
+
emails: this.emails.getEmails(),
|
|
39
|
+
};
|
|
40
|
+
const json = JSON.stringify(data);
|
|
41
|
+
const tmpPath = filePath + ".tmp";
|
|
42
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
43
|
+
await writeFile(tmpPath, json, "utf-8");
|
|
44
|
+
await rename(tmpPath, filePath);
|
|
45
|
+
}
|
|
46
|
+
/** Restore collector data from a JSON file on disk. */
|
|
47
|
+
async loadFromDisk(filePath) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(filePath, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // file doesn't exist yet
|
|
54
|
+
}
|
|
55
|
+
const data = JSON.parse(raw);
|
|
56
|
+
if (Array.isArray(data.queries) && data.queries.length > 0) {
|
|
57
|
+
this.queries.loadRecords(data.queries);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(data.events) && data.events.length > 0) {
|
|
60
|
+
this.events.loadRecords(data.events);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(data.emails) && data.emails.length > 0) {
|
|
63
|
+
this.emails.loadRecords(data.emails);
|
|
64
|
+
}
|
|
25
65
|
}
|
|
26
66
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EmailRecord } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Listens to AdonisJS mail events and stores captured emails in a ring buffer.
|
|
4
|
+
*
|
|
5
|
+
* Events:
|
|
6
|
+
* - `mail:sending` — email about to be sent
|
|
7
|
+
* - `mail:sent` — email successfully sent (updates matching 'sending' record)
|
|
8
|
+
* - `mail:queued` — email queued for later delivery
|
|
9
|
+
* - `queued:mail:error` — queued email failed
|
|
10
|
+
*/
|
|
11
|
+
export declare class EmailCollector {
|
|
12
|
+
private buffer;
|
|
13
|
+
private emitter;
|
|
14
|
+
private handlers;
|
|
15
|
+
constructor(maxEmails?: number);
|
|
16
|
+
start(emitter: any): Promise<void>;
|
|
17
|
+
stop(): void;
|
|
18
|
+
getEmails(): EmailRecord[];
|
|
19
|
+
getLatest(n?: number): EmailRecord[];
|
|
20
|
+
getEmailHtml(id: number): string | null;
|
|
21
|
+
getTotalCount(): number;
|
|
22
|
+
clear(): void;
|
|
23
|
+
private buildRecord;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize various address formats to a comma-separated string.
|
|
26
|
+
*
|
|
27
|
+
* AdonisJS mail addresses can be:
|
|
28
|
+
* - A string: `"user@example.com"`
|
|
29
|
+
* - An object: `{ address: "user@example.com", name: "User" }`
|
|
30
|
+
* - An array of strings or objects
|
|
31
|
+
*/
|
|
32
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
33
|
+
loadRecords(records: EmailRecord[]): void;
|
|
34
|
+
private extractAddresses;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=email_collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/email_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,QAAQ,CAAmD;gBAEvD,SAAS,GAAE,MAAY;IAI7B,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDxC,IAAI,IAAI,IAAI;IAUZ,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMvC,aAAa,IAAI,MAAM;IAIvB,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,WAAW;IAsBnB;;;;;;;OAOG;IACH,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;IAMzC,OAAO,CAAC,gBAAgB;CAYzB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { RingBuffer } from './ring_buffer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Listens to AdonisJS mail events and stores captured emails in a ring buffer.
|
|
4
|
+
*
|
|
5
|
+
* Events:
|
|
6
|
+
* - `mail:sending` — email about to be sent
|
|
7
|
+
* - `mail:sent` — email successfully sent (updates matching 'sending' record)
|
|
8
|
+
* - `mail:queued` — email queued for later delivery
|
|
9
|
+
* - `queued:mail:error` — queued email failed
|
|
10
|
+
*/
|
|
11
|
+
export class EmailCollector {
|
|
12
|
+
buffer;
|
|
13
|
+
emitter = null;
|
|
14
|
+
handlers = [];
|
|
15
|
+
constructor(maxEmails = 100) {
|
|
16
|
+
this.buffer = new RingBuffer(maxEmails);
|
|
17
|
+
}
|
|
18
|
+
async start(emitter) {
|
|
19
|
+
if (!emitter || typeof emitter.on !== 'function')
|
|
20
|
+
return;
|
|
21
|
+
this.emitter = emitter;
|
|
22
|
+
const onSending = (data) => {
|
|
23
|
+
const msg = data?.message || data;
|
|
24
|
+
const record = this.buildRecord(msg, 'sending', data);
|
|
25
|
+
this.buffer.push(record);
|
|
26
|
+
};
|
|
27
|
+
const onSent = (data) => {
|
|
28
|
+
const msg = data?.message || data;
|
|
29
|
+
const to = this.extractAddresses(msg?.to);
|
|
30
|
+
const subject = msg?.subject || '';
|
|
31
|
+
// Try to find the matching 'sending' record and update it
|
|
32
|
+
const all = this.buffer.toArray();
|
|
33
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
34
|
+
const rec = all[i];
|
|
35
|
+
if (rec.status === 'sending' && rec.to === to && rec.subject === subject) {
|
|
36
|
+
rec.status = 'sent';
|
|
37
|
+
rec.messageId = data?.response?.messageId || data?.messageId || null;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// No matching 'sending' record — insert a new 'sent' record
|
|
42
|
+
const record = this.buildRecord(msg, 'sent', data);
|
|
43
|
+
record.messageId = data?.response?.messageId || data?.messageId || null;
|
|
44
|
+
this.buffer.push(record);
|
|
45
|
+
};
|
|
46
|
+
const onQueued = (data) => {
|
|
47
|
+
const msg = data?.message || data;
|
|
48
|
+
const record = this.buildRecord(msg, 'queued', data);
|
|
49
|
+
this.buffer.push(record);
|
|
50
|
+
};
|
|
51
|
+
const onQueuedError = (data) => {
|
|
52
|
+
const msg = data?.message || data;
|
|
53
|
+
const record = this.buildRecord(msg, 'failed', data);
|
|
54
|
+
this.buffer.push(record);
|
|
55
|
+
};
|
|
56
|
+
this.handlers = [
|
|
57
|
+
{ event: 'mail:sending', fn: onSending },
|
|
58
|
+
{ event: 'mail:sent', fn: onSent },
|
|
59
|
+
{ event: 'mail:queued', fn: onQueued },
|
|
60
|
+
{ event: 'queued:mail:error', fn: onQueuedError },
|
|
61
|
+
];
|
|
62
|
+
for (const h of this.handlers) {
|
|
63
|
+
emitter.on(h.event, h.fn);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
stop() {
|
|
67
|
+
if (this.emitter && typeof this.emitter.off === 'function') {
|
|
68
|
+
for (const h of this.handlers) {
|
|
69
|
+
this.emitter.off(h.event, h.fn);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.handlers = [];
|
|
73
|
+
this.emitter = null;
|
|
74
|
+
}
|
|
75
|
+
getEmails() {
|
|
76
|
+
return this.buffer.toArray();
|
|
77
|
+
}
|
|
78
|
+
getLatest(n = 100) {
|
|
79
|
+
return this.buffer.latest(n);
|
|
80
|
+
}
|
|
81
|
+
getEmailHtml(id) {
|
|
82
|
+
const all = this.buffer.toArray();
|
|
83
|
+
const record = all.find((r) => r.id === id);
|
|
84
|
+
return record?.html ?? null;
|
|
85
|
+
}
|
|
86
|
+
getTotalCount() {
|
|
87
|
+
return this.buffer.size();
|
|
88
|
+
}
|
|
89
|
+
clear() {
|
|
90
|
+
this.buffer.clear();
|
|
91
|
+
}
|
|
92
|
+
buildRecord(msg, status, data) {
|
|
93
|
+
return {
|
|
94
|
+
id: this.buffer.getNextId(),
|
|
95
|
+
from: this.extractAddresses(msg?.from) || 'unknown',
|
|
96
|
+
to: this.extractAddresses(msg?.to) || 'unknown',
|
|
97
|
+
cc: this.extractAddresses(msg?.cc) || null,
|
|
98
|
+
bcc: this.extractAddresses(msg?.bcc) || null,
|
|
99
|
+
subject: msg?.subject || '(no subject)',
|
|
100
|
+
html: msg?.html || null,
|
|
101
|
+
text: msg?.text || null,
|
|
102
|
+
mailer: data?.mailerName || data?.mailer || 'unknown',
|
|
103
|
+
status,
|
|
104
|
+
messageId: null,
|
|
105
|
+
attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalize various address formats to a comma-separated string.
|
|
111
|
+
*
|
|
112
|
+
* AdonisJS mail addresses can be:
|
|
113
|
+
* - A string: `"user@example.com"`
|
|
114
|
+
* - An object: `{ address: "user@example.com", name: "User" }`
|
|
115
|
+
* - An array of strings or objects
|
|
116
|
+
*/
|
|
117
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
118
|
+
loadRecords(records) {
|
|
119
|
+
this.buffer.load(records);
|
|
120
|
+
const maxId = records.reduce((m, r) => Math.max(m, r.id), 0);
|
|
121
|
+
this.buffer.setNextId(maxId + 1);
|
|
122
|
+
}
|
|
123
|
+
extractAddresses(value) {
|
|
124
|
+
if (!value)
|
|
125
|
+
return '';
|
|
126
|
+
if (typeof value === 'string')
|
|
127
|
+
return value;
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
return value
|
|
130
|
+
.map((v) => (typeof v === 'string' ? v : v?.address || ''))
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(', ');
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === 'object' && value.address)
|
|
135
|
+
return value.address;
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -16,5 +16,7 @@ export declare class EventCollector {
|
|
|
16
16
|
getLatest(n?: number): EventRecord[];
|
|
17
17
|
getTotalCount(): number;
|
|
18
18
|
clear(): void;
|
|
19
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
20
|
+
loadRecords(records: EventRecord[]): void;
|
|
19
21
|
}
|
|
20
22
|
//# sourceMappingURL=event_collector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,OAAO,CAAa;gBAEhB,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,OAAO,CAAa;gBAEhB,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI;IA+BzB,IAAI,IAAI,IAAI;IAQZ,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;IAapB,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,aAAa,IAAI,MAAM;IAIvB,KAAK,IAAI,IAAI;IAIb,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
|
|
@@ -19,8 +19,11 @@ export class EventCollector {
|
|
|
19
19
|
emitter.emit = function (event, data) {
|
|
20
20
|
// Resolve event name: class-based events use the class name, string events are used as-is
|
|
21
21
|
const eventName = typeof event === "string" ? event : event?.name || "unknown";
|
|
22
|
-
// Skip internal/noisy events
|
|
23
|
-
if (!eventName.startsWith("__") &&
|
|
22
|
+
// Skip internal/noisy events and mail events (handled by EmailCollector)
|
|
23
|
+
if (!eventName.startsWith("__") &&
|
|
24
|
+
eventName !== "db:query" &&
|
|
25
|
+
!eventName.startsWith("mail:") &&
|
|
26
|
+
eventName !== "queued:mail:error") {
|
|
24
27
|
const record = {
|
|
25
28
|
id: self.buffer.getNextId(),
|
|
26
29
|
event: eventName,
|
|
@@ -83,4 +86,10 @@ export class EventCollector {
|
|
|
83
86
|
clear() {
|
|
84
87
|
this.buffer.clear();
|
|
85
88
|
}
|
|
89
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
90
|
+
loadRecords(records) {
|
|
91
|
+
this.buffer.load(records);
|
|
92
|
+
const maxId = records.reduce((m, r) => Math.max(m, r.id), 0);
|
|
93
|
+
this.buffer.setNextId(maxId + 1);
|
|
94
|
+
}
|
|
86
95
|
}
|
|
@@ -23,5 +23,7 @@ export declare class QueryCollector {
|
|
|
23
23
|
};
|
|
24
24
|
getTotalCount(): number;
|
|
25
25
|
clear(): void;
|
|
26
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
27
|
+
loadRecords(records: QueryRecord[]): void;
|
|
26
28
|
}
|
|
27
29
|
//# sourceMappingURL=query_collector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAsC;gBAEzC,UAAU,GAAE,MAAY,EAAE,eAAe,GAAE,MAAY;IAK7D,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BxC,IAAI,IAAI,IAAI;IAYZ,UAAU,IAAI,WAAW,EAAE;IAI3B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,UAAU;;;;;;IA0BV,aAAa,IAAI,MAAM;IAIvB,KAAK,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAsC;gBAEzC,UAAU,GAAE,MAAY,EAAE,eAAe,GAAE,MAAY;IAK7D,KAAK,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BxC,IAAI,IAAI,IAAI;IAYZ,UAAU,IAAI,WAAW,EAAE;IAI3B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,UAAU;;;;;;IA0BV,aAAa,IAAI,MAAM;IAIvB,KAAK,IAAI,IAAI;IAIb,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
|
|
@@ -77,4 +77,10 @@ export class QueryCollector {
|
|
|
77
77
|
clear() {
|
|
78
78
|
this.buffer.clear();
|
|
79
79
|
}
|
|
80
|
+
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
81
|
+
loadRecords(records) {
|
|
82
|
+
this.buffer.load(records);
|
|
83
|
+
const maxId = records.reduce((m, r) => Math.max(m, r.id), 0);
|
|
84
|
+
this.buffer.setNextId(maxId + 1);
|
|
85
|
+
}
|
|
80
86
|
}
|
|
@@ -17,5 +17,9 @@ export declare class RingBuffer<T> {
|
|
|
17
17
|
getNextId(): number;
|
|
18
18
|
size(): number;
|
|
19
19
|
clear(): void;
|
|
20
|
+
/** Bulk-load items (e.g. from disk). Pushes each in order, respecting capacity. */
|
|
21
|
+
load(items: T[]): void;
|
|
22
|
+
/** Restore the auto-increment counter (e.g. after loading persisted data). */
|
|
23
|
+
setNextId(id: number): void;
|
|
20
24
|
}
|
|
21
25
|
//# sourceMappingURL=ring_buffer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAMX,OAAO,CAAC,QAAQ;IAL5B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAa;gBAEP,QAAQ,EAAE,MAAM;IAIpC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IAQnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;IAKtB,SAAS,IAAI,MAAM;IAInB,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAMX,OAAO,CAAC,QAAQ;IAL5B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAa;gBAEP,QAAQ,EAAE,MAAM;IAIpC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IAQnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;IAKtB,SAAS,IAAI,MAAM;IAInB,IAAI,IAAI,MAAM;IAId,KAAK,IAAI,IAAI;IAMb,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG5B"}
|
|
@@ -47,4 +47,14 @@ export class RingBuffer {
|
|
|
47
47
|
this.head = 0;
|
|
48
48
|
this.count = 0;
|
|
49
49
|
}
|
|
50
|
+
/** Bulk-load items (e.g. from disk). Pushes each in order, respecting capacity. */
|
|
51
|
+
load(items) {
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
this.push(item);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Restore the auto-increment counter (e.g. after loading persisted data). */
|
|
57
|
+
setNextId(id) {
|
|
58
|
+
this.nextId = id;
|
|
59
|
+
}
|
|
50
60
|
}
|
|
@@ -40,6 +40,40 @@ export interface EventRecord {
|
|
|
40
40
|
/** Unix timestamp in **milliseconds** when the event was emitted. */
|
|
41
41
|
timestamp: number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A captured email sent via AdonisJS mail.
|
|
45
|
+
*
|
|
46
|
+
* Stored in a {@link RingBuffer} by the {@link EmailCollector} and
|
|
47
|
+
* served via the debug API endpoint.
|
|
48
|
+
*/
|
|
49
|
+
export interface EmailRecord {
|
|
50
|
+
/** Auto-incrementing sequence number. */
|
|
51
|
+
id: number;
|
|
52
|
+
/** Sender address (e.g. `"noreply@example.com"`). */
|
|
53
|
+
from: string;
|
|
54
|
+
/** Comma-separated recipient addresses. */
|
|
55
|
+
to: string;
|
|
56
|
+
/** CC recipients, or `null` if none. */
|
|
57
|
+
cc: string | null;
|
|
58
|
+
/** BCC recipients, or `null` if none. */
|
|
59
|
+
bcc: string | null;
|
|
60
|
+
/** Email subject line. */
|
|
61
|
+
subject: string;
|
|
62
|
+
/** Full HTML body for iframe preview, or `null`. */
|
|
63
|
+
html: string | null;
|
|
64
|
+
/** Plain-text body, or `null`. */
|
|
65
|
+
text: string | null;
|
|
66
|
+
/** Mailer name (e.g. `"smtp"`, `"ses"`). */
|
|
67
|
+
mailer: string;
|
|
68
|
+
/** Current delivery status. */
|
|
69
|
+
status: 'sending' | 'sent' | 'queued' | 'failed';
|
|
70
|
+
/** Message ID from the mail transport response, or `null`. */
|
|
71
|
+
messageId: string | null;
|
|
72
|
+
/** Number of file attachments. */
|
|
73
|
+
attachmentCount: number;
|
|
74
|
+
/** Unix timestamp in **milliseconds** when the email was captured. */
|
|
75
|
+
timestamp: number;
|
|
76
|
+
}
|
|
43
77
|
/**
|
|
44
78
|
* A registered route extracted from the AdonisJS router.
|
|
45
79
|
*
|
|
@@ -75,8 +109,12 @@ export interface DevToolbarConfig {
|
|
|
75
109
|
maxQueries: number;
|
|
76
110
|
/** Maximum events to buffer. */
|
|
77
111
|
maxEvents: number;
|
|
112
|
+
/** Maximum emails to buffer. */
|
|
113
|
+
maxEmails: number;
|
|
78
114
|
/** Slow query highlight threshold in **milliseconds**. */
|
|
79
115
|
slowQueryThresholdMs: number;
|
|
116
|
+
/** Whether to persist debug data to disk across restarts. */
|
|
117
|
+
persistDebugData: boolean;
|
|
80
118
|
}
|
|
81
119
|
/**
|
|
82
120
|
* Color names available for the `badge` column format.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/debug/types.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAA;IAEV,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAA;IAEX,yDAAyD;IACzD,QAAQ,EAAE,GAAG,EAAE,CAAA;IAEf,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAA;IAEhB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IAEd,wEAAwE;IACxE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAEpB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAA;IAElB,mDAAmD;IACnD,aAAa,EAAE,OAAO,CAAA;IAEtB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAA;IAEV,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IAEb,oEAAoE;IACpE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAEnB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAA;IAEd,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAA;IAEf,oDAAoD;IACpD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAEnB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IAEf;;;;OAIG;IACH,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAMD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAA;IAEhB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAA;IAElB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAA;IAEjB,0DAA0D;IAC1D,oBAAoB,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/debug/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -310,6 +310,56 @@
|
|
|
310
310
|
.ss-dbg-badge-purple { background: #2e1065; color: #c084fc; }
|
|
311
311
|
.ss-dbg-badge-muted { background: #262626; color: #737373; }
|
|
312
312
|
|
|
313
|
+
/* Email preview overlay */
|
|
314
|
+
.ss-dbg-email-preview {
|
|
315
|
+
position: absolute;
|
|
316
|
+
inset: 0;
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
background: #0f0f0f;
|
|
320
|
+
z-index: 10;
|
|
321
|
+
}
|
|
322
|
+
.ss-dbg-email-preview-header {
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: flex-start;
|
|
325
|
+
justify-content: space-between;
|
|
326
|
+
gap: 12px;
|
|
327
|
+
padding: 10px 12px;
|
|
328
|
+
border-bottom: 1px solid #262626;
|
|
329
|
+
background: #141414;
|
|
330
|
+
flex-shrink: 0;
|
|
331
|
+
}
|
|
332
|
+
.ss-dbg-email-preview-meta {
|
|
333
|
+
font-size: 11px;
|
|
334
|
+
color: #a3a3a3;
|
|
335
|
+
line-height: 1.6;
|
|
336
|
+
overflow: hidden;
|
|
337
|
+
}
|
|
338
|
+
.ss-dbg-email-preview-meta strong { color: #d4d4d4; font-weight: 600; }
|
|
339
|
+
.ss-dbg-email-iframe {
|
|
340
|
+
flex: 1;
|
|
341
|
+
border: none;
|
|
342
|
+
background: #fff;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Email row */
|
|
346
|
+
.ss-dbg-email-row { cursor: pointer; }
|
|
347
|
+
.ss-dbg-email-row:hover td { background: rgba(52, 211, 153, 0.06) !important; }
|
|
348
|
+
|
|
349
|
+
/* Email status badges */
|
|
350
|
+
.ss-dbg-email-status {
|
|
351
|
+
display: inline-block;
|
|
352
|
+
padding: 1px 6px;
|
|
353
|
+
border-radius: 3px;
|
|
354
|
+
font-size: 10px;
|
|
355
|
+
font-weight: 600;
|
|
356
|
+
text-transform: uppercase;
|
|
357
|
+
}
|
|
358
|
+
.ss-dbg-email-status-sent { background: #064e3b; color: #34d399; }
|
|
359
|
+
.ss-dbg-email-status-sending { background: #422006; color: #fbbf24; }
|
|
360
|
+
.ss-dbg-email-status-queued { background: #1e3a5f; color: #60a5fa; }
|
|
361
|
+
.ss-dbg-email-status-failed { background: #450a0a; color: #f87171; }
|
|
362
|
+
|
|
313
363
|
/* Filterable cell */
|
|
314
364
|
.ss-dbg-filterable:hover { background: rgba(52, 211, 153, 0.08); }
|
|
315
365
|
|
|
@@ -189,6 +189,7 @@
|
|
|
189
189
|
else if (name === 'events') fetchEvents();
|
|
190
190
|
else if (name === 'routes' && !fetched.routes) fetchRoutes();
|
|
191
191
|
else if (name === 'logs') fetchLogs();
|
|
192
|
+
else if (name === 'emails') fetchEmails();
|
|
192
193
|
else {
|
|
193
194
|
const cp = customPanes.find((p) => p.id === name);
|
|
194
195
|
if (cp) {
|
|
@@ -580,6 +581,122 @@
|
|
|
580
581
|
});
|
|
581
582
|
});
|
|
582
583
|
|
|
584
|
+
// ── Emails Tab ─────────────────────────────────────────────────
|
|
585
|
+
const emailSearchInput = document.getElementById('ss-dbg-search-emails');
|
|
586
|
+
const emailSummaryEl = document.getElementById('ss-dbg-emails-summary');
|
|
587
|
+
const emailBodyEl = document.getElementById('ss-dbg-emails-body');
|
|
588
|
+
const emailClearBtn = document.getElementById('ss-dbg-emails-clear');
|
|
589
|
+
const emailPreviewEl = document.getElementById('ss-dbg-email-preview');
|
|
590
|
+
const emailPreviewMeta = document.getElementById('ss-dbg-email-preview-meta');
|
|
591
|
+
const emailPreviewClose = document.getElementById('ss-dbg-email-preview-close');
|
|
592
|
+
const emailIframe = document.getElementById('ss-dbg-email-iframe');
|
|
593
|
+
let cachedEmails = { emails: [], total: 0 };
|
|
594
|
+
|
|
595
|
+
const fetchEmails = () => {
|
|
596
|
+
fetchJSON(BASE + '/emails')
|
|
597
|
+
.then((data) => {
|
|
598
|
+
cachedEmails = data;
|
|
599
|
+
renderEmails();
|
|
600
|
+
})
|
|
601
|
+
.catch(() => {
|
|
602
|
+
if (emailBodyEl) emailBodyEl.innerHTML = '<div class="ss-dbg-empty">Failed to load emails</div>';
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const renderEmails = () => {
|
|
607
|
+
if (!emailBodyEl) return;
|
|
608
|
+
const filter = (emailSearchInput ? emailSearchInput.value : '').toLowerCase();
|
|
609
|
+
const emails = cachedEmails.emails || [];
|
|
610
|
+
|
|
611
|
+
if (emailSummaryEl) {
|
|
612
|
+
emailSummaryEl.textContent = cachedEmails.total + ' emails';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let filtered = emails;
|
|
616
|
+
if (filter) {
|
|
617
|
+
filtered = emails.filter((e) =>
|
|
618
|
+
(e.from || '').toLowerCase().indexOf(filter) !== -1
|
|
619
|
+
|| (e.to || '').toLowerCase().indexOf(filter) !== -1
|
|
620
|
+
|| (e.subject || '').toLowerCase().indexOf(filter) !== -1
|
|
621
|
+
|| (e.mailer || '').toLowerCase().indexOf(filter) !== -1
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (filtered.length === 0) {
|
|
626
|
+
emailBodyEl.innerHTML = '<div class="ss-dbg-empty">' + (filter ? 'No matching emails' : 'No emails captured yet') + '</div>';
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let html = '<table class="ss-dbg-table"><thead><tr>'
|
|
631
|
+
+ '<th style="width:40px">#</th>'
|
|
632
|
+
+ '<th style="width:160px">From</th>'
|
|
633
|
+
+ '<th style="width:160px">To</th>'
|
|
634
|
+
+ '<th>Subject</th>'
|
|
635
|
+
+ '<th style="width:60px">Status</th>'
|
|
636
|
+
+ '<th style="width:60px">Mailer</th>'
|
|
637
|
+
+ '<th style="width:30px" title="Attachments">📎</th>'
|
|
638
|
+
+ '<th style="width:70px">Time</th>'
|
|
639
|
+
+ '</tr></thead><tbody>';
|
|
640
|
+
|
|
641
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
642
|
+
const e = filtered[i];
|
|
643
|
+
html += '<tr class="ss-dbg-email-row" data-email-id="' + e.id + '">'
|
|
644
|
+
+ '<td style="color:#525252">' + e.id + '</td>'
|
|
645
|
+
+ '<td style="color:#a3a3a3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(e.from) + '">' + esc(e.from) + '</td>'
|
|
646
|
+
+ '<td style="color:#a3a3a3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="' + esc(e.to) + '">' + esc(e.to) + '</td>'
|
|
647
|
+
+ '<td style="color:#93c5fd;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(e.subject) + '</td>'
|
|
648
|
+
+ '<td><span class="ss-dbg-email-status ss-dbg-email-status-' + esc(e.status) + '">' + esc(e.status) + '</span></td>'
|
|
649
|
+
+ '<td style="color:#737373">' + esc(e.mailer) + '</td>'
|
|
650
|
+
+ '<td style="color:#525252;text-align:center">' + (e.attachmentCount > 0 ? e.attachmentCount : '-') + '</td>'
|
|
651
|
+
+ '<td class="ss-dbg-event-time">' + timeAgo(e.timestamp) + '</td>'
|
|
652
|
+
+ '</tr>';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
html += '</tbody></table>';
|
|
656
|
+
emailBodyEl.innerHTML = html;
|
|
657
|
+
|
|
658
|
+
// Click row to open preview
|
|
659
|
+
emailBodyEl.querySelectorAll('.ss-dbg-email-row').forEach((row) => {
|
|
660
|
+
row.addEventListener('click', () => {
|
|
661
|
+
const id = row.getAttribute('data-email-id');
|
|
662
|
+
showEmailPreview(id, filtered);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const showEmailPreview = (id, emails) => {
|
|
668
|
+
if (!emailPreviewEl || !emailIframe || !emailPreviewMeta) return;
|
|
669
|
+
const email = emails.find((e) => String(e.id) === String(id));
|
|
670
|
+
|
|
671
|
+
if (emailPreviewMeta && email) {
|
|
672
|
+
emailPreviewMeta.innerHTML =
|
|
673
|
+
'<strong>Subject:</strong> ' + esc(email.subject)
|
|
674
|
+
+ ' | <strong>From:</strong> ' + esc(email.from)
|
|
675
|
+
+ ' | <strong>To:</strong> ' + esc(email.to)
|
|
676
|
+
+ (email.cc ? ' | <strong>CC:</strong> ' + esc(email.cc) : '')
|
|
677
|
+
+ ' | <strong>Status:</strong> <span class="ss-dbg-email-status ss-dbg-email-status-' + esc(email.status) + '">' + esc(email.status) + '</span>'
|
|
678
|
+
+ ' | <strong>Mailer:</strong> ' + esc(email.mailer);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
emailIframe.src = BASE + '/emails/' + id + '/preview';
|
|
682
|
+
emailPreviewEl.style.display = 'flex';
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
if (emailPreviewClose) {
|
|
686
|
+
emailPreviewClose.addEventListener('click', () => {
|
|
687
|
+
if (emailPreviewEl) emailPreviewEl.style.display = 'none';
|
|
688
|
+
if (emailIframe) emailIframe.src = 'about:blank';
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (emailSearchInput) emailSearchInput.addEventListener('input', renderEmails);
|
|
693
|
+
if (emailClearBtn) {
|
|
694
|
+
emailClearBtn.addEventListener('click', () => {
|
|
695
|
+
cachedEmails = { emails: [], total: 0 };
|
|
696
|
+
renderEmails();
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
583
700
|
// ── Custom panes: fetch, render, bind ───────────────────────────
|
|
584
701
|
const getNestedValue = (obj, path) => {
|
|
585
702
|
const parts = path.split('.');
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<button type="button" class="ss-dbg-tab" data-ss-dbg-tab="events">Events</button>
|
|
5
5
|
<button type="button" class="ss-dbg-tab" data-ss-dbg-tab="routes">Routes</button>
|
|
6
6
|
<button type="button" class="ss-dbg-tab" data-ss-dbg-tab="logs">Logs</button>
|
|
7
|
+
<button type="button" class="ss-dbg-tab" data-ss-dbg-tab="emails">Emails</button>
|
|
7
8
|
@each(pane in customPanes)
|
|
8
9
|
<button type="button" class="ss-dbg-tab" data-ss-dbg-tab="{{ pane.id }}">{{ pane.label }}</button>
|
|
9
10
|
@end
|
|
@@ -48,6 +49,21 @@
|
|
|
48
49
|
</div>
|
|
49
50
|
<div id="ss-dbg-logs-body"><div class="ss-dbg-empty">Loading logs...</div></div>
|
|
50
51
|
</div>
|
|
52
|
+
<div id="ss-dbg-pane-emails" class="ss-dbg-pane" style="position:relative">
|
|
53
|
+
<div class="ss-dbg-search-bar">
|
|
54
|
+
<input type="text" class="ss-dbg-search" id="ss-dbg-search-emails" placeholder="Filter emails by from, to, subject, or mailer..." />
|
|
55
|
+
<span id="ss-dbg-emails-summary" class="ss-dbg-summary"></span>
|
|
56
|
+
<button type="button" class="ss-dbg-btn-clear" id="ss-dbg-emails-clear">Clear</button>
|
|
57
|
+
</div>
|
|
58
|
+
<div id="ss-dbg-emails-body"><div class="ss-dbg-empty">Loading emails...</div></div>
|
|
59
|
+
<div id="ss-dbg-email-preview" class="ss-dbg-email-preview" style="display:none">
|
|
60
|
+
<div class="ss-dbg-email-preview-header">
|
|
61
|
+
<div id="ss-dbg-email-preview-meta" class="ss-dbg-email-preview-meta"></div>
|
|
62
|
+
<button type="button" class="ss-dbg-btn-clear" id="ss-dbg-email-preview-close">Close</button>
|
|
63
|
+
</div>
|
|
64
|
+
<iframe id="ss-dbg-email-iframe" class="ss-dbg-email-iframe" sandbox="allow-same-origin" src="about:blank"></iframe>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
51
67
|
@each(pane in customPanes)
|
|
52
68
|
<div id="ss-dbg-pane-{{ pane.id }}" class="ss-dbg-pane">
|
|
53
69
|
@if(pane.search || pane.clearable)
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export { StatsEngine } from './engine/stats_engine.js';
|
|
|
3
3
|
export { RequestMetrics } from './engine/request_metrics.js';
|
|
4
4
|
export type { MetricCollector } from './collectors/collector.js';
|
|
5
5
|
export type { MetricValue, ServerStats, ServerStatsConfig, LogStats, DevToolbarOptions } from './types.js';
|
|
6
|
-
export type { DebugPane, DebugPaneColumn, DebugPaneFormatType, DebugPaneSearch, BadgeColor, QueryRecord, EventRecord, RouteRecord, DevToolbarConfig, } from './debug/types.js';
|
|
6
|
+
export type { DebugPane, DebugPaneColumn, DebugPaneFormatType, DebugPaneSearch, BadgeColor, QueryRecord, EventRecord, EmailRecord, RouteRecord, DevToolbarConfig, } from './debug/types.js';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC1G,YAAY,EACV,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC1G,YAAY,EACV,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,kBAAkB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,MAAM,CAAC,OAAO,OAAO,mBAAmB;
|
|
1
|
+
{"version":3,"file":"server_stats_provider.d.ts","sourceRoot":"","sources":["../../../src/provider/server_stats_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI/D,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAO1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAN7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAA+C;gBAE3C,GAAG,EAAE,kBAAkB;IAEvC,IAAI;IAoBJ,KAAK;YAoEG,eAAe;IA6CvB,QAAQ;CAuBf"}
|
|
@@ -6,6 +6,8 @@ export default class ServerStatsProvider {
|
|
|
6
6
|
intervalId = null;
|
|
7
7
|
engine = null;
|
|
8
8
|
debugStore = null;
|
|
9
|
+
persistPath = null;
|
|
10
|
+
flushTimer = null;
|
|
9
11
|
constructor(app) {
|
|
10
12
|
this.app = app;
|
|
11
13
|
}
|
|
@@ -45,7 +47,9 @@ export default class ServerStatsProvider {
|
|
|
45
47
|
enabled: true,
|
|
46
48
|
maxQueries: toolbarConfig.maxQueries ?? 500,
|
|
47
49
|
maxEvents: toolbarConfig.maxEvents ?? 200,
|
|
50
|
+
maxEmails: toolbarConfig.maxEmails ?? 100,
|
|
48
51
|
slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
|
|
52
|
+
persistDebugData: toolbarConfig.persistDebugData ?? false,
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
let transmit = null;
|
|
@@ -85,6 +89,11 @@ export default class ServerStatsProvider {
|
|
|
85
89
|
this.debugStore = new DebugStore(toolbarConfig);
|
|
86
90
|
// Bind debug store to container
|
|
87
91
|
this.app.container.singleton("debug.store", () => this.debugStore);
|
|
92
|
+
// Load persisted data before starting collectors
|
|
93
|
+
if (toolbarConfig.persistDebugData) {
|
|
94
|
+
this.persistPath = this.app.makePath("tmp", "debug-data.json");
|
|
95
|
+
await this.debugStore.loadFromDisk(this.persistPath);
|
|
96
|
+
}
|
|
88
97
|
// Get the emitter
|
|
89
98
|
let emitter = null;
|
|
90
99
|
try {
|
|
@@ -102,12 +111,36 @@ export default class ServerStatsProvider {
|
|
|
102
111
|
// Router not available
|
|
103
112
|
}
|
|
104
113
|
await this.debugStore.start(emitter, router);
|
|
114
|
+
// Periodic flush every 30 seconds (handles crashes)
|
|
115
|
+
if (this.persistPath) {
|
|
116
|
+
this.flushTimer = setInterval(async () => {
|
|
117
|
+
try {
|
|
118
|
+
await this.debugStore?.saveToDisk(this.persistPath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Silently ignore flush errors
|
|
122
|
+
}
|
|
123
|
+
}, 30_000);
|
|
124
|
+
}
|
|
105
125
|
}
|
|
106
126
|
async shutdown() {
|
|
107
127
|
if (this.intervalId) {
|
|
108
128
|
clearInterval(this.intervalId);
|
|
109
129
|
this.intervalId = null;
|
|
110
130
|
}
|
|
131
|
+
if (this.flushTimer) {
|
|
132
|
+
clearInterval(this.flushTimer);
|
|
133
|
+
this.flushTimer = null;
|
|
134
|
+
}
|
|
135
|
+
// Save debug data before stopping collectors
|
|
136
|
+
if (this.persistPath && this.debugStore) {
|
|
137
|
+
try {
|
|
138
|
+
await this.debugStore.saveToDisk(this.persistPath);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Silently ignore save errors during shutdown
|
|
142
|
+
}
|
|
143
|
+
}
|
|
111
144
|
this.debugStore?.stop();
|
|
112
145
|
await this.engine?.stop();
|
|
113
146
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -199,6 +199,11 @@ export interface DevToolbarOptions {
|
|
|
199
199
|
* @default 200
|
|
200
200
|
*/
|
|
201
201
|
maxEvents?: number;
|
|
202
|
+
/**
|
|
203
|
+
* Maximum number of captured emails to keep in the ring buffer.
|
|
204
|
+
* @default 100
|
|
205
|
+
*/
|
|
206
|
+
maxEmails?: number;
|
|
202
207
|
/**
|
|
203
208
|
* Queries slower than this threshold (in **milliseconds**) are
|
|
204
209
|
* highlighted in the toolbar.
|
|
@@ -214,6 +219,13 @@ export interface DevToolbarOptions {
|
|
|
214
219
|
* @see {@link DebugPane}
|
|
215
220
|
*/
|
|
216
221
|
panes?: DebugPane[];
|
|
222
|
+
/**
|
|
223
|
+
* Persist debug data (queries, events, emails) to disk so it
|
|
224
|
+
* survives server restarts. Data is saved to `tmp/debug-data.json`.
|
|
225
|
+
*
|
|
226
|
+
* @default false
|
|
227
|
+
*/
|
|
228
|
+
persistDebugData?: boolean;
|
|
217
229
|
}
|
|
218
230
|
/**
|
|
219
231
|
* Top-level configuration for `adonisjs-server-stats`.
|
package/dist/src/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adonisjs-server-stats",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Real-time server monitoring for AdonisJS v6 applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"author": "",
|
|
87
87
|
"license": "MIT",
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@adonisjs/core": "^6.0.0"
|
|
89
|
+
"@adonisjs/core": "^6.0.0 || ^7.0.0-next || ^7.0.0"
|
|
90
90
|
},
|
|
91
91
|
"peerDependenciesMeta": {
|
|
92
92
|
"@adonisjs/lucid": {
|