@zerotal/monitor 1.0.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/package.json +74 -0
- package/src/MonitorStore.ts +1471 -0
- package/src/alerting.ts +173 -0
- package/src/config.ts +171 -0
- package/src/facades/Monitor.ts +75 -0
- package/src/index.ts +62 -0
- package/src/instance.ts +16 -0
- package/src/middleware/MonitorAuthMiddleware.ts +20 -0
- package/src/middleware/MonitorPayloadMiddleware.ts +96 -0
- package/src/panel.ts +159 -0
- package/src/prometheus.ts +128 -0
- package/src/provider/MonitorProvider.ts +318 -0
- package/src/recorder/MonitorEventBridge.ts +157 -0
- package/src/recorder/ctxBuffers.ts +91 -0
- package/src/sources/live.ts +249 -0
- package/src/sources/realtime.ts +24 -0
- package/src/sources/system.ts +151 -0
- package/src/store/MonitorDb.ts +415 -0
- package/src/store/RingBuffer.ts +89 -0
- package/src/store/types.ts +580 -0
- package/src/support/time.ts +25 -0
- package/src/ui/MonitorLayout.tsx +42 -0
- package/src/ui/MonitorPage.tsx +3722 -0
- package/src/ui/charts.tsx +55 -0
- package/src/ui/icons.ts +32 -0
- package/src/ui/tones.ts +78 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared data shapes for the monitoring panel.
|
|
3
|
+
*
|
|
4
|
+
* These describe the *read model* — the aggregated snapshot the Flow page
|
|
5
|
+
* renders. The recorder ({@link ../recorder/MonitorEventBridge.ts}) and the
|
|
6
|
+
* live sources ({@link ../sources}) feed the {@link ../MonitorStore.ts}, which
|
|
7
|
+
* derives a {@link MonitorSnapshot} on demand.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StorageInfo } from "./MonitorDb.ts";
|
|
11
|
+
export type { StorageInfo } from "./MonitorDb.ts";
|
|
12
|
+
|
|
13
|
+
/** Selectable time window for the dashboard. */
|
|
14
|
+
export type MonitorRange = "live" | "1h" | "24h" | "7d";
|
|
15
|
+
|
|
16
|
+
/** A coarse tone used to colour values across the UI. */
|
|
17
|
+
export type Tone = "ok" | "warn" | "bad" | "neutral";
|
|
18
|
+
|
|
19
|
+
// ── Overview ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface StatCard {
|
|
22
|
+
label: string;
|
|
23
|
+
value: string;
|
|
24
|
+
/** Percentage delta vs the previous window. */
|
|
25
|
+
delta: number;
|
|
26
|
+
/** Sparkline series (oldest → newest). */
|
|
27
|
+
series: number[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Percentile {
|
|
31
|
+
label: string; // p50 / p95 / p99
|
|
32
|
+
value: number; // milliseconds
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RouteStat {
|
|
36
|
+
method: string;
|
|
37
|
+
path: string;
|
|
38
|
+
ms: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Count of requests in one HTTP status class (Route detail). */
|
|
42
|
+
export interface StatusClassCount {
|
|
43
|
+
label: "2xx" | "3xx" | "4xx" | "5xx";
|
|
44
|
+
count: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Aggregated latency / throughput / error history for a single route, powering
|
|
49
|
+
* the per-route drill-in page. Derived on demand from `mon_requests` filtered to
|
|
50
|
+
* one method+path within the selected window.
|
|
51
|
+
*/
|
|
52
|
+
export interface RouteDetail {
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
range: MonitorRange;
|
|
56
|
+
/** Total requests to this route in the window. */
|
|
57
|
+
total: number;
|
|
58
|
+
/** Requests per minute over the window. */
|
|
59
|
+
rpm: number;
|
|
60
|
+
/** Number of 5xx responses. */
|
|
61
|
+
errorCount: number;
|
|
62
|
+
/** 5xx responses as a percentage of total. */
|
|
63
|
+
errorRate: number;
|
|
64
|
+
p50: number;
|
|
65
|
+
p95: number;
|
|
66
|
+
p99: number;
|
|
67
|
+
avgMs: number;
|
|
68
|
+
maxMs: number;
|
|
69
|
+
/** Request counts bucketed over the window (oldest → newest). */
|
|
70
|
+
throughput: number[];
|
|
71
|
+
/** Average latency (ms) bucketed over the window. */
|
|
72
|
+
latency: number[];
|
|
73
|
+
/** 5xx counts bucketed over the window. */
|
|
74
|
+
errors: number[];
|
|
75
|
+
/** Response breakdown by status class. */
|
|
76
|
+
statusDist: StatusClassCount[];
|
|
77
|
+
/** Most-recent requests to this route. */
|
|
78
|
+
recent: RequestEntry[];
|
|
79
|
+
/** Slowest requests to this route. */
|
|
80
|
+
slowest: RequestEntry[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OutgoingHttp {
|
|
84
|
+
host: string;
|
|
85
|
+
calls: number;
|
|
86
|
+
p95: number;
|
|
87
|
+
errRate: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface Deploy {
|
|
91
|
+
/** Seconds-into-window position used to place the marker (0..windowSeconds). */
|
|
92
|
+
at: number;
|
|
93
|
+
sha: string;
|
|
94
|
+
when: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Alert {
|
|
98
|
+
tone: "red" | "amber";
|
|
99
|
+
text: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Live "right now" gauges for the Overview pulse row — independent of the range tab. */
|
|
103
|
+
export interface PulseStats {
|
|
104
|
+
/** HTTP requests currently being processed (in-flight concurrency). */
|
|
105
|
+
activeRequests: number;
|
|
106
|
+
/** Open Flow WebSocket connections. */
|
|
107
|
+
activeConnections: number;
|
|
108
|
+
/** Requests per second over the last 5 minutes. */
|
|
109
|
+
requestsPerSec: number;
|
|
110
|
+
/** 4xx+5xx rate over the last 5 minutes (%). */
|
|
111
|
+
errorRatePct: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A snapshot of surrounding state captured when an alert fired — the "why". */
|
|
115
|
+
export interface AlertContext {
|
|
116
|
+
errorRate: number;
|
|
117
|
+
rpm: number;
|
|
118
|
+
p50: number;
|
|
119
|
+
p95: number;
|
|
120
|
+
p99: number;
|
|
121
|
+
apdex: number;
|
|
122
|
+
pending: number;
|
|
123
|
+
rolledBack: number;
|
|
124
|
+
slowRoutes: RouteStat[];
|
|
125
|
+
topException: string | null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** One alert kind, with its firings grouped and the latest firing's context. */
|
|
129
|
+
export interface AlertEntry {
|
|
130
|
+
id: string;
|
|
131
|
+
title: string;
|
|
132
|
+
level: "warning" | "critical";
|
|
133
|
+
status: "ok" | "warn" | "bad" | "info";
|
|
134
|
+
detail: string;
|
|
135
|
+
metric: string;
|
|
136
|
+
value: number;
|
|
137
|
+
threshold: number;
|
|
138
|
+
unit: string;
|
|
139
|
+
/** How many times this alert fired in the window. */
|
|
140
|
+
count: number;
|
|
141
|
+
firstSeen: string;
|
|
142
|
+
lastSeen: string;
|
|
143
|
+
context: AlertContext;
|
|
144
|
+
/** Most-recent firings (newest first), for the timeline. */
|
|
145
|
+
occurrences: { when: string; detail: string; value: number }[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Requests (trace explorer) ───────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export type SpanKind = "boot" | "middleware" | "controller" | "query" | "cache" | "http" | "view";
|
|
151
|
+
|
|
152
|
+
export interface RequestSpan {
|
|
153
|
+
label: string;
|
|
154
|
+
kind: SpanKind;
|
|
155
|
+
start: number;
|
|
156
|
+
dur: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface RequestQuery {
|
|
160
|
+
ms: number;
|
|
161
|
+
sql: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface RequestLog {
|
|
165
|
+
level: "info" | "warn" | "error";
|
|
166
|
+
msg: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Captured request/response headers + bodies (redacted), when payload capture is on. */
|
|
170
|
+
export interface RequestPayload {
|
|
171
|
+
reqHeaders: Record<string, string>;
|
|
172
|
+
reqBody: string;
|
|
173
|
+
resBody: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface RequestEntry {
|
|
177
|
+
id: number;
|
|
178
|
+
method: string;
|
|
179
|
+
path: string;
|
|
180
|
+
status: number;
|
|
181
|
+
ms: number;
|
|
182
|
+
when: string;
|
|
183
|
+
nplus: boolean;
|
|
184
|
+
/** Captured payload (headers + bodies), or null when capture is off / unavailable. */
|
|
185
|
+
payload: RequestPayload | null;
|
|
186
|
+
/** Authenticated user (id or email), or null when the request was unauthenticated. */
|
|
187
|
+
user: string | null;
|
|
188
|
+
/** Client IP address, or null when unavailable. */
|
|
189
|
+
ip: string | null;
|
|
190
|
+
/** Process heap (KB) at request completion — a per-request memory proxy. */
|
|
191
|
+
memKb: number;
|
|
192
|
+
/** Custom metadata attached via `Monitor.context(...)` during the request. */
|
|
193
|
+
context: Record<string, unknown>;
|
|
194
|
+
/** The exception message, when this request failed — links the request to its error. */
|
|
195
|
+
error: string | null;
|
|
196
|
+
spans: RequestSpan[];
|
|
197
|
+
queries: RequestQuery[];
|
|
198
|
+
logs: RequestLog[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Top active users by request volume (Application Usage widget). */
|
|
202
|
+
export interface UserUsage {
|
|
203
|
+
/** User id or email. */
|
|
204
|
+
id: string;
|
|
205
|
+
requests: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Heaviest routes by memory (Top Memory Requests widget). */
|
|
209
|
+
export interface MemoryRoute {
|
|
210
|
+
method: string;
|
|
211
|
+
path: string;
|
|
212
|
+
/** Peak heap (KB) observed for this route in the window. */
|
|
213
|
+
memKb: number;
|
|
214
|
+
count: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Exceptions ───────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
export interface ExceptionGroup {
|
|
220
|
+
type: string;
|
|
221
|
+
message: string;
|
|
222
|
+
location: string;
|
|
223
|
+
count: number;
|
|
224
|
+
users: number;
|
|
225
|
+
lastSeen: string;
|
|
226
|
+
d1: number;
|
|
227
|
+
d7: number;
|
|
228
|
+
d30: number;
|
|
229
|
+
series: number[];
|
|
230
|
+
frames: string[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Queues ───────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
export interface QueueMetric {
|
|
236
|
+
label: string;
|
|
237
|
+
value: string;
|
|
238
|
+
sub: string;
|
|
239
|
+
tone?: Tone;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface Worker {
|
|
243
|
+
name: string;
|
|
244
|
+
status: "running" | "paused" | "stopped";
|
|
245
|
+
processes: number;
|
|
246
|
+
jobsPerMin: number;
|
|
247
|
+
series: number[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface QueueRow {
|
|
251
|
+
name: string;
|
|
252
|
+
pending: number;
|
|
253
|
+
wait: number;
|
|
254
|
+
throughput: number;
|
|
255
|
+
paused: boolean;
|
|
256
|
+
series: number[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface FailedJob {
|
|
260
|
+
id: number;
|
|
261
|
+
name: string;
|
|
262
|
+
error: string;
|
|
263
|
+
failedAt: string;
|
|
264
|
+
attempts: number;
|
|
265
|
+
tags: string[];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface ScheduledJob {
|
|
269
|
+
name: string;
|
|
270
|
+
runsIn: string;
|
|
271
|
+
queue: string;
|
|
272
|
+
tag: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface DeadJob {
|
|
276
|
+
id: number;
|
|
277
|
+
name: string;
|
|
278
|
+
error: string;
|
|
279
|
+
attempts: number;
|
|
280
|
+
deadAt: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Database ─────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export interface DbStat {
|
|
286
|
+
label: string;
|
|
287
|
+
value: string;
|
|
288
|
+
sub: string;
|
|
289
|
+
tone?: Tone;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface SlowQuery {
|
|
293
|
+
sql: string;
|
|
294
|
+
ms: number;
|
|
295
|
+
callers: number;
|
|
296
|
+
location: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Cache ────────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
export interface CacheKey {
|
|
302
|
+
key: string;
|
|
303
|
+
hits: number;
|
|
304
|
+
rate: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface CacheStats {
|
|
308
|
+
hitRate: number;
|
|
309
|
+
hits: number;
|
|
310
|
+
misses: number;
|
|
311
|
+
evictions: number;
|
|
312
|
+
keys: CacheKey[];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Framework-event feeds (security, scheduler, transactions, …) ───────────────
|
|
316
|
+
|
|
317
|
+
/** A single row in an event feed (security, scheduled-task runs, etc.). */
|
|
318
|
+
export interface FeedEvent {
|
|
319
|
+
when: string;
|
|
320
|
+
label: string;
|
|
321
|
+
detail: string;
|
|
322
|
+
status: "ok" | "warn" | "bad" | "info";
|
|
323
|
+
route: string | null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** A failed or slow background job. */
|
|
327
|
+
export interface JobEntry {
|
|
328
|
+
className: string;
|
|
329
|
+
queue: string;
|
|
330
|
+
status: string;
|
|
331
|
+
ms: number;
|
|
332
|
+
when: string;
|
|
333
|
+
error: string | null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** An N+1 query offender (Database tab). */
|
|
337
|
+
export interface NPlusOne {
|
|
338
|
+
fingerprint: string;
|
|
339
|
+
occurrences: number;
|
|
340
|
+
worstCount: number;
|
|
341
|
+
route: string | null;
|
|
342
|
+
lastSeen: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Database transaction summary. */
|
|
346
|
+
export interface TxStats {
|
|
347
|
+
committed: number;
|
|
348
|
+
rolledBack: number;
|
|
349
|
+
avgMs: number;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A migration run (Database tab). */
|
|
353
|
+
export interface MigrationEntry {
|
|
354
|
+
name: string;
|
|
355
|
+
direction: string;
|
|
356
|
+
ms: number;
|
|
357
|
+
ok: boolean;
|
|
358
|
+
when: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** A single Flow WebSocket action with its request-scoped context. */
|
|
362
|
+
export interface WsAction {
|
|
363
|
+
/** Stable id (the action's recorded timestamp) — keys the expandable row across pages. */
|
|
364
|
+
id: number;
|
|
365
|
+
component: string;
|
|
366
|
+
action: string;
|
|
367
|
+
ms: number;
|
|
368
|
+
ok: boolean;
|
|
369
|
+
user: string | null;
|
|
370
|
+
ip: string | null;
|
|
371
|
+
nplus: boolean;
|
|
372
|
+
/** Process heap (KB) at action completion — the per-action memory proxy. */
|
|
373
|
+
memKb: number;
|
|
374
|
+
queries: RequestQuery[];
|
|
375
|
+
context: Record<string, unknown>;
|
|
376
|
+
when: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** One currently-connected Flow WebSocket client. */
|
|
380
|
+
export interface ConnectedClient {
|
|
381
|
+
id: string;
|
|
382
|
+
ip: string;
|
|
383
|
+
/** Authenticated user (email/id), or null if no action has identified them yet. */
|
|
384
|
+
user: string | null;
|
|
385
|
+
connectedFor: string;
|
|
386
|
+
lastActivity: string;
|
|
387
|
+
actions: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Realtime / Flow WebSocket activity. */
|
|
391
|
+
export interface RealtimeStats {
|
|
392
|
+
activeConnections: number;
|
|
393
|
+
actionsPerMin: number;
|
|
394
|
+
avgActionMs: number;
|
|
395
|
+
/** Average per-action heap (KB) across the window. */
|
|
396
|
+
avgMemKb: number;
|
|
397
|
+
opened: number;
|
|
398
|
+
closed: number;
|
|
399
|
+
series: number[];
|
|
400
|
+
components: { name: string; actions: number; avgMs: number }[];
|
|
401
|
+
slowActions: { component: string; action: string; ms: number; when: string }[];
|
|
402
|
+
recentActions: WsAction[];
|
|
403
|
+
/** Who is connected right now (live, not windowed). */
|
|
404
|
+
clients: ConnectedClient[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** A delivered notification (one per channel), for the Notifications feed. */
|
|
408
|
+
export interface NotificationEntry {
|
|
409
|
+
notification: string;
|
|
410
|
+
channel: string;
|
|
411
|
+
recipient: string;
|
|
412
|
+
status: "ok" | "bad";
|
|
413
|
+
ms: number;
|
|
414
|
+
when: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** A console/Artisan command run, for the Commands feed. */
|
|
418
|
+
export interface CommandEntry {
|
|
419
|
+
name: string;
|
|
420
|
+
status: "ok" | "bad";
|
|
421
|
+
ms: number;
|
|
422
|
+
code: number;
|
|
423
|
+
when: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Per-model created/updated/deleted counts (the models watcher). */
|
|
427
|
+
export interface ModelStat {
|
|
428
|
+
model: string;
|
|
429
|
+
table: string;
|
|
430
|
+
created: number;
|
|
431
|
+
updated: number;
|
|
432
|
+
deleted: number;
|
|
433
|
+
total: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** A single model-change event, for the recent-changes timeline. */
|
|
437
|
+
export interface ModelEvent {
|
|
438
|
+
model: string;
|
|
439
|
+
table: string;
|
|
440
|
+
operation: "created" | "updated" | "deleted";
|
|
441
|
+
when: string;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Mail ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
export interface MailEntry {
|
|
447
|
+
id: number;
|
|
448
|
+
subject: string;
|
|
449
|
+
to: string;
|
|
450
|
+
mailer: string;
|
|
451
|
+
status: "sent" | "queued" | "failed";
|
|
452
|
+
when: string;
|
|
453
|
+
ms: number;
|
|
454
|
+
body: string;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── System / health ──────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
export interface HealthEntry {
|
|
460
|
+
name: string;
|
|
461
|
+
ok: boolean;
|
|
462
|
+
detail: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export interface Gauge {
|
|
466
|
+
label: string;
|
|
467
|
+
value: number;
|
|
468
|
+
sub: string;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export interface UptimeCheck {
|
|
472
|
+
name: string;
|
|
473
|
+
ok: boolean;
|
|
474
|
+
ms: number;
|
|
475
|
+
uptime: string;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export interface CheckIn {
|
|
479
|
+
name: string;
|
|
480
|
+
cron: string;
|
|
481
|
+
last: string;
|
|
482
|
+
status: "ok" | "late" | "missed";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export interface SystemMeta {
|
|
486
|
+
uptime: string;
|
|
487
|
+
bun: string;
|
|
488
|
+
zerotal: string;
|
|
489
|
+
region: string;
|
|
490
|
+
deploy: string;
|
|
491
|
+
/** Deployment environment (production / staging / development …). */
|
|
492
|
+
environment: string;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ── The full snapshot the page renders ──────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
export interface MonitorSnapshot {
|
|
498
|
+
range: MonitorRange;
|
|
499
|
+
alerts: Alert[];
|
|
500
|
+
deploys: Deploy[];
|
|
501
|
+
|
|
502
|
+
// live pulse (in-flight requests, connections, rate, error rate) — not windowed
|
|
503
|
+
pulse: PulseStats;
|
|
504
|
+
|
|
505
|
+
// overview
|
|
506
|
+
statCards: StatCard[];
|
|
507
|
+
percentiles: Percentile[];
|
|
508
|
+
apdex: number;
|
|
509
|
+
throughput: number[];
|
|
510
|
+
jobsSeries: number[];
|
|
511
|
+
slowRoutes: RouteStat[];
|
|
512
|
+
outgoingHttp: OutgoingHttp[];
|
|
513
|
+
|
|
514
|
+
// queues
|
|
515
|
+
queueStats: QueueMetric[];
|
|
516
|
+
workers: Worker[];
|
|
517
|
+
queues: QueueRow[];
|
|
518
|
+
jobTags: string[];
|
|
519
|
+
failedJobs: FailedJob[];
|
|
520
|
+
scheduledJobs: ScheduledJob[];
|
|
521
|
+
deadLetter: DeadJob[];
|
|
522
|
+
|
|
523
|
+
// requests
|
|
524
|
+
requests: RequestEntry[];
|
|
525
|
+
slowRequests: RequestEntry[];
|
|
526
|
+
topUsers: UserUsage[];
|
|
527
|
+
topMemory: MemoryRoute[];
|
|
528
|
+
|
|
529
|
+
// exceptions
|
|
530
|
+
exceptions: ExceptionGroup[];
|
|
531
|
+
|
|
532
|
+
// database
|
|
533
|
+
dbStats: DbStat[];
|
|
534
|
+
slowQueries: SlowQuery[];
|
|
535
|
+
transactions: TxStats;
|
|
536
|
+
migrations: MigrationEntry[];
|
|
537
|
+
nplusOnes: NPlusOne[];
|
|
538
|
+
|
|
539
|
+
// security / audit
|
|
540
|
+
security: FeedEvent[];
|
|
541
|
+
|
|
542
|
+
// threshold-alert history (firings grouped by kind, with captured context)
|
|
543
|
+
alertHistory: AlertEntry[];
|
|
544
|
+
|
|
545
|
+
// application logs
|
|
546
|
+
logs: FeedEvent[];
|
|
547
|
+
|
|
548
|
+
// realtime / websocket
|
|
549
|
+
realtime: RealtimeStats;
|
|
550
|
+
|
|
551
|
+
// scheduler run history + slow jobs
|
|
552
|
+
scheduledRuns: FeedEvent[];
|
|
553
|
+
slowJobs: JobEntry[];
|
|
554
|
+
|
|
555
|
+
// cache
|
|
556
|
+
cache: CacheStats;
|
|
557
|
+
|
|
558
|
+
// mail
|
|
559
|
+
mail: MailEntry[];
|
|
560
|
+
|
|
561
|
+
// notifications (distinct from mail — one row per channel delivery)
|
|
562
|
+
notifications: NotificationEntry[];
|
|
563
|
+
|
|
564
|
+
// console / Artisan command runs
|
|
565
|
+
commands: CommandEntry[];
|
|
566
|
+
|
|
567
|
+
// ORM model-change counts (created/updated/deleted)
|
|
568
|
+
models: ModelStat[];
|
|
569
|
+
|
|
570
|
+
// ORM model-change timeline (most recent first)
|
|
571
|
+
recentModels: ModelEvent[];
|
|
572
|
+
|
|
573
|
+
// system
|
|
574
|
+
health: HealthEntry[];
|
|
575
|
+
gauges: Gauge[];
|
|
576
|
+
uptimeChecks: UptimeCheck[];
|
|
577
|
+
checkins: CheckIn[];
|
|
578
|
+
storage: StorageInfo;
|
|
579
|
+
meta: SystemMeta;
|
|
580
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Time + number formatting helpers shared across the store and UI. */
|
|
2
|
+
|
|
3
|
+
/** Human "x ago" string from a millisecond timestamp. */
|
|
4
|
+
export function ago(tsMs: number): string {
|
|
5
|
+
const s = Math.max(0, Math.floor((Date.now() - tsMs) / 1000));
|
|
6
|
+
if (s < 5) return "just now";
|
|
7
|
+
if (s < 60) return `${s}s ago`;
|
|
8
|
+
const m = Math.floor(s / 60);
|
|
9
|
+
if (m < 60) return `${m}m ago`;
|
|
10
|
+
const h = Math.floor(m / 60);
|
|
11
|
+
if (h < 24) return `${h}h ago`;
|
|
12
|
+
const d = Math.floor(h / 24);
|
|
13
|
+
return `${d}d ago`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Thousands-separated integer. */
|
|
17
|
+
export function commas(n: number): string {
|
|
18
|
+
return Math.round(n).toLocaleString("en-US");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Signed percentage delta between current and previous values. */
|
|
22
|
+
export function delta(current: number, previous: number): number {
|
|
23
|
+
if (previous === 0) return current === 0 ? 0 : 100;
|
|
24
|
+
return Math.round(((current - previous) / previous) * 100);
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @jsxImportSource @zerotal/flow */
|
|
2
|
+
import { Layout } from "@zerotal/flow";
|
|
3
|
+
import type { HtmlNode } from "@zerotal/flow";
|
|
4
|
+
import { flowUiHead } from "@zerotal/flow-ui";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shell for the monitoring panel.
|
|
8
|
+
*
|
|
9
|
+
* The panel is built from `@zerotal/flow-ui` and themed with its design tokens,
|
|
10
|
+
* so it inherits light/dark mode and stays visually consistent with everything
|
|
11
|
+
* else Zerotal ships. The only thing the monitor overrides is `--primary`: the
|
|
12
|
+
* panel's own orange, applied on top of the shared palette rather than baked
|
|
13
|
+
* into its markup.
|
|
14
|
+
*
|
|
15
|
+
* Because every component reads `--primary` rather than a literal colour, that
|
|
16
|
+
* one override recolours the whole panel — and an app can re-brand it further by
|
|
17
|
+
* appending its own token CSS.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The monitor's orange, layered over flow-ui's neutral palette. */
|
|
21
|
+
const MONITOR_TOKENS = `
|
|
22
|
+
:root {
|
|
23
|
+
--primary: 21 90% 48%;
|
|
24
|
+
--primary-foreground: 0 0% 100%;
|
|
25
|
+
--ring: 21 90% 48%;
|
|
26
|
+
}
|
|
27
|
+
.dark {
|
|
28
|
+
--primary: 25 95% 58%;
|
|
29
|
+
--primary-foreground: 224 71% 4%;
|
|
30
|
+
--ring: 25 95% 58%;
|
|
31
|
+
}
|
|
32
|
+
`.trim();
|
|
33
|
+
|
|
34
|
+
export class MonitorLayout extends Layout {
|
|
35
|
+
static override get head(): string {
|
|
36
|
+
return flowUiHead("Zerotal · Monitor", { tokensCss: MONITOR_TOKENS });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override render(slot: HtmlNode): HtmlNode {
|
|
40
|
+
return <div class="h-screen bg-background text-foreground antialiased">{slot}</div>;
|
|
41
|
+
}
|
|
42
|
+
}
|