@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,377 @@
1
+ ---
2
+ title: Monitor
3
+ description: The Super Panel — a self-hosted, real-data monitoring dashboard for requests, queries, jobs, cache, realtime, and more, built on the same event bus the logger reads.
4
+ ---
5
+
6
+ # Monitor
7
+
8
+ `@zerotal/monitor` is the **Super Panel**: a production monitoring dashboard that ships with your app. It rolls request-level debugging and live application metrics into one — a server-driven Flow page that reads from the framework's `FrameworkEvents` bus, persists every sample to its own SQLite file, and renders fourteen tabs of live, range-aware data. There is no sample data and no separate agent to run: a quiet app shows honest zeros, a busy one shows exactly what happened.
9
+
10
+ The panel observes HTTP requests, SQL queries (with N+1 detection), exceptions, the cache, queues and jobs, outgoing HTTP, mail, notifications, console commands, model changes, scheduled tasks, security events, application logs, and Flow WebSocket activity — and it raises threshold alerts on top of all of it.
11
+
12
+ ## Getting Started
13
+
14
+ It is a workspace package. Add the provider after `FlowProvider` in `bootstrap/providers.ts`:
15
+
16
+ ```ts
17
+ // bootstrap/providers.ts
18
+ import { FlowProvider } from "@zerotal/flow";
19
+ import { MonitorProvider } from "@zerotal/monitor";
20
+
21
+ export default [FlowProvider, MonitorProvider];
22
+ ```
23
+
24
+ Visit `/monitor`. Every tab populates from real activity as soon as your app handles traffic — nothing else to wire up.
25
+
26
+ ## What each tab shows
27
+
28
+ The sidebar groups the tabs into **Monitoring**, **Jobs & Mail**, and **Infrastructure**. If you are not sure where to look, this is the map.
29
+
30
+ **Overview** is the at-a-glance health screen, ordered from "right now" to
31
+ "what's slow". It reads top to bottom in four bands:
32
+
33
+ - **Live pulse** — the truly real-time gauges: HTTP requests currently being
34
+ processed (in-flight concurrency), open WebSocket connections, requests per
35
+ second, and the current error rate. Each carries a status dot that pulses while
36
+ there is activity.
37
+ - **Performance and health** — a compact latency profile (p50/p95/p99), the Apdex
38
+ and cache-hit scores, and the busiest WebSocket components.
39
+ - **Throughput** — one chart overlaying HTTP requests and WebSocket actions on a
40
+ single timeline, so you can correlate a spike across both.
41
+ - **System bottlenecks** — the slowest routes, top exceptions, and slow outgoing
42
+ HTTP side by side, for a single horizontal scan.
43
+
44
+ Start at the pulse, then scan down to whatever looks wrong.
45
+
46
+ **Requests** is the request explorer. Every request is a row: method, route,
47
+ status, the authenticated user, IP, query count, peak memory, and duration.
48
+
49
+ Click a row to expand its full trace — a span waterfall, the exact SQL that ran
50
+ (correlated to that request), and its log lines. A failed request shows its
51
+ exception message inline, so the trace explains why it failed. With
52
+ `capturePayloads` on, the trace also carries the request and response headers and
53
+ bodies, with sensitive headers and keys redacted.
54
+
55
+ Three widgets sit alongside: Slow requests, Application usage (busiest users),
56
+ and Top memory (heaviest routes). Search by path or method, filter by status
57
+ class, and page through ten at a time.
58
+
59
+ Clicking a route name — here or in the Overview's slow-routes list — opens a
60
+ **per-route drill-in**: that one route's latency percentiles, throughput, error
61
+ rate, status breakdown, and its recent and slowest requests. The drill-in is
62
+ shareable, because its URL carries the route — a refresh or a link reopens it.
63
+
64
+ **Realtime** is the same idea for Flow WebSocket actions. Flow re-runs your
65
+ middleware on every round-trip, so each action carries the same rich context a
66
+ request does: user, IP, the SQL it ran, and per-action memory.
67
+
68
+ The tab shows live connection counts, actions per minute, a **Connected clients**
69
+ list (who is online right now, not just how many), the busiest components, the
70
+ slowest actions, and a searchable, paginated action log with expandable traces.
71
+
72
+ The monitor excludes its own polling from this view, so you see your app rather
73
+ than the panel watching itself.
74
+
75
+ **Exceptions** groups failures by type and location, with a sparkline, 24h/7d/30d counts, the affected-user count, and an expandable stack trace.
76
+
77
+ **Alerts** is the history of threshold alerts that have fired. They are grouped
78
+ by kind, so a flapping metric collapses into one card with a count rather than a
79
+ wall of duplicates.
80
+
81
+ Expand a card to see why it fired:
82
+
83
+ - The breaching value, against its threshold.
84
+ - A snapshot of the surrounding state at that moment — error rate, percentiles,
85
+ throughput, pending jobs, rollbacks, the slowest routes at the time, and the
86
+ top exception.
87
+ - The timeline of firings.
88
+
89
+ **Security** is the audit feed — logins, logouts, authorization denials, and issued tokens — searchable and paged.
90
+
91
+ **Logs** is every application log entry, level-filterable and request-correlated, rendered like a terminal.
92
+
93
+ **Queues** shows workers, queue depth and throughput, failed jobs with a retry button, scheduled and delayed jobs, the dead-letter list (jobs that exhausted their retries) with a requeue button, and the slowest jobs. It is live data from `@zerotal/queue` when that package is installed, and degrades to empty otherwise.
94
+
95
+ **Mail** is the mail log — every message sent, queued, or failed, with its subject, recipients, mailer, render time, and body.
96
+
97
+ **Notifications** is distinct from Mail: one row per channel delivery (mail, database, slack, sms, broadcast), so a single notification fanned out to three channels shows three rows. Filter by status, search by recipient or channel, page through.
98
+
99
+ **Database** covers slow queries, N+1 offenders, transaction commit/rollback counts, recent migrations, and a **Model changes** panel with per-model created/updated/deleted counts plus a recent-changes timeline.
100
+
101
+ **Cache** shows the hit rate, hits and misses, evictions, and the hottest keys.
102
+
103
+ **Commands** is the log of console command runs — name, exit code, duration, when — from both the CLI and in-process `Artisan.call()`.
104
+
105
+ **System** is the host view: health checks, CPU/memory/heap gauges, uptime
106
+ checks, scheduled-task check-ins and run history, the storage panel (row counts,
107
+ oldest record, and the cleanup/export controls), and runtime metadata.
108
+
109
+ The environment badge in the top bar reads your real `APP_ENV` / `NODE_ENV` —
110
+ green in production, amber otherwise. A staging box never lies about being
111
+ production.
112
+
113
+ ## How the data flows
114
+
115
+ Everything is driven by core's `FrameworkEvents` bus, the same synchronous substrate the logger and telemetry read. A single subscriber, installed when the provider boots, maps each event to the right tab. You do not instrument anything: if your code uses the ORM, the cache, the mailer, notifications, the queue, or Flow, the panel already sees it.
116
+
117
+ | What you see | Where it comes from |
118
+ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
119
+ | Requests, latency, throughput, error rate, Apdex, slow routes, per-request user/IP/memory | `RequestHandled` / `RequestFailed` |
120
+ | Per-request SQL (correlated by context) and the N+1 flag | `QueryExecuted` / `NPlusOneDetected` |
121
+ | Exceptions grouped by type and route, with affected users | `RequestFailed` |
122
+ | Cache hit-rate, hot keys, evictions | `CacheQueried` / `CacheEvicted` |
123
+ | Mail sent / queued / failed | `MessageSent` / `MessageQueued` / `MessageFailed` |
124
+ | Notifications per channel | `NotificationSent` |
125
+ | Console command runs | `CommandRan` |
126
+ | Model created / updated / deleted | `ModelChanged` |
127
+ | Outgoing HTTP per host (calls, p95, error rate) | `OutgoingRequestCompleted` |
128
+ | Job throughput and slowest jobs | `JobRan` |
129
+ | Realtime actions with user, IP, queries, memory | `WebSocketConnected` / `WebSocketDisconnected` / `FlowActionHandled` |
130
+ | Security feed | `LoginSucceeded` / `LoginFailed` / `LoggedOut` / `AuthorizationDenied` / `TokenIssued` |
131
+ | Application logs | the core logger's tap |
132
+ | Transactions, migrations | `TransactionCommitted` / `TransactionRolledBack` / `MigrationRan` |
133
+ | Scheduled-task run history and check-ins | `TaskRan` / `TaskFailed` / `TaskSkipped` |
134
+ | Queues, workers, failed and dead-letter jobs | `@zerotal/queue`, when installed |
135
+ | Health, gauges, uptime, deploy SHA | the `Health` registry, the process, and git/env |
136
+
137
+ For anything that does not flow through the bus — a deploy marker, or a third-party call you make without the `Http` client — the `Monitor` facade records it directly and takes precedence. Every `Monitor.*` call is a safe no-op until the provider boots, so you can call it from anywhere without guarding.
138
+
139
+ ```ts
140
+ import { Monitor } from "@zerotal/monitor";
141
+
142
+ Monitor.recordDeploy(gitSha);
143
+ Monitor.recordHttp({ host: "api.stripe.com", ms: 412, error: false });
144
+
145
+ // Attach metadata to the current request or action — it shows on the trace.
146
+ Monitor.context({ tenant: tenant.id, plan: user.plan });
147
+ ```
148
+
149
+ ## Configuration
150
+
151
+ Create `config/monitor.ts` with the `MonitorConfig()` helper, which type-checks every field and applies sensible defaults, so you only override what you need.
152
+
153
+ ```ts
154
+ // config/monitor.ts
155
+ import { MonitorConfig } from "@zerotal/monitor";
156
+
157
+ export default MonitorConfig({
158
+ path: "/monitor",
159
+ auth: (user) => user?.role === "admin",
160
+ });
161
+ ```
162
+
163
+ | Field | Default | What it does |
164
+ | -------------------------------------- | ----------------------------- | -------------------------------------------------------------------- |
165
+ | `path` | `/monitor` | URL prefix the panel mounts at. |
166
+ | `title` / `subtitle` | `Super Panel` / `Zerotal Ops` | Branding in the sidebar and tab title. |
167
+ | `auth` | allow outside production | The access gate; see below. |
168
+ | `record` | `true` | Install the event recorder. Off means a read-only shell. |
169
+ | `refreshMs` | `3000` | Live auto-refresh cadence. |
170
+ | `apdexTargetMs` | `100` | Apdex satisfaction threshold (T). |
171
+ | `slowQueryMs` | `100` | Cut-off for a query to count as slow. |
172
+ | `slowRequestMs` | `1000` | Cut-off for the Slow requests widget. |
173
+ | `snapshotCacheMs` | `1000` | How long to reuse a built snapshot; see Performance. |
174
+ | `capturePayloads` | `false` | Capture request/response headers and bodies on the trace (redacted). |
175
+ | `payloadMaxBytes` | `65536` | Truncate captured bodies to this size. |
176
+ | `storage` | `storage/monitor.sqlite` | The persistence file. Use `:memory:` for ephemeral. |
177
+ | `retentionDays` | `7` | History kept before pruning. |
178
+ | `retentionMode` | `delete` | `delete` or `archive` data past retention. |
179
+ | `metrics` / `metricsPath` | `true` / `/metrics` | The Prometheus endpoint. |
180
+ | `alerts` | `true` | Evaluate threshold alerts. |
181
+ | `alertThresholds` | see Alerts | Error rate, p95, queue backlog, rollback limits. |
182
+ | `alertCooldownMs` | `1800000` | Minimum gap before the same alert re-fires. |
183
+ | `alertWebhook` | unset | Slack-compatible URL to POST firing alerts to. |
184
+ | `zerotalVersion` / `region` / `deploy` | derived | Footer metadata. |
185
+
186
+ ## Access control
187
+
188
+ The `auth` callback receives the authenticated user (or `undefined`) and returns whether to allow access. The default permits access only outside production, which is safe for local development but means **you must set an `auth` gate before deploying** — otherwise the panel, which exposes request payloads, user identities, and logs, is unreachable in production by default but wide open the moment you flip the environment. Gate it to an admin role:
189
+
190
+ ```ts
191
+ auth: (user) => user?.role === "admin",
192
+ ```
193
+
194
+ The same gate protects the JSON export route. The Prometheus endpoint is deliberately outside it (scrapers cannot log in) — protect that one at the network layer instead.
195
+
196
+ ## Alerts
197
+
198
+ The panel evaluates threshold alerts every fifteen seconds against the live snapshot. Out of the box it watches four signals: the 5xx error rate, p95 latency, total pending jobs across queues, and transaction rollbacks. Tune the limits through `alertThresholds`:
199
+
200
+ ```ts
201
+ alertThresholds: { errorRatePct: 5, p95Ms: 2000, queuePending: 500, rolledBackInWindow: 10 },
202
+ ```
203
+
204
+ Alerts are edge-triggered — each one fires once when it crosses its threshold and resets when the metric recovers. Because a metric hovering near its limit would otherwise re-fire every time it dips back over, a **cooldown** (`alertCooldownMs`, thirty minutes by default) suppresses re-firing of the same alert within the window. Set it to `0` if you genuinely want a fresh alert on every breach.
205
+
206
+ When an alert fires it is logged, recorded so the Alerts tab can show it with its full context, and dispatched. There are two ways to be paged. The simplest is a webhook — set `alertWebhook` to a Slack-compatible URL and each firing is POSTed as JSON, no extra dependency. For richer routing, register a handler and wire it to notifications, PagerDuty, or anything else:
207
+
208
+ ```ts
209
+ import { onAlert } from "@zerotal/monitor";
210
+
211
+ onAlert((alert) => {
212
+ // alert: { id, level, title, detail, metric, value, threshold, unit }
213
+ notifyOpsTeam(alert);
214
+ });
215
+ ```
216
+
217
+ Use the webhook when you just want a message in a channel; use `onAlert` when you need to choose a destination, format a payload, or escalate by severity.
218
+
219
+ ## Prometheus
220
+
221
+ A Prometheus text-exposition endpoint is served at `/metrics` (configurable, on by default). It exports cumulative HTTP counters, latency/Apdex/cache/queue/WebSocket/exception gauges, system gauges, and per-route latency — ready for Grafana and Alertmanager.
222
+
223
+ ```
224
+ zerotal_http_requests_total 1284
225
+ zerotal_http_request_duration_ms_p95 318
226
+ zerotal_apdex 0.94
227
+ zerotal_ws_connections 12
228
+ zerotal_route_duration_ms_avg{method="GET",route="/dashboard"} 612
229
+ ```
230
+
231
+ ## Persistence, retention, and export
232
+
233
+ The panel writes every sample to `bun:sqlite`, so the **live / 1h / 24h / 7d**
234
+ ranges trace real history and survive restarts — not just whatever happened since
235
+ boot. Each stream is a timestamped table, and reading a snapshot selects the rows
236
+ inside the chosen window, so every tab stays range-consistent.
237
+
238
+ **Retention.** Data older than `retentionDays` is pruned hourly and on boot. Set
239
+ `retentionMode: 'archive'` and the old rows move to `*_archive` tables instead of
240
+ being deleted, which keeps them for cold storage.
241
+
242
+ **The storage panel** on the System tab shows live row counts and the oldest
243
+ record, with three controls:
244
+
245
+ | Control | What it does |
246
+ | ---------------- | ----------------------------------------------------------- |
247
+ | **Clean up now** | Prunes past-retention data immediately. |
248
+ | **Clear all** | Wipes everything, behind a confirm prompt. |
249
+ | **Export JSON** | Downloads the full current snapshot for the selected range. |
250
+
251
+ The export sits behind the same auth gate as the panel. It is worth knowing about
252
+ for incident write-ups, or for diffing state either side of a deploy.
253
+
254
+ All three are available on the store programmatically, as `store.prune()`,
255
+ `store.wipe()`, and `store.snapshot(range)`.
256
+
257
+ ## Theming
258
+
259
+ The panel is built from [`@zerotal/flow-ui`](/docs/components) and themed with its design tokens, so it follows light and dark mode and looks like the same product as the [admin](/docs/admin) rather than a separate tool. There is no build step: the layout injects the kit's theme, which loads the Tailwind Play CDN and emits the palette inline.
260
+
261
+ Every colour in the panel resolves to a token — `bg-card`, `text-muted-foreground`, `text-success`, `text-destructive`, `hsl(var(--chart-3))` — so the only thing the monitor overrides is its own orange `--primary`. That one variable recolours the whole panel, and an app can push it further by appending its own token CSS.
262
+
263
+ Practically, this means re-branding the monitor is a few CSS variables rather than a fork, and a contributed section written against the same tokens matches the built-in ones for free.
264
+
265
+ ## Adding your own sections
266
+
267
+ The panel owns the shell, the time-range selector, the storage and the retention policy — but it doesn't own the knowledge of what is worth watching about any given package. So it's a **host**: it publishes a write surface as the `monitor.panel` container binding, and a package pushes a section into it at boot.
268
+
269
+ A section is _described_, not rendered. The contributor returns stats and tables; the panel draws them. That keeps the panel coherent no matter who contributed a section, and means the contributing package needs no JSX and no dependency on `@zerotal/monitor`:
270
+
271
+ ```ts
272
+ // In a contributing provider's onBooting()
273
+ interface MonitorHost {
274
+ enabled(id: string): boolean;
275
+ section(s: { id: string; label: string; group?: string; resolve(range: string): unknown }): void;
276
+ }
277
+
278
+ const monitor = app.container.tryMake("monitor.panel") as MonitorHost | undefined;
279
+ if (!monitor?.enabled("billing")) return;
280
+
281
+ monitor.section({
282
+ id: "billing",
283
+ label: "Billing",
284
+ group: "Infrastructure",
285
+ resolve: () => ({
286
+ stats: [
287
+ { label: "Failed charges", value: failures.length, tone: failures.length ? "bad" : "good" },
288
+ { label: "Success rate", value: `${rate}%`, percent: rate, tone: "good" },
289
+ ],
290
+ tables: [
291
+ {
292
+ title: "Recent failures",
293
+ columns: [
294
+ { key: "id", label: "Charge", mono: true },
295
+ { key: "reason", label: "Reason" },
296
+ { key: "amount", label: "Amount", align: "end" },
297
+ ],
298
+ rows: failures,
299
+ empty: "No failed charges in this range.",
300
+ },
301
+ ],
302
+ }),
303
+ });
304
+ ```
305
+
306
+ `resolve` receives the selected range and is called on every render and every auto-refresh, so it should read from what the package already records rather than doing expensive work. A section that throws renders as empty rather than taking the panel down — the monitor has to stay up precisely when the thing it watches is unhealthy.
307
+
308
+ Declaring the host's shape locally rather than importing it is the point: the package compiles and ships with no dependency on the monitor, and an app that runs it without the panel pulls in nothing extra — the binding simply isn't there and the function returns.
309
+
310
+ This is the other half of how the recorders already work. A package writes its measurements into `monitor.store` through its own `observability.ts`; a section says what those measurements should look like on screen.
311
+
312
+ To keep a contributor installed but drop its section, name it in `sections`:
313
+
314
+ ```ts
315
+ // config/monitor.ts
316
+ export default MonitorConfig({
317
+ sections: { scheduler: false },
318
+ });
319
+ ```
320
+
321
+ ### Scheduled tasks
322
+
323
+ `@zerotal/scheduler` ships the first contributed section. A cron task that silently stopped firing is one of the harder failures to notice — nothing errors, work just stops happening — so the section leads with counts of tasks that are failing or have never run, then lists every task with its cron expression, last result, duration and next due time. Install both providers and it appears under **Infrastructure**; no configuration.
324
+
325
+ ## Testing
326
+
327
+ The starter app ships a **Monitoring Lab** at `/lab` that deliberately exercises every observed path, so you can fill the panel with real activity instead of waiting for organic traffic. It lives under `app/flow/pages/(authenticated)/lab`, with the shared scenario logic in `app/services/lab-scenarios.ts`.
328
+
329
+ The lab page has two kinds of trigger. The buttons run as Flow WebSocket actions, so they populate the Realtime tab as well as whatever they touch — N+1 bursts, cache hits and misses, a model create/update/delete, a mail notification, logs at every level, and a deliberately slow action. The links are full HTTP requests: `/lab/load` runs the whole kitchen sink in one request, `/lab/slow` sleeps for a second and a half to land in Slow requests, and `/lab/boom` throws so you can watch the Exceptions tab group the failure and attribute it to your signed-in user.
330
+
331
+ Open `/lab` in one tab and `/monitor` in another, click through, and watch each panel light up. It is the fastest way to learn what every tab is showing and to sanity-check a fresh install.
332
+
333
+ > **Note** — The lab pages are development scaffolding. Delete the `lab` directory and `lab-scenarios.ts` before shipping, or gate them so they never reach production.
334
+
335
+ ## Performance and honest limits
336
+
337
+ The panel is built to be cheap, but a few things are worth knowing.
338
+
339
+ Recording never sits on the request hot path. Each `record*` call pushes a row into an in-memory buffer (just an array push, no I/O); a timer flushes batched inserts in one transaction roughly once a second, and a read flushes first so the panel always sees the latest activity. So even under heavy load the monitor adds no synchronous disk write to your responses — the buffer also flushes early if it fills, to bound memory.
340
+
341
+ Snapshots are cached for `snapshotCacheMs` (one second by default), keyed by range. This de-duplicates the overlapping reads that happen when the panel poll, the alert loop, and a Prometheus scrape all ask for the live snapshot at once, and any action that changes data invalidates the cache immediately so you never see a stale count after clicking. A single operator polling every few seconds sees little benefit; the win is under concurrent access. Set it to `0` to always build fresh.
342
+
343
+ Per-request and per-action memory is the process heap at completion, not an isolated per-request figure — Bun shares one heap across concurrent work, so treat it as indicative rather than exact. The live queue, scheduler, and health adapters degrade gracefully: if the optional peer package is not installed, that section reads empty rather than throwing. And the panel never invents data — if a number is zero, nothing happened.
344
+
345
+ ## References
346
+
347
+ `@zerotal/monitor` exports the panel, its provider and config helper, and the
348
+ pieces you need to record or read data yourself.
349
+
350
+ | Export | What it is |
351
+ | ----------------------------- | ------------------------------------------------------------------------------------------- |
352
+ | `MonitorProvider` | Registers the panel, the recorders, and the alert loop. Add it to `bootstrap/providers.ts`. |
353
+ | `MonitorConfig(shape)` | Type-checked builder for `config/monitor.ts`. |
354
+ | `Monitor` | Facade over the live store — read a snapshot or record an event by hand. |
355
+ | `MonitorStore` | The buffered store itself, when you need it outside the container. |
356
+ | `MonitorPanel` | The panel definition, for contributing your own sections. |
357
+ | `renderPrometheus(snapshot)` | Renders a snapshot in the Prometheus text exposition format. |
358
+ | `evaluateAlerts(snapshot)` | Runs the configured thresholds and returns the notices that fired. |
359
+ | `onAlert(handler)` | Subscribe to alert notices — route them to Slack, email, or a pager. |
360
+ | `installMonitorEventBridge()` | Wires framework events into the recorders. The provider calls this for you. |
361
+ | `MonitorAuthMiddleware` | Guards the panel route with your `auth` predicate. |
362
+ | `MonitorPayloadMiddleware` | Captures request/response bodies when `capturePayloads` is on. |
363
+
364
+ Types: `MonitorConfigShape`, `ResolvedMonitorConfig`, `MonitorStoreOptions`,
365
+ `AlertThresholds`, `AlertNotice`, plus the store's row types.
366
+
367
+ ### Commands
368
+
369
+ `@zerotal/monitor` ships no CLI commands. The panel is a route, not a console
370
+ tool — everything is read through the browser or the Prometheus endpoint.
371
+
372
+ ## Next steps
373
+
374
+ - [Telemetry](/docs/telemetry) — export the same signal to an OTLP backend for long-term storage.
375
+ - [Logger](/docs/logger) — the channels the Logs tab reads from.
376
+ - [Health](/docs/health) — the readiness endpoint the panel's Health section surfaces.
377
+ - [Devtools](/docs/devtools) — the in-page inspector for a single request, rather than the fleet view.