@zerotal/monitor 1.8.1 → 1.10.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,43 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.10.0] — 2026-08-30
12
+
13
+ ### Fixed
14
+
15
+ - **`MonitorStore` options no longer overwrite their own defaults with `undefined`.**
16
+ The constructor applied `?? 100`-style defaults and then spread `...opts` **after**
17
+ them, and object spread copies own properties even when their value is `undefined`.
18
+ So `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config put
19
+ `undefined` straight back over the 7, and `prune()` computed a cutoff of
20
+ `Date.now() - undefined * DAY_MS` — `NaN`, which prunes nothing and reports nothing.
21
+ Spreading last is what made every `??` in that constructor decorative.
22
+
23
+ The caller's options now go first and the defaults fill the gaps, which is the
24
+ ordering `Socket`'s constructor already documents at length for the same reason.
25
+
26
+ ## [1.9.0] — 2026-08-29
27
+
28
+ ### Added
29
+
30
+ - **The snapshot shape is documented.** **Export JSON** hands you a `MonitorSnapshot` — the whole
31
+ panel for a range, as data — and none of the forty-odd row types it is built from was named
32
+ anywhere. `docs/monitor.md` now maps every field to its row type, which is what you need to
33
+ post one somewhere, diff two of them, or drive an alerting integration off it rather than off
34
+ the built-in thresholds.
35
+
36
+ - **Section contributions can be typed.** `MonitorSection`, `MonitorSectionData`, `MonitorStat`,
37
+ `MonitorTable`, `MonitorTableColumn` and `MonitorRow` are documented. Declaring the host's
38
+ shape locally stays the recommended path — it is what keeps a contributing package free of any
39
+ dependency on the monitor — but a large `resolve` is better served by a typo in a column key
40
+ being a compile error than a blank cell.
41
+
42
+ ### Changed
43
+
44
+ - **INTERNAL: `MonitorLayout`, `MonitorPage` and `MonitorPanelHost` are marked `@internal`.**
45
+ The panel's own shell. Still exported, still working; a contributor pushes a section into the
46
+ host binding rather than constructing any of them.
47
+
11
48
  ## [1.6.0] — 2026-08-15
12
49
 
13
50
  ### Fixed
package/api-surface.md CHANGED
@@ -286,37 +286,32 @@ interface ModelStat = {
286
286
  }
287
287
 
288
288
  interface MonitorConfigShape = {
289
- accent?: string
290
- alertCooldownMs?: number
291
- alertThresholds?: AlertThresholds
292
- alertWebhook?: string
293
- alerts?: boolean
294
- apdexTargetMs?: number
295
- auth?: (user: unknown) => boolean | Promise<boolean>
296
- capturePayloads?: boolean
297
- deploy?: string
298
- metrics?: boolean
299
- metricsPath?: string
300
- path?: string
301
- payloadMaxBytes?: number
302
- record?: boolean
303
- refreshMs?: number
304
- region?: string
305
- retentionDays?: number
306
- retentionMode?: RetentionMode
307
- sections?: Record<string, boolean>
308
- slowQueryMs?: number
309
- slowRequestMs?: number
310
- snapshotCacheMs?: number
311
- storage?: string
312
- subtitle?: string
313
- title?: string
314
- zerotalVersion?: string
315
- }
316
-
317
- interface MonitorPanelHost = {
318
- enabled: (id: string) => boolean
319
- section: (section: MonitorSection) => void
289
+ accent?: string | undefined
290
+ alertCooldownMs?: number | undefined
291
+ alertThresholds?: AlertThresholds | undefined
292
+ alertWebhook?: string | undefined
293
+ alerts?: boolean | undefined
294
+ apdexTargetMs?: number | undefined
295
+ auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined
296
+ capturePayloads?: boolean | undefined
297
+ deploy?: string | undefined
298
+ metrics?: boolean | undefined
299
+ metricsPath?: string | undefined
300
+ path?: string | undefined
301
+ payloadMaxBytes?: number | undefined
302
+ record?: boolean | undefined
303
+ refreshMs?: number | undefined
304
+ region?: string | undefined
305
+ retentionDays?: number | undefined
306
+ retentionMode?: RetentionMode | undefined
307
+ sections?: Record<string, boolean> | undefined
308
+ slowQueryMs?: number | undefined
309
+ slowRequestMs?: number | undefined
310
+ snapshotCacheMs?: number | undefined
311
+ storage?: string | undefined
312
+ subtitle?: string | undefined
313
+ title?: string | undefined
314
+ zerotalVersion?: string | undefined
320
315
  }
321
316
 
322
317
  interface MonitorSection = {
@@ -391,16 +386,16 @@ interface MonitorStat = {
391
386
  }
392
387
 
393
388
  interface MonitorStoreOptions = {
394
- apdexTargetMs?: number
395
- deploy?: string
396
- region?: string
397
- retentionDays?: number
398
- retentionMode?: RetentionMode
399
- slowQueryMs?: number
400
- slowRequestMs?: number
401
- snapshotCacheMs?: number
402
- storage?: string
403
- zerotalVersion?: string
389
+ apdexTargetMs?: number | undefined
390
+ deploy?: string | undefined
391
+ region?: string | undefined
392
+ retentionDays?: number | undefined
393
+ retentionMode?: RetentionMode | undefined
394
+ slowQueryMs?: number | undefined
395
+ slowRequestMs?: number | undefined
396
+ snapshotCacheMs?: number | undefined
397
+ storage?: string | undefined
398
+ zerotalVersion?: string | undefined
404
399
  }
405
400
 
406
401
  interface MonitorTable = {
@@ -530,30 +525,30 @@ interface RequestSpan = {
530
525
  interface ResolvedMonitorConfig = {
531
526
  accent: string
532
527
  alertCooldownMs: number
533
- alertThresholds?: AlertThresholds
534
- alertWebhook?: string
528
+ alertThresholds?: AlertThresholds | undefined
529
+ alertWebhook?: string | undefined
535
530
  alerts: boolean
536
531
  apdexTargetMs: number
537
532
  auth: (user: unknown) => boolean | Promise<boolean>
538
533
  capturePayloads: boolean
539
- deploy?: string
534
+ deploy?: string | undefined
540
535
  metrics: boolean
541
536
  metricsPath: string
542
537
  path: string
543
538
  payloadMaxBytes: number
544
539
  record: boolean
545
540
  refreshMs: number
546
- region?: string
541
+ region?: string | undefined
547
542
  retentionDays: number
548
543
  retentionMode: RetentionMode
549
- sections?: Record<string, boolean>
544
+ sections?: Record<string, boolean> | undefined
550
545
  slowQueryMs: number
551
546
  slowRequestMs: number
552
547
  snapshotCacheMs: number
553
548
  storage: string
554
549
  subtitle: string
555
550
  title: string
556
- zerotalVersion?: string
551
+ zerotalVersion?: string | undefined
557
552
  }
558
553
 
559
554
  interface RouteDetail = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/monitor",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -30,14 +30,14 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@zerotal/core": "1.8.1",
34
- "@zerotal/flow-ui": "1.8.1",
35
- "@zerotal/flow": "1.8.1"
33
+ "@zerotal/core": "1.10.0",
34
+ "@zerotal/flow-ui": "1.10.0",
35
+ "@zerotal/flow": "1.10.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@zerotal/queue": "1.8.1",
39
- "@zerotal/scheduler": "1.8.1",
40
- "@zerotal/telemetry": "1.8.1"
38
+ "@zerotal/queue": "1.10.0",
39
+ "@zerotal/scheduler": "1.10.0",
40
+ "@zerotal/telemetry": "1.10.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@zerotal/queue": "^1.0.0",
@@ -12,6 +12,7 @@
12
12
  * A retention policy (`prune`) deletes or archives data past the configured
13
13
  * window, and `wipe` clears everything on demand from the panel.
14
14
  */
15
+ import type { Resolved } from "@zerotal/core";
15
16
  import { percentile } from "./store/RingBuffer.ts";
16
17
  import { MonitorDb } from "./store/MonitorDb.ts";
17
18
  import {
@@ -107,25 +108,25 @@ interface ReqSample {
107
108
  }
108
109
 
109
110
  export interface MonitorStoreOptions {
110
- apdexTargetMs?: number;
111
- slowQueryMs?: number;
112
- zerotalVersion?: string;
113
- region?: string;
114
- deploy?: string;
111
+ apdexTargetMs?: number | undefined;
112
+ slowQueryMs?: number | undefined;
113
+ zerotalVersion?: string | undefined;
114
+ region?: string | undefined;
115
+ deploy?: string | undefined;
115
116
  /** SQLite path. `:memory:` (default) for tests; a file path for persistence. */
116
- storage?: string;
117
+ storage?: string | undefined;
117
118
  /** Days of history to keep before pruning. Default: 7. */
118
- retentionDays?: number;
119
+ retentionDays?: number | undefined;
119
120
  /** What to do with data past retention: delete it or move it to the archive. Default: `delete`. */
120
- retentionMode?: RetentionMode;
121
+ retentionMode?: RetentionMode | undefined;
121
122
  /** Requests at/above this many ms count as "slow". Default: 1000. */
122
- slowRequestMs?: number;
123
+ slowRequestMs?: number | undefined;
123
124
  /**
124
125
  * Cache built snapshots for this many ms, keyed by range. De-dupes overlapping
125
126
  * builds (panel poll + Prometheus scrape + alert loop all hit "live"). Mutating
126
127
  * actions invalidate it. Default: 0 (off) — the provider enables it in production.
127
128
  */
128
- snapshotCacheMs?: number;
129
+ snapshotCacheMs?: number | undefined;
129
130
  }
130
131
 
131
132
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -139,22 +140,32 @@ export class MonitorStore {
139
140
  private readonly _snapTtlMs: number;
140
141
  private readonly _snapCache = new Map<MonitorRange, { at: number; snap: MonitorSnapshot }>();
141
142
 
142
- private readonly _opts: Required<
143
+ private readonly _opts: Resolved<
143
144
  Pick<
144
145
  MonitorStoreOptions,
145
146
  "apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
146
147
  >
147
148
  > &
148
- MonitorStoreOptions;
149
+ Omit<
150
+ MonitorStoreOptions,
151
+ "apdexTargetMs" | "slowQueryMs" | "slowRequestMs" | "retentionDays" | "retentionMode"
152
+ >;
149
153
 
150
154
  constructor(opts: MonitorStoreOptions = {}) {
155
+ // The caller's options go FIRST, then the defaults fill the gaps — the same
156
+ // ordering, for the same reason, as `Socket`'s constructor documents at length.
157
+ // Object spread copies own properties even when their value is `undefined`, so
158
+ // `new MonitorStore({ retentionDays: cfg.retentionDays })` with an unset config
159
+ // put `undefined` straight back over the 7 and `prune()` then computed a cutoff
160
+ // of `Date.now() - undefined * DAY_MS` — NaN, which deletes nothing and reports
161
+ // nothing. Spreading last is what made the `??` above decorative.
151
162
  this._opts = {
163
+ ...opts,
152
164
  apdexTargetMs: opts.apdexTargetMs ?? 100,
153
165
  slowQueryMs: opts.slowQueryMs ?? 100,
154
166
  slowRequestMs: opts.slowRequestMs ?? 1000,
155
167
  retentionDays: opts.retentionDays ?? 7,
156
168
  retentionMode: opts.retentionMode ?? "delete",
157
- ...opts,
158
169
  };
159
170
  this._snapTtlMs = Math.max(0, opts.snapshotCacheMs ?? 0);
160
171
  this._db = new MonitorDb(opts.storage ?? ":memory:");
package/src/config.ts CHANGED
@@ -13,91 +13,91 @@ import type { AlertThresholds } from "./alerting.ts";
13
13
 
14
14
  export interface MonitorConfigShape {
15
15
  /** URL prefix the panel mounts at. Default: `/monitor`. */
16
- path?: string;
16
+ path?: string | undefined;
17
17
  /** Browser tab title / sidebar heading. Default: `Super Panel`. */
18
- title?: string;
18
+ title?: string | undefined;
19
19
  /** Sub-label under the title. Default: `Zerotal Ops`. */
20
- subtitle?: string;
20
+ subtitle?: string | undefined;
21
21
  /**
22
22
  * Authorization gate. Receives the authenticated user (or undefined) and
23
23
  * must return true to allow access. Default: allow only outside production.
24
24
  */
25
- auth?: (user: unknown) => boolean | Promise<boolean>;
25
+ auth?: ((user: unknown) => boolean | Promise<boolean>) | undefined;
26
26
  /** Install the recorder middleware to capture live request data. Default: true. */
27
- record?: boolean;
27
+ record?: boolean | undefined;
28
28
  /** Auto-refresh cadence in milliseconds while "Live" is on. Default: 3000. */
29
- refreshMs?: number;
29
+ refreshMs?: number | undefined;
30
30
  /** Apdex satisfaction threshold (T) in ms. Default: 100. */
31
- apdexTargetMs?: number;
31
+ apdexTargetMs?: number | undefined;
32
32
  /** Queries at/above this many ms count as "slow". Default: 100. */
33
- slowQueryMs?: number;
33
+ slowQueryMs?: number | undefined;
34
34
  /** Requests at/above this many ms count as "slow" (Slow Requests widget). Default: 1000. */
35
- slowRequestMs?: number;
35
+ slowRequestMs?: number | undefined;
36
36
  /**
37
37
  * Capture request/response headers and bodies on each request (Telescope-style),
38
38
  * shown in the request trace. Off by default — it buffers bodies and is
39
39
  * privacy-sensitive. Sensitive headers (authorization/cookie) and body keys
40
40
  * (password/token/secret/…) are redacted automatically.
41
41
  */
42
- capturePayloads?: boolean;
42
+ capturePayloads?: boolean | undefined;
43
43
  /** Truncate captured request/response bodies to this many bytes. Default: 65536. */
44
- payloadMaxBytes?: number;
44
+ payloadMaxBytes?: number | undefined;
45
45
  /**
46
46
  * Cache built snapshots for this many ms, keyed by range, so overlapping reads
47
47
  * (panel poll + Prometheus scrape + alert loop) share one build. Mutating actions
48
48
  * invalidate it. Default: 1000. Set `0` to always build fresh.
49
49
  */
50
- snapshotCacheMs?: number;
50
+ snapshotCacheMs?: number | undefined;
51
51
  /**
52
52
  * SQLite file the panel persists to, so history survives restarts and the
53
53
  * 1h/24h/7d ranges trace real data. Default: `storage/monitor.sqlite`. Use
54
54
  * `:memory:` for an ephemeral, in-process store.
55
55
  */
56
- storage?: string;
56
+ storage?: string | undefined;
57
57
  /** Days of history to keep before pruning. Default: 7. */
58
- retentionDays?: number;
58
+ retentionDays?: number | undefined;
59
59
  /** What to do with data past retention: `delete` it or `archive` it. Default: `delete`. */
60
- retentionMode?: RetentionMode;
60
+ retentionMode?: RetentionMode | undefined;
61
61
  /**
62
62
  * Expose a Prometheus text-exposition endpoint. Default: `false` (opt-in).
63
63
  * The endpoint is unauthenticated (a scraper can't satisfy user-auth), so it
64
64
  * ships off; enable it only when you protect `metricsPath` at the network
65
65
  * layer (firewall/ingress) or scope it to a private interface.
66
66
  */
67
- metrics?: boolean;
67
+ metrics?: boolean | undefined;
68
68
  /** Path for the Prometheus endpoint. Default: `/metrics`. Protect it at the network layer. */
69
- metricsPath?: string;
69
+ metricsPath?: string | undefined;
70
70
  /** Evaluate threshold alerts on a short interval. Default: `true`. */
71
- alerts?: boolean;
71
+ alerts?: boolean | undefined;
72
72
  /** Alert thresholds (error rate, queue backlog, p95, rollbacks). */
73
- alertThresholds?: AlertThresholds;
73
+ alertThresholds?: AlertThresholds | undefined;
74
74
  /**
75
75
  * Minimum gap (ms) before the same alert can fire again after it recovers and
76
76
  * re-breaches. Stops an oscillating metric from re-paging / re-flooding the feed
77
77
  * for one ongoing issue. Default: 30 min. Set `0` to fire on every fresh breach.
78
78
  */
79
- alertCooldownMs?: number;
79
+ alertCooldownMs?: number | undefined;
80
80
  /**
81
81
  * Slack-compatible webhook URL. When set, every newly-firing alert is POSTed to
82
82
  * it as JSON (`{ text, level, title, detail }`) so alerts page someone instead of
83
83
  * only lighting up the panel. Dependency-free (uses `fetch`); delivery is
84
84
  * best-effort. For richer routing, register a handler with `onAlert()` instead.
85
85
  */
86
- alertWebhook?: string;
86
+ alertWebhook?: string | undefined;
87
87
  /** Accent colour (hex) for the panel chrome. Default: Zerotal orange. */
88
- accent?: string;
88
+ accent?: string | undefined;
89
89
  /** Version string shown in the System footer. */
90
- zerotalVersion?: string;
90
+ zerotalVersion?: string | undefined;
91
91
  /** Region label shown in the System footer. */
92
- region?: string;
92
+ region?: string | undefined;
93
93
  /** Deploy SHA shown in the System footer. */
94
- deploy?: string;
94
+ deploy?: string | undefined;
95
95
  /**
96
96
  * Switch contributed sections off by id — `{ scheduler: false }` keeps the
97
97
  * scheduler installed but drops its section from the panel. Anything absent
98
98
  * here is on.
99
99
  */
100
- sections?: Record<string, boolean>;
100
+ sections?: Record<string, boolean> | undefined;
101
101
  }
102
102
 
103
103
  export interface ResolvedMonitorConfig extends MonitorConfigShape {
package/src/panel.ts CHANGED
@@ -99,6 +99,8 @@ export interface MonitorSection {
99
99
  *
100
100
  * Contributors should declare their own minimal copy of the members they use
101
101
  * rather than importing this type, so they depend on this package not at all.
102
+ *
103
+ * @internal
102
104
  */
103
105
  export interface MonitorPanelHost {
104
106
  /**
@@ -31,6 +31,7 @@ const MONITOR_TOKENS = `
31
31
  }
32
32
  `.trim();
33
33
 
34
+ /** @internal */
34
35
  export class MonitorLayout extends Layout {
35
36
  static override get head(): string {
36
37
  return flowUiHead("Zerotal · Monitor", { tokensCss: MONITOR_TOKENS });
@@ -121,6 +121,8 @@ const FEED_PER_PAGE = 12;
121
121
  * `super-panel.html` built on Flow. Eight tabs (Overview, Requests,
122
122
  * Exceptions, Queues, Mail, Database, Cache, System) render from a live
123
123
  * {@link MonitorSnapshot}; interactions round-trip over the Flow WebSocket.
124
+ *
125
+ * @internal
124
126
  */
125
127
  export class MonitorPage extends Component {
126
128
  static layout = MonitorLayout;