create-rezi 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +30 -10
- package/dist/index.js +31 -16
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts +2 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +35 -36
- package/dist/scaffold.js.map +1 -1
- package/package.json +7 -4
- package/templates/cli-tool/README.md +42 -0
- package/templates/{file-browser → cli-tool}/package.json +3 -2
- package/templates/cli-tool/src/main.ts +1037 -0
- package/templates/dashboard/README.md +30 -7
- package/templates/dashboard/package.json +3 -2
- package/templates/dashboard/src/main.ts +1675 -198
- package/templates/stress-test/README.md +81 -0
- package/templates/{streaming-viewer → stress-test}/package.json +3 -2
- package/templates/stress-test/src/main.ts +1615 -0
- package/dist/__tests__/scaffold.test.d.ts +0 -2
- package/dist/__tests__/scaffold.test.d.ts.map +0 -1
- package/dist/__tests__/scaffold.test.js +0 -29
- package/dist/__tests__/scaffold.test.js.map +0 -1
- package/templates/file-browser/README.md +0 -18
- package/templates/file-browser/src/main.ts +0 -258
- package/templates/form-app/README.md +0 -18
- package/templates/form-app/package.json +0 -23
- package/templates/form-app/src/main.ts +0 -222
- package/templates/streaming-viewer/README.md +0 -17
- package/templates/streaming-viewer/src/main.ts +0 -176
- package/templates/streaming-viewer/tsconfig.json +0 -14
- /package/templates/{file-browser → cli-tool}/tsconfig.json +0 -0
- /package/templates/{form-app → stress-test}/tsconfig.json +0 -0
|
@@ -1,259 +1,1736 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
BadgeVariant,
|
|
3
|
+
StatusType,
|
|
4
|
+
TableColumn,
|
|
5
|
+
TextStyle,
|
|
6
|
+
ThemeDefinition,
|
|
7
|
+
VNode,
|
|
8
|
+
} from "@rezi-ui/core";
|
|
9
|
+
import {
|
|
10
|
+
createApp,
|
|
11
|
+
darkTheme,
|
|
12
|
+
dimmedTheme,
|
|
13
|
+
draculaTheme,
|
|
14
|
+
highContrastTheme,
|
|
15
|
+
lightTheme,
|
|
16
|
+
nordTheme,
|
|
17
|
+
ui,
|
|
18
|
+
} from "@rezi-ui/core";
|
|
2
19
|
import { createNodeBackend } from "@rezi-ui/node";
|
|
3
20
|
|
|
4
21
|
type ServiceStatus = "healthy" | "warning" | "down";
|
|
22
|
+
type Filter = "all" | ServiceStatus;
|
|
23
|
+
type SortKey = "name" | "latencyMs" | "errorRate" | "trafficRpm";
|
|
24
|
+
type SortDirection = "asc" | "desc";
|
|
25
|
+
type ThemeName = "nord" | "dracula" | "dimmed" | "dark" | "light" | "high-contrast";
|
|
5
26
|
|
|
6
27
|
type Service = {
|
|
28
|
+
id: string;
|
|
7
29
|
name: string;
|
|
8
30
|
region: string;
|
|
31
|
+
tier: "edge" | "core" | "stateful";
|
|
9
32
|
status: ServiceStatus;
|
|
10
33
|
latencyMs: number;
|
|
11
34
|
errorRate: number;
|
|
35
|
+
trafficRpm: number;
|
|
36
|
+
cpuPct: number;
|
|
37
|
+
memoryPct: number;
|
|
38
|
+
saturation: number;
|
|
39
|
+
history: readonly number[];
|
|
12
40
|
};
|
|
13
41
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
42
|
+
type Incident = {
|
|
43
|
+
id: number;
|
|
44
|
+
at: string;
|
|
45
|
+
severity: "info" | "warn" | "critical";
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
21
48
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
type State = {
|
|
50
|
+
services: readonly Service[];
|
|
51
|
+
selectedId: string;
|
|
52
|
+
pinnedId: string | null;
|
|
53
|
+
filter: Filter;
|
|
54
|
+
sort: SortKey;
|
|
55
|
+
sortDirection: SortDirection;
|
|
56
|
+
paused: boolean;
|
|
57
|
+
debug: boolean;
|
|
58
|
+
helpOpen: boolean;
|
|
59
|
+
themeName: ThemeName;
|
|
60
|
+
ticks: number;
|
|
61
|
+
updatesApplied: number;
|
|
62
|
+
incidents: readonly Incident[];
|
|
63
|
+
nextIncidentId: number;
|
|
64
|
+
startedAt: number;
|
|
65
|
+
nowMs: number;
|
|
66
|
+
lastUpdateAt: number;
|
|
67
|
+
fleetLatencyHistory: readonly number[];
|
|
68
|
+
fleetErrorHistory: readonly number[];
|
|
69
|
+
fleetTrafficHistory: readonly number[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type ThemeSpec = {
|
|
73
|
+
label: string;
|
|
74
|
+
theme: ThemeDefinition;
|
|
75
|
+
badge: BadgeVariant;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const themeCatalog: Record<ThemeName, ThemeSpec> = {
|
|
79
|
+
nord: { label: "Nord", theme: nordTheme, badge: "info" },
|
|
80
|
+
dracula: { label: "Dracula", theme: draculaTheme, badge: "warning" },
|
|
81
|
+
dimmed: { label: "Dimmed", theme: dimmedTheme, badge: "default" },
|
|
82
|
+
dark: { label: "Dark", theme: darkTheme, badge: "default" },
|
|
83
|
+
light: { label: "Light", theme: lightTheme, badge: "success" },
|
|
84
|
+
"high-contrast": {
|
|
85
|
+
label: "High Contrast",
|
|
86
|
+
theme: highContrastTheme,
|
|
87
|
+
badge: "error",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
27
90
|
|
|
28
|
-
const
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
91
|
+
const themeOrder: readonly ThemeName[] = [
|
|
92
|
+
"nord",
|
|
93
|
+
"dracula",
|
|
94
|
+
"dimmed",
|
|
95
|
+
"dark",
|
|
96
|
+
"light",
|
|
97
|
+
"high-contrast",
|
|
33
98
|
];
|
|
34
99
|
|
|
35
|
-
|
|
100
|
+
const UI_FPS_CAP = 30;
|
|
101
|
+
const TELEMETRY_CADENCE_MS = 1000;
|
|
102
|
+
const TELEMETRY_MAX_DRIFT_MS = TELEMETRY_CADENCE_MS * 2;
|
|
103
|
+
const PRODUCT_NAME = "__APP_NAME__";
|
|
104
|
+
const PRODUCT_TAGLINE = "Streaming edge reliability console";
|
|
105
|
+
const PRODUCT_MISSION =
|
|
106
|
+
"Operate fleet health, incident response, and service recovery from one deterministic terminal console.";
|
|
107
|
+
const PRODUCT_ENVIRONMENT = "Production";
|
|
108
|
+
const PRODUCT_CLUSTER = "global-edge";
|
|
109
|
+
const SHOWCASE_MODE = true;
|
|
110
|
+
const LIVE_SPINNER_VARIANT = "dots" as const;
|
|
111
|
+
const CADENCE_PULSE_FRAMES = Object.freeze([
|
|
112
|
+
"▁",
|
|
113
|
+
"▂",
|
|
114
|
+
"▃",
|
|
115
|
+
"▄",
|
|
116
|
+
"▅",
|
|
117
|
+
"▆",
|
|
118
|
+
"▇",
|
|
119
|
+
"█",
|
|
120
|
+
"▇",
|
|
121
|
+
"▆",
|
|
122
|
+
"▅",
|
|
123
|
+
"▄",
|
|
124
|
+
]);
|
|
125
|
+
const PANEL_PADDING_X = 1;
|
|
126
|
+
const PANEL_PADDING_Y = 0;
|
|
127
|
+
const KPI_SPARKLINE_WIDTH = 20;
|
|
128
|
+
const KPI_PROGRESS_WIDTH = 20;
|
|
129
|
+
const SIGNAL_SPARKLINE_WIDTH = 20;
|
|
130
|
+
const SELECTED_HISTORY_WIDTH = 24;
|
|
131
|
+
const RESOURCE_SPARKLINE_WIDTH = 10;
|
|
132
|
+
const TABLE_REGION_WIDTH = 13;
|
|
133
|
+
const TABLE_STATUS_WIDTH = 13;
|
|
134
|
+
const TABLE_STATUS_LABEL_WIDTH = 7;
|
|
135
|
+
const INCIDENT_VISIBLE_ROWS = 7;
|
|
136
|
+
const INCIDENT_BADGE_WIDTH = 8;
|
|
137
|
+
const INCIDENT_TEXT_MAX_WIDTH = 96;
|
|
138
|
+
const CLUSTER_HEALTH_LABEL_WIDTH = 8;
|
|
139
|
+
const REFRESH_LABEL_WIDTH = 8;
|
|
140
|
+
const SORT_PANEL_LABEL_WIDTH = 11;
|
|
141
|
+
const RATE_LABEL_WIDTH = 7;
|
|
142
|
+
const SUMMARY_ALERT_LABEL_WIDTH = 16;
|
|
143
|
+
type ShortcutSpec = Readonly<{
|
|
144
|
+
keys: string | readonly string[];
|
|
145
|
+
description: string;
|
|
146
|
+
}>;
|
|
36
147
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
148
|
+
const HELP_SHORTCUTS: readonly ShortcutSpec[] = Object.freeze([
|
|
149
|
+
{ keys: ["up", "down", "j", "k"], description: "Move service selection" },
|
|
150
|
+
{ keys: "enter", description: "Pin or unpin selected service" },
|
|
151
|
+
{ keys: ["p", "space"], description: "Pause or resume live stream" },
|
|
152
|
+
{ keys: "f", description: "Cycle status filter" },
|
|
153
|
+
{ keys: "s", description: "Cycle sort field" },
|
|
154
|
+
{ keys: "o", description: "Toggle sort direction" },
|
|
155
|
+
{ keys: "t", description: "Cycle theme preset" },
|
|
156
|
+
{ keys: "d", description: "Toggle debug counters" },
|
|
157
|
+
{ keys: "c", description: "Clear active events feed" },
|
|
158
|
+
{ keys: "q", description: "Exit console" },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
function timeStamp(date = new Date()): string {
|
|
162
|
+
return date.toLocaleTimeString("en-US", { hour12: false });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const seedServices = [
|
|
166
|
+
{
|
|
167
|
+
id: "auth",
|
|
168
|
+
name: "Auth Gateway",
|
|
169
|
+
region: "us-east-1",
|
|
170
|
+
tier: "edge",
|
|
171
|
+
status: "healthy",
|
|
172
|
+
latencyMs: 22,
|
|
173
|
+
errorRate: 0.18,
|
|
174
|
+
trafficRpm: 14250,
|
|
175
|
+
cpuPct: 44,
|
|
176
|
+
memoryPct: 53,
|
|
177
|
+
saturation: 41,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "billing",
|
|
181
|
+
name: "Billing API",
|
|
182
|
+
region: "us-west-2",
|
|
183
|
+
tier: "stateful",
|
|
184
|
+
status: "warning",
|
|
185
|
+
latencyMs: 84,
|
|
186
|
+
errorRate: 1.12,
|
|
187
|
+
trafficRpm: 7340,
|
|
188
|
+
cpuPct: 61,
|
|
189
|
+
memoryPct: 70,
|
|
190
|
+
saturation: 66,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "search",
|
|
194
|
+
name: "Search Index",
|
|
195
|
+
region: "eu-central-1",
|
|
196
|
+
tier: "core",
|
|
197
|
+
status: "healthy",
|
|
198
|
+
latencyMs: 36,
|
|
199
|
+
errorRate: 0.32,
|
|
200
|
+
trafficRpm: 9860,
|
|
201
|
+
cpuPct: 52,
|
|
202
|
+
memoryPct: 58,
|
|
203
|
+
saturation: 52,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "realtime",
|
|
207
|
+
name: "Realtime Fanout",
|
|
208
|
+
region: "ap-south-1",
|
|
209
|
+
tier: "core",
|
|
210
|
+
status: "warning",
|
|
211
|
+
latencyMs: 98,
|
|
212
|
+
errorRate: 1.92,
|
|
213
|
+
trafficRpm: 12320,
|
|
214
|
+
cpuPct: 78,
|
|
215
|
+
memoryPct: 74,
|
|
216
|
+
saturation: 79,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "exports",
|
|
220
|
+
name: "Export Workers",
|
|
221
|
+
region: "us-east-1",
|
|
222
|
+
tier: "stateful",
|
|
223
|
+
status: "down",
|
|
224
|
+
latencyMs: 0,
|
|
225
|
+
errorRate: 8.4,
|
|
226
|
+
trafficRpm: 640,
|
|
227
|
+
cpuPct: 96,
|
|
228
|
+
memoryPct: 91,
|
|
229
|
+
saturation: 97,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "notify",
|
|
233
|
+
name: "Notification Bus",
|
|
234
|
+
region: "eu-west-1",
|
|
235
|
+
tier: "edge",
|
|
236
|
+
status: "healthy",
|
|
237
|
+
latencyMs: 31,
|
|
238
|
+
errorRate: 0.27,
|
|
239
|
+
trafficRpm: 8110,
|
|
240
|
+
cpuPct: 47,
|
|
241
|
+
memoryPct: 55,
|
|
242
|
+
saturation: 48,
|
|
243
|
+
},
|
|
244
|
+
] as const satisfies readonly Omit<Service, "history">[];
|
|
245
|
+
|
|
246
|
+
const initialServices: readonly Service[] = Object.freeze(
|
|
247
|
+
seedServices.map((service) => ({
|
|
248
|
+
...service,
|
|
249
|
+
history: Object.freeze(Array.from({ length: 24 }, () => service.latencyMs)),
|
|
250
|
+
})),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const initialIncidents: readonly Incident[] = Object.freeze([
|
|
254
|
+
{
|
|
255
|
+
id: 1,
|
|
256
|
+
at: timeStamp(),
|
|
257
|
+
severity: "critical",
|
|
258
|
+
message: "Export Workers entered fail-safe mode after queue timeout in us-east-1.",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 2,
|
|
262
|
+
at: timeStamp(),
|
|
263
|
+
severity: "warn",
|
|
264
|
+
message: "Realtime Fanout jitter exceeded 90 ms SLO in ap-south-1.",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 3,
|
|
268
|
+
at: timeStamp(),
|
|
269
|
+
severity: "info",
|
|
270
|
+
message: "Canary deploy #491 promoted to production on global-edge.",
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
const initialNowMs = Date.now();
|
|
275
|
+
|
|
276
|
+
function averageLatency(services: readonly Service[]): number {
|
|
277
|
+
return Math.round(
|
|
278
|
+
services.reduce((total, service) => total + service.latencyMs, 0) /
|
|
279
|
+
Math.max(1, services.length),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function averageErrorRate(services: readonly Service[]): number {
|
|
284
|
+
return round2(
|
|
285
|
+
services.reduce((total, service) => total + service.errorRate, 0) /
|
|
286
|
+
Math.max(1, services.length),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function totalTraffic(services: readonly Service[]): number {
|
|
291
|
+
return services.reduce((total, service) => total + service.trafficRpm, 0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function repeatSeries(value: number, size = 28): readonly number[] {
|
|
295
|
+
return Object.freeze(Array.from({ length: size }, () => value));
|
|
296
|
+
}
|
|
43
297
|
|
|
44
298
|
const app = createApp<State>({
|
|
45
|
-
backend: createNodeBackend(
|
|
299
|
+
backend: createNodeBackend({
|
|
300
|
+
emojiWidthPolicy: "auto",
|
|
301
|
+
fpsCap: UI_FPS_CAP,
|
|
302
|
+
// Keep engine present/poll off the app thread so animated frames don't
|
|
303
|
+
// delay keyboard/mouse routing under load.
|
|
304
|
+
executionMode: "worker",
|
|
305
|
+
}),
|
|
306
|
+
config: { fpsCap: UI_FPS_CAP },
|
|
307
|
+
theme: themeCatalog.nord.theme,
|
|
46
308
|
initialState: {
|
|
47
|
-
|
|
309
|
+
services: initialServices,
|
|
310
|
+
selectedId:
|
|
311
|
+
initialServices.find((service) => service.status === "down")?.id ??
|
|
312
|
+
initialServices[0]?.id ??
|
|
313
|
+
"",
|
|
314
|
+
pinnedId: null,
|
|
48
315
|
filter: "all",
|
|
49
|
-
|
|
50
|
-
|
|
316
|
+
sort: "name",
|
|
317
|
+
sortDirection: "asc",
|
|
318
|
+
paused: false,
|
|
319
|
+
debug: false,
|
|
320
|
+
helpOpen: false,
|
|
321
|
+
themeName: "nord",
|
|
322
|
+
ticks: 0,
|
|
323
|
+
updatesApplied: 0,
|
|
324
|
+
incidents: initialIncidents,
|
|
325
|
+
nextIncidentId: 4,
|
|
326
|
+
startedAt: initialNowMs,
|
|
327
|
+
nowMs: initialNowMs,
|
|
328
|
+
lastUpdateAt: initialNowMs,
|
|
329
|
+
fleetLatencyHistory: repeatSeries(averageLatency(initialServices)),
|
|
330
|
+
fleetErrorHistory: repeatSeries(averageErrorRate(initialServices)),
|
|
331
|
+
fleetTrafficHistory: repeatSeries(totalTraffic(initialServices)),
|
|
51
332
|
},
|
|
52
333
|
});
|
|
53
334
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
335
|
+
function clamp(value: number, min: number, max: number): number {
|
|
336
|
+
return Math.max(min, Math.min(max, value));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function round2(value: number): number {
|
|
340
|
+
return Math.round(value * 100) / 100;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function smoothInt(current: number, target: number, gain: number): number {
|
|
344
|
+
return Math.round(current + (target - current) * gain);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function smoothFloat(current: number, target: number, gain: number): number {
|
|
348
|
+
return current + (target - current) * gain;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function formatTrafficCompact(rpm: number): string {
|
|
352
|
+
if (rpm >= 1000) return `${(rpm / 1000).toFixed(1)}k`;
|
|
353
|
+
return `${rpm}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatTrafficFixed(rpm: number): string {
|
|
357
|
+
return formatTrafficCompact(rpm).padStart(6, " ");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatHzFixed(hz: number): string {
|
|
361
|
+
return `${hz.toFixed(2).padStart(5, " ")} Hz`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatMsFixed(ms: number): string {
|
|
365
|
+
return `${Math.max(0, Math.round(ms)).toString().padStart(4, " ")} ms`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function formatSecondsFixed(seconds: number): string {
|
|
369
|
+
return `${Math.max(0, Math.round(seconds)).toString().padStart(4, " ")} s`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function clipLabel(value: string, maxChars: number): string {
|
|
373
|
+
if (maxChars <= 1) return value.slice(0, 1);
|
|
374
|
+
return value.length > maxChars ? `${value.slice(0, maxChars - 1)}…` : value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function fixedLabel(value: string, maxChars: number, width = maxChars): string {
|
|
378
|
+
return clipLabel(value, maxChars).padEnd(width, " ");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function signedDelta(value: number, digits = 0): string {
|
|
382
|
+
const rounded = digits === 0 ? Math.round(value).toString() : value.toFixed(digits);
|
|
383
|
+
if (value > 0) return `+${rounded}`;
|
|
384
|
+
if (value < 0) return rounded;
|
|
385
|
+
return digits === 0 ? "0" : Number(value).toFixed(digits);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function deltaSeverity(
|
|
389
|
+
value: number,
|
|
390
|
+
warningThreshold: number,
|
|
391
|
+
criticalThreshold: number,
|
|
392
|
+
): BadgeVariant {
|
|
393
|
+
const magnitude = Math.abs(value);
|
|
394
|
+
if (magnitude >= criticalThreshold) return "error";
|
|
395
|
+
if (magnitude >= warningThreshold) return "warning";
|
|
396
|
+
return "success";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function statusBadge(status: ServiceStatus): { text: string; variant: BadgeVariant } {
|
|
400
|
+
if (status === "healthy") return { text: "Healthy", variant: "success" };
|
|
401
|
+
if (status === "warning") return { text: "Warning", variant: "warning" };
|
|
402
|
+
return { text: "Critical", variant: "error" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function incidentBadge(severity: Incident["severity"]): { text: string; variant: BadgeVariant } {
|
|
406
|
+
if (severity === "critical") return { text: "Critical", variant: "error" };
|
|
407
|
+
if (severity === "warn") return { text: "Warning", variant: "warning" };
|
|
408
|
+
return { text: "Info", variant: "info" };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function incidentIcon(severity: Incident["severity"]): string {
|
|
412
|
+
if (severity === "critical") return "status.cross";
|
|
413
|
+
if (severity === "warn") return "status.warning";
|
|
414
|
+
return "status.info";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function statusToIndicator(status: ServiceStatus): StatusType {
|
|
418
|
+
if (status === "healthy") return "online";
|
|
419
|
+
if (status === "warning") return "away";
|
|
420
|
+
return "busy";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function serviceIcon(status: ServiceStatus): string {
|
|
424
|
+
if (status === "healthy") return "status.check";
|
|
425
|
+
if (status === "warning") return "status.dot";
|
|
426
|
+
return "status.cross";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function statusEmoji(status: ServiceStatus): string {
|
|
430
|
+
if (status === "healthy") return "🟢";
|
|
431
|
+
if (status === "warning") return "🟡";
|
|
432
|
+
return "🔴";
|
|
433
|
+
}
|
|
65
434
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
if (status === "warning") return colors.warn;
|
|
69
|
-
return colors.down;
|
|
435
|
+
function statusCellGlyph(_status: ServiceStatus): string {
|
|
436
|
+
return "●";
|
|
70
437
|
}
|
|
71
438
|
|
|
72
|
-
function
|
|
439
|
+
function animationFrame(frames: readonly string[], tick: number): string {
|
|
440
|
+
if (frames.length === 0) return "";
|
|
441
|
+
const index = Math.abs(tick) % frames.length;
|
|
442
|
+
return frames[index] ?? frames[0] ?? "";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function tierSymbol(tier: Service["tier"]): string {
|
|
446
|
+
if (tier === "edge") return "◌";
|
|
447
|
+
if (tier === "core") return "◆";
|
|
448
|
+
return "▣";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function serviceOwner(service: Service): string {
|
|
452
|
+
if (service.tier === "stateful") return "Data Platform";
|
|
453
|
+
if (service.tier === "core") return "Core Runtime";
|
|
454
|
+
return "Edge Runtime";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function serviceRunbook(service: Service): string {
|
|
458
|
+
if (service.status === "down") return "RB-017";
|
|
459
|
+
if (service.status === "warning") return "RB-011";
|
|
460
|
+
return "RB-004";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function serviceSlo(service: Service): string {
|
|
464
|
+
if (service.tier === "stateful") return "150 ms";
|
|
465
|
+
if (service.tier === "core") return "120 ms";
|
|
466
|
+
return "95 ms";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function panel(
|
|
470
|
+
title: string,
|
|
471
|
+
children: readonly VNode[],
|
|
472
|
+
flex = 1,
|
|
473
|
+
style: TextStyle | undefined = undefined,
|
|
474
|
+
): VNode {
|
|
475
|
+
const props =
|
|
476
|
+
style === undefined
|
|
477
|
+
? {
|
|
478
|
+
title,
|
|
479
|
+
flex,
|
|
480
|
+
border: "rounded" as const,
|
|
481
|
+
px: PANEL_PADDING_X,
|
|
482
|
+
py: PANEL_PADDING_Y,
|
|
483
|
+
}
|
|
484
|
+
: {
|
|
485
|
+
title,
|
|
486
|
+
flex,
|
|
487
|
+
border: "rounded" as const,
|
|
488
|
+
px: PANEL_PADDING_X,
|
|
489
|
+
py: PANEL_PADDING_Y,
|
|
490
|
+
style,
|
|
491
|
+
};
|
|
492
|
+
return ui.box(props, children);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function toolbarAction(
|
|
496
|
+
_iconPath: string,
|
|
497
|
+
buttonId: string,
|
|
498
|
+
label: string,
|
|
499
|
+
onPress: () => void,
|
|
500
|
+
): VNode {
|
|
501
|
+
return ui.button({ id: buttonId, label, onPress });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function shortcutLabel(keys: string | readonly string[]): string {
|
|
505
|
+
const parts = Array.isArray(keys) ? keys : [keys];
|
|
506
|
+
return parts.join(" + ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function applyFilter(services: readonly Service[], filter: Filter): Service[] {
|
|
73
510
|
if (filter === "all") return [...services];
|
|
74
|
-
return services.filter((
|
|
511
|
+
return services.filter((service) => service.status === filter);
|
|
75
512
|
}
|
|
76
513
|
|
|
77
|
-
function
|
|
78
|
-
|
|
514
|
+
function statusPriority(status: ServiceStatus): number {
|
|
515
|
+
if (status === "down") return 0;
|
|
516
|
+
if (status === "warning") return 1;
|
|
517
|
+
return 2;
|
|
79
518
|
}
|
|
80
519
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
520
|
+
function sortServices(
|
|
521
|
+
services: readonly Service[],
|
|
522
|
+
sort: SortKey,
|
|
523
|
+
direction: SortDirection,
|
|
524
|
+
): Service[] {
|
|
525
|
+
const weight = direction === "asc" ? 1 : -1;
|
|
526
|
+
const sorted = [...services].sort((a, b) => {
|
|
527
|
+
const severityOrder = statusPriority(a.status) - statusPriority(b.status);
|
|
528
|
+
if (severityOrder !== 0) return severityOrder;
|
|
529
|
+
|
|
530
|
+
let base = 0;
|
|
531
|
+
if (sort === "name") {
|
|
532
|
+
base = a.name.localeCompare(b.name);
|
|
533
|
+
} else if (sort === "latencyMs") {
|
|
534
|
+
base = a.latencyMs - b.latencyMs;
|
|
535
|
+
} else if (sort === "errorRate") {
|
|
536
|
+
base = a.errorRate - b.errorRate;
|
|
537
|
+
} else {
|
|
538
|
+
base = a.trafficRpm - b.trafficRpm;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (base === 0) base = a.name.localeCompare(b.name);
|
|
542
|
+
return base * weight;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return sorted;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function visibleServicesFor(
|
|
549
|
+
services: readonly Service[],
|
|
550
|
+
filter: Filter,
|
|
551
|
+
sort: SortKey,
|
|
552
|
+
direction: SortDirection,
|
|
553
|
+
): Service[] {
|
|
554
|
+
return sortServices(applyFilter(services, filter), sort, direction);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function selectedFromVisible(visible: readonly Service[], selectedId: string): Service | undefined {
|
|
558
|
+
return visible.find((service) => service.id === selectedId) ?? visible[0];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function withResolvedSelection(state: State): State {
|
|
562
|
+
const visible = visibleServicesFor(state.services, state.filter, state.sort, state.sortDirection);
|
|
563
|
+
if (visible.length === 0) return state;
|
|
564
|
+
const selected = selectedFromVisible(visible, state.selectedId);
|
|
565
|
+
if (!selected) return state;
|
|
566
|
+
if (selected.id === state.selectedId) return state;
|
|
567
|
+
return { ...state, selectedId: selected.id };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function moveSelection(state: State, delta: number): State {
|
|
571
|
+
const visible = visibleServicesFor(state.services, state.filter, state.sort, state.sortDirection);
|
|
572
|
+
if (visible.length === 0) return state;
|
|
573
|
+
|
|
574
|
+
const current = visible.findIndex((service) => service.id === state.selectedId);
|
|
575
|
+
const nextIndex = clamp((current < 0 ? 0 : current) + delta, 0, visible.length - 1);
|
|
576
|
+
const next = visible[nextIndex];
|
|
577
|
+
if (!next) return state;
|
|
578
|
+
if (next.id === state.selectedId) return state;
|
|
579
|
+
return { ...state, selectedId: next.id };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function cycleFilter(filter: Filter): Filter {
|
|
583
|
+
if (filter === "all") return "warning";
|
|
584
|
+
if (filter === "warning") return "down";
|
|
585
|
+
if (filter === "down") return "healthy";
|
|
586
|
+
return "all";
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function filterLabel(filter: Filter): string {
|
|
590
|
+
if (filter === "all") return "ALL";
|
|
591
|
+
if (filter === "healthy") return "HEALTHY";
|
|
592
|
+
if (filter === "warning") return "WARNING";
|
|
593
|
+
return "DOWN";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function defaultDirectionForSort(sort: SortKey): SortDirection {
|
|
597
|
+
return sort === "name" ? "asc" : "desc";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function cycleSort(sort: SortKey): SortKey {
|
|
601
|
+
const order: readonly SortKey[] = ["name", "latencyMs", "errorRate", "trafficRpm"];
|
|
602
|
+
const index = order.indexOf(sort);
|
|
603
|
+
const nextIndex = index < 0 ? 0 : (index + 1) % order.length;
|
|
604
|
+
return order[nextIndex] ?? "name";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function toSortKey(value: string): SortKey | null {
|
|
608
|
+
if (value === "name") return "name";
|
|
609
|
+
if (value === "latencyMs") return "latencyMs";
|
|
610
|
+
if (value === "errorRate") return "errorRate";
|
|
611
|
+
if (value === "trafficRpm") return "trafficRpm";
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function sortLabel(sort: SortKey): string {
|
|
616
|
+
if (sort === "latencyMs") return "Latency SLO";
|
|
617
|
+
if (sort === "errorRate") return "Error Rate";
|
|
618
|
+
if (sort === "trafficRpm") return "Traffic";
|
|
619
|
+
return "Service";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function deriveStatus(latencyMs: number, errorRate: number, saturation: number): ServiceStatus {
|
|
623
|
+
if (errorRate >= 4.2 || saturation >= 95 || latencyMs >= 170) {
|
|
624
|
+
return "down";
|
|
625
|
+
}
|
|
626
|
+
if (errorRate >= 1.4 || saturation >= 82 || latencyMs >= 95) {
|
|
627
|
+
return "warning";
|
|
628
|
+
}
|
|
629
|
+
return "healthy";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function pushSeries(history: readonly number[], value: number, maxSize = 28): readonly number[] {
|
|
633
|
+
return Object.freeze([...history, value].slice(-maxSize));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function pushHistory(history: readonly number[], latencyMs: number): readonly number[] {
|
|
637
|
+
return pushSeries(history, latencyMs, 24);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function nextThemeName(current: ThemeName): ThemeName {
|
|
641
|
+
const index = themeOrder.indexOf(current);
|
|
642
|
+
const nextIndex = index < 0 ? 0 : (index + 1) % themeOrder.length;
|
|
643
|
+
return themeOrder[nextIndex] ?? themeOrder[0] ?? "nord";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function cycleThemeAction(): void {
|
|
647
|
+
let next: ThemeName = "nord";
|
|
648
|
+
app.update((state) => {
|
|
649
|
+
next = nextThemeName(state.themeName);
|
|
650
|
+
if (next === state.themeName) return state;
|
|
651
|
+
return {
|
|
652
|
+
...state,
|
|
653
|
+
themeName: next,
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
app.setTheme(themeCatalog[next].theme);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function togglePauseAction(): void {
|
|
660
|
+
app.update((state) => ({ ...state, paused: !state.paused }));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function cycleFilterAction(): void {
|
|
664
|
+
app.update((state) =>
|
|
665
|
+
withResolvedSelection({
|
|
666
|
+
...state,
|
|
667
|
+
filter: cycleFilter(state.filter),
|
|
668
|
+
}),
|
|
92
669
|
);
|
|
93
670
|
}
|
|
94
671
|
|
|
672
|
+
function cycleSortAction(): void {
|
|
673
|
+
app.update((state) => {
|
|
674
|
+
const nextSort = cycleSort(state.sort);
|
|
675
|
+
return withResolvedSelection({
|
|
676
|
+
...state,
|
|
677
|
+
sort: nextSort,
|
|
678
|
+
sortDirection: defaultDirectionForSort(nextSort),
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function toggleSortDirectionAction(): void {
|
|
684
|
+
app.update((state) =>
|
|
685
|
+
withResolvedSelection({
|
|
686
|
+
...state,
|
|
687
|
+
sortDirection: state.sortDirection === "asc" ? "desc" : "asc",
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function toggleDebugAction(): void {
|
|
693
|
+
app.update((state) => ({ ...state, debug: !state.debug }));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function clearIncidentsAction(): void {
|
|
697
|
+
app.update((state) => ({ ...state, incidents: Object.freeze([]) }));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function togglePinAction(): void {
|
|
701
|
+
app.update((state) => {
|
|
702
|
+
const visible = visibleServicesFor(
|
|
703
|
+
state.services,
|
|
704
|
+
state.filter,
|
|
705
|
+
state.sort,
|
|
706
|
+
state.sortDirection,
|
|
707
|
+
);
|
|
708
|
+
const selected = selectedFromVisible(visible, state.selectedId);
|
|
709
|
+
if (!selected) return state;
|
|
710
|
+
return {
|
|
711
|
+
...state,
|
|
712
|
+
pinnedId: state.pinnedId === selected.id ? null : selected.id,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function openHelpAction(): void {
|
|
718
|
+
app.update((state) => ({ ...state, helpOpen: true }));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function closeHelpAction(): void {
|
|
722
|
+
app.update((state) => ({ ...state, helpOpen: false }));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function simulateTick(state: State, nowMs: number): State {
|
|
726
|
+
if (state.paused) return state;
|
|
727
|
+
const nextTick = state.ticks + 1;
|
|
728
|
+
|
|
729
|
+
let nextIncidentId = state.nextIncidentId;
|
|
730
|
+
const generated: Incident[] = [];
|
|
731
|
+
|
|
732
|
+
const addIncident = (severity: Incident["severity"], message: string): void => {
|
|
733
|
+
generated.push({ id: nextIncidentId, at: timeStamp(), severity, message });
|
|
734
|
+
nextIncidentId += 1;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const nextServices = state.services.map((service, index) => {
|
|
738
|
+
const phaseA = nextTick * 0.18 + index * 1.17;
|
|
739
|
+
const phaseB = nextTick * 0.09 + index * 0.63;
|
|
740
|
+
const latencySwingA = SHOWCASE_MODE ? 6 : 9;
|
|
741
|
+
const latencySwingB = SHOWCASE_MODE ? 3 : 4;
|
|
742
|
+
const errorSwing = SHOWCASE_MODE ? 0.12 : 0.2;
|
|
743
|
+
const trafficSwingA = SHOWCASE_MODE ? 300 : 420;
|
|
744
|
+
const trafficSwingB = SHOWCASE_MODE ? 170 : 260;
|
|
745
|
+
const cpuSwing = SHOWCASE_MODE ? 4 : 6;
|
|
746
|
+
const memSwing = SHOWCASE_MODE ? 3 : 5;
|
|
747
|
+
const satSwing = SHOWCASE_MODE ? 3 : 4;
|
|
748
|
+
|
|
749
|
+
const targetLatency = clamp(
|
|
750
|
+
Math.round(
|
|
751
|
+
service.latencyMs + Math.sin(phaseA) * latencySwingA + Math.cos(phaseB) * latencySwingB,
|
|
752
|
+
),
|
|
753
|
+
12,
|
|
754
|
+
280,
|
|
755
|
+
);
|
|
756
|
+
const latencyMs = clamp(smoothInt(service.latencyMs, targetLatency, 0.22), 12, 280);
|
|
757
|
+
|
|
758
|
+
const targetError = clamp(service.errorRate + Math.sin(phaseB) * errorSwing, 0.03, 9.9);
|
|
759
|
+
const errorRate = round2(clamp(smoothFloat(service.errorRate, targetError, 0.2), 0.03, 9.9));
|
|
760
|
+
|
|
761
|
+
const targetTraffic = clamp(
|
|
762
|
+
Math.round(
|
|
763
|
+
service.trafficRpm + Math.sin(phaseA) * trafficSwingA + Math.cos(phaseB) * trafficSwingB,
|
|
764
|
+
),
|
|
765
|
+
400,
|
|
766
|
+
34000,
|
|
767
|
+
);
|
|
768
|
+
const trafficRpm = clamp(smoothInt(service.trafficRpm, targetTraffic, 0.2), 400, 34000);
|
|
769
|
+
|
|
770
|
+
const targetCpu = clamp(Math.round(service.cpuPct + Math.sin(phaseA) * cpuSwing), 18, 99);
|
|
771
|
+
const targetMem = clamp(Math.round(service.memoryPct + Math.cos(phaseB) * memSwing), 16, 99);
|
|
772
|
+
const targetSat = clamp(Math.round(service.saturation + Math.sin(phaseA) * satSwing), 14, 99);
|
|
773
|
+
|
|
774
|
+
const cpuPct = clamp(smoothInt(service.cpuPct, targetCpu, 0.24), 18, 99);
|
|
775
|
+
const memoryPct = clamp(smoothInt(service.memoryPct, targetMem, 0.22), 16, 99);
|
|
776
|
+
const saturation = clamp(smoothInt(service.saturation, targetSat, 0.24), 14, 99);
|
|
777
|
+
|
|
778
|
+
const status = deriveStatus(latencyMs, errorRate, saturation);
|
|
779
|
+
|
|
780
|
+
if (status !== service.status) {
|
|
781
|
+
addIncident(
|
|
782
|
+
status === "down" ? "critical" : status === "warning" ? "warn" : "info",
|
|
783
|
+
`${service.name} health changed ${service.status.toUpperCase()} -> ${status.toUpperCase()}.`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (latencyMs >= 145 && status === "warning" && nextTick % 18 === (index + 3) % 18) {
|
|
788
|
+
addIncident("warn", `${service.name} crossed ${latencyMs} ms p95 latency SLO.`);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
...service,
|
|
793
|
+
status,
|
|
794
|
+
latencyMs,
|
|
795
|
+
errorRate,
|
|
796
|
+
trafficRpm,
|
|
797
|
+
cpuPct,
|
|
798
|
+
memoryPct,
|
|
799
|
+
saturation,
|
|
800
|
+
history: pushHistory(service.history, latencyMs),
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
if (nextTick % 16 === 0 && nextServices.length > 0) {
|
|
805
|
+
const hottest = nextServices.reduce((prev, curr) =>
|
|
806
|
+
curr.saturation > prev.saturation ? curr : prev,
|
|
807
|
+
);
|
|
808
|
+
addIncident("info", `${hottest.name} saturation is now ${hottest.saturation}%.`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const fleetLatency = averageLatency(nextServices);
|
|
812
|
+
const fleetError = averageErrorRate(nextServices);
|
|
813
|
+
const fleetTraffic = totalTraffic(nextServices);
|
|
814
|
+
|
|
815
|
+
const nextState = withResolvedSelection({
|
|
816
|
+
...state,
|
|
817
|
+
services: nextServices,
|
|
818
|
+
ticks: nextTick,
|
|
819
|
+
updatesApplied: state.updatesApplied + 1,
|
|
820
|
+
incidents: Object.freeze([...generated, ...state.incidents].slice(0, 12)),
|
|
821
|
+
nextIncidentId,
|
|
822
|
+
nowMs,
|
|
823
|
+
lastUpdateAt: nowMs,
|
|
824
|
+
fleetLatencyHistory: pushSeries(state.fleetLatencyHistory, fleetLatency, 30),
|
|
825
|
+
fleetErrorHistory: pushSeries(state.fleetErrorHistory, fleetError, 30),
|
|
826
|
+
fleetTrafficHistory: pushSeries(state.fleetTrafficHistory, fleetTraffic, 30),
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
return nextState;
|
|
830
|
+
}
|
|
831
|
+
|
|
95
832
|
app.view((state) => {
|
|
96
|
-
const visible =
|
|
97
|
-
const selected = visible
|
|
98
|
-
const pinned = state.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
833
|
+
const visible = visibleServicesFor(state.services, state.filter, state.sort, state.sortDirection);
|
|
834
|
+
const selected = selectedFromVisible(visible, state.selectedId) ?? state.services[0];
|
|
835
|
+
const pinned = state.pinnedId
|
|
836
|
+
? (state.services.find((service) => service.id === state.pinnedId) ?? null)
|
|
837
|
+
: null;
|
|
838
|
+
|
|
839
|
+
const healthyCount = state.services.filter((service) => service.status === "healthy").length;
|
|
840
|
+
const warningCount = state.services.filter((service) => service.status === "warning").length;
|
|
841
|
+
const downCount = state.services.filter((service) => service.status === "down").length;
|
|
842
|
+
const overallStatus: ServiceStatus =
|
|
843
|
+
downCount > 0 ? "down" : warningCount > 0 ? "warning" : "healthy";
|
|
844
|
+
|
|
845
|
+
const overallBadge = statusBadge(overallStatus);
|
|
846
|
+
const clusterHealthLabel =
|
|
847
|
+
overallStatus === "down" ? "Critical" : overallStatus === "warning" ? "Degraded" : "Healthy";
|
|
848
|
+
|
|
849
|
+
const themeSpec = themeCatalog[state.themeName];
|
|
850
|
+
const palette = themeSpec.theme.colors;
|
|
851
|
+
const rootStyle: TextStyle = { bg: palette.bg.base, fg: palette.fg.primary };
|
|
852
|
+
const panelStyle: TextStyle = { bg: palette.bg.elevated, fg: palette.fg.primary };
|
|
853
|
+
const stripStyle: TextStyle = { bg: palette.bg.subtle, fg: palette.fg.primary };
|
|
854
|
+
const sectionLabelStyle: TextStyle = { fg: palette.fg.secondary, bold: true };
|
|
855
|
+
const metaStyle: TextStyle = { fg: palette.fg.secondary, dim: true };
|
|
856
|
+
const quietStyle: TextStyle = { fg: palette.fg.muted, dim: true };
|
|
857
|
+
const accentStyle: TextStyle = { fg: palette.accent.primary };
|
|
858
|
+
|
|
859
|
+
const avgLatency = state.fleetLatencyHistory[state.fleetLatencyHistory.length - 1] ?? 0;
|
|
860
|
+
const avgError = state.fleetErrorHistory[state.fleetErrorHistory.length - 1] ?? 0;
|
|
861
|
+
const avgTraffic = state.fleetTrafficHistory[state.fleetTrafficHistory.length - 1] ?? 0;
|
|
862
|
+
const prevLatency = state.fleetLatencyHistory[state.fleetLatencyHistory.length - 2] ?? avgLatency;
|
|
863
|
+
const prevError = state.fleetErrorHistory[state.fleetErrorHistory.length - 2] ?? avgError;
|
|
864
|
+
const prevTraffic = state.fleetTrafficHistory[state.fleetTrafficHistory.length - 2] ?? avgTraffic;
|
|
865
|
+
const latencyDelta = avgLatency - prevLatency;
|
|
866
|
+
const errorDelta = avgError - prevError;
|
|
867
|
+
const trafficDelta = avgTraffic - prevTraffic;
|
|
868
|
+
const cadencePulse = animationFrame(CADENCE_PULSE_FRAMES, state.ticks);
|
|
869
|
+
|
|
870
|
+
const avgCpu = Math.round(
|
|
871
|
+
state.services.reduce((total, service) => total + service.cpuPct, 0) /
|
|
872
|
+
Math.max(1, state.services.length),
|
|
873
|
+
);
|
|
874
|
+
const avgMem = Math.round(
|
|
875
|
+
state.services.reduce((total, service) => total + service.memoryPct, 0) /
|
|
876
|
+
Math.max(1, state.services.length),
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const wallNowMs = state.paused ? Date.now() : state.nowMs;
|
|
880
|
+
const uptimeSeconds = Math.max(1, Math.floor((wallNowMs - state.startedAt) / 1000));
|
|
881
|
+
const updateRate = state.updatesApplied / uptimeSeconds;
|
|
882
|
+
const stalenessMs = wallNowMs - state.lastUpdateAt;
|
|
883
|
+
const liveCadenceMs = Math.max(1, Math.round(1000 / Math.max(updateRate, 0.1)));
|
|
884
|
+
const stalenessSeconds = Math.max(0, Math.round(stalenessMs / 1000));
|
|
885
|
+
const updateRateLabel = formatHzFixed(updateRate);
|
|
886
|
+
const cadenceLabel = formatMsFixed(liveCadenceMs);
|
|
887
|
+
const staleLabel = formatSecondsFixed(stalenessSeconds);
|
|
888
|
+
|
|
889
|
+
const themeLabel = fixedLabel(themeSpec.label, 12, 12);
|
|
890
|
+
const pinnedLabel = pinned ? fixedLabel(pinned.name, 16, 16) : fixedLabel("none", 16, 16);
|
|
891
|
+
const selectedLabel = selected ? fixedLabel(selected.name, 18, 18) : "";
|
|
892
|
+
const incidentCountLabel = String(state.incidents.length).padStart(2, " ");
|
|
893
|
+
const filterBadgeLabel = fixedLabel(filterLabel(state.filter), 7, 7);
|
|
894
|
+
const sortBadgeLabel = `${fixedLabel(sortLabel(state.sort), 10, 10)} ${state.sortDirection === "asc" ? "↑" : "↓"}`;
|
|
895
|
+
const sortPanelLabel = `${fixedLabel(sortLabel(state.sort), SORT_PANEL_LABEL_WIDTH, SORT_PANEL_LABEL_WIDTH)} ${state.sortDirection === "asc" ? "↑" : "↓"}`;
|
|
896
|
+
const clusterHealthFixedLabel = fixedLabel(
|
|
897
|
+
clusterHealthLabel.toUpperCase(),
|
|
898
|
+
CLUSTER_HEALTH_LABEL_WIDTH,
|
|
899
|
+
CLUSTER_HEALTH_LABEL_WIDTH,
|
|
900
|
+
);
|
|
901
|
+
const refreshLabel = state.paused
|
|
902
|
+
? fixedLabel("paused", REFRESH_LABEL_WIDTH, REFRESH_LABEL_WIDTH)
|
|
903
|
+
: fixedLabel(cadenceLabel.trimStart(), REFRESH_LABEL_WIDTH, REFRESH_LABEL_WIDTH);
|
|
904
|
+
const rateLabel = fixedLabel(updateRateLabel.trimStart(), RATE_LABEL_WIDTH, RATE_LABEL_WIDTH);
|
|
905
|
+
const syncValueLabel = refreshLabel;
|
|
906
|
+
const latencyValueLabel = `${String(Math.round(avgLatency)).padStart(3, " ")} ms`;
|
|
907
|
+
const errorValueLabel = `${avgError.toFixed(2).padStart(5, " ")}%`;
|
|
908
|
+
const throughputValueLabel = `${formatTrafficFixed(avgTraffic)} rpm`;
|
|
909
|
+
|
|
910
|
+
const cadenceVariant: BadgeVariant =
|
|
911
|
+
state.paused || stalenessMs > Math.max(TELEMETRY_CADENCE_MS * 2, liveCadenceMs * 2)
|
|
912
|
+
? "warning"
|
|
913
|
+
: "success";
|
|
914
|
+
const latencyDeltaVariant = deltaSeverity(latencyDelta, 6, 12);
|
|
915
|
+
const errorDeltaVariant = deltaSeverity(errorDelta, 0.15, 0.4);
|
|
916
|
+
const trafficDeltaVariant = deltaSeverity(trafficDelta, 500, 1100);
|
|
917
|
+
const loadVariant: BadgeVariant =
|
|
918
|
+
avgCpu >= 85 || avgMem >= 88 ? "error" : avgCpu >= 72 ? "warning" : "success";
|
|
919
|
+
const freshnessRatio = state.paused
|
|
920
|
+
? 0
|
|
921
|
+
: clamp(1 - stalenessMs / Math.max(TELEMETRY_CADENCE_MS * 2, liveCadenceMs * 2), 0, 1);
|
|
922
|
+
const latencyRatio = clamp(avgLatency / 180, 0, 1);
|
|
923
|
+
const errorRatio = clamp(avgError / 5, 0, 1);
|
|
924
|
+
const trafficRatio = clamp(avgTraffic / 60000, 0, 1);
|
|
925
|
+
const loadRatio = clamp(Math.max(avgCpu, avgMem) / 100, 0, 1);
|
|
926
|
+
|
|
927
|
+
const kpiTrackStyle: TextStyle = { fg: palette.border.subtle };
|
|
928
|
+
const freshnessBarStyle: TextStyle = { fg: palette.accent.primary };
|
|
929
|
+
const latencyBarStyle: TextStyle = { fg: palette.warning };
|
|
930
|
+
const errorBarStyle: TextStyle = { fg: palette.error };
|
|
931
|
+
const throughputBarStyle: TextStyle = { fg: palette.success };
|
|
932
|
+
const loadBarStyle: TextStyle = { fg: palette.accent.secondary };
|
|
933
|
+
|
|
934
|
+
const summaryBanner: Readonly<{
|
|
935
|
+
icon: string;
|
|
936
|
+
title: string;
|
|
937
|
+
message: string;
|
|
938
|
+
variant: BadgeVariant;
|
|
939
|
+
}> =
|
|
940
|
+
downCount > 0
|
|
941
|
+
? {
|
|
942
|
+
icon: "status.cross",
|
|
943
|
+
title: "CRITICAL OUTAGE",
|
|
944
|
+
message: `${String(downCount).padStart(2, " ")} services down - escalate and reroute traffic.`,
|
|
945
|
+
variant: "error",
|
|
946
|
+
}
|
|
947
|
+
: warningCount > 0
|
|
948
|
+
? {
|
|
949
|
+
icon: "status.warning",
|
|
950
|
+
title: "DEGRADED OPS",
|
|
951
|
+
message: `${String(warningCount).padStart(2, " ")} services degraded - monitor burn and prepare mitigation.`,
|
|
952
|
+
variant: "warning",
|
|
953
|
+
}
|
|
954
|
+
: {
|
|
955
|
+
icon: "status.check",
|
|
956
|
+
title: "HEALTHY FLEET",
|
|
957
|
+
message: "All services are within expected SLO bounds.",
|
|
958
|
+
variant: "success",
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const runbookPlan: Readonly<{
|
|
962
|
+
title: string;
|
|
963
|
+
variant: BadgeVariant;
|
|
964
|
+
summary: string;
|
|
965
|
+
step1: string;
|
|
966
|
+
step2: string;
|
|
967
|
+
step3: string;
|
|
968
|
+
}> =
|
|
969
|
+
overallStatus === "down"
|
|
970
|
+
? {
|
|
971
|
+
title: "Incident Commander",
|
|
972
|
+
variant: "error",
|
|
973
|
+
summary: "P1 path active for failing services.",
|
|
974
|
+
step1: "Page primary on-call and assign commander.",
|
|
975
|
+
step2: "Reroute traffic from failing region.",
|
|
976
|
+
step3: "Validate queue depth and upstream deps.",
|
|
977
|
+
}
|
|
978
|
+
: overallStatus === "warning"
|
|
979
|
+
? {
|
|
980
|
+
title: "Heightened Monitoring",
|
|
981
|
+
variant: "warning",
|
|
982
|
+
summary: "No page yet, active watch required.",
|
|
983
|
+
step1: "Track p95 latency and error trend.",
|
|
984
|
+
step2: "Prepare rollback + canary hold.",
|
|
985
|
+
step3: "Escalate if burn crosses threshold.",
|
|
986
|
+
}
|
|
987
|
+
: {
|
|
988
|
+
title: "Steady State",
|
|
989
|
+
variant: "success",
|
|
990
|
+
summary: "Runbook idle, baseline operations.",
|
|
991
|
+
step1: "Observe release health indicators.",
|
|
992
|
+
step2: "Review event feed for anomalies.",
|
|
993
|
+
step3: "Keep incident channel clear.",
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const pulseCard = (
|
|
997
|
+
title: string,
|
|
998
|
+
value: string,
|
|
999
|
+
variant: BadgeVariant,
|
|
1000
|
+
body: readonly VNode[],
|
|
1001
|
+
): VNode =>
|
|
1002
|
+
ui.box(
|
|
1003
|
+
{
|
|
1004
|
+
border: "rounded",
|
|
1005
|
+
px: PANEL_PADDING_X,
|
|
1006
|
+
py: PANEL_PADDING_Y,
|
|
1007
|
+
flex: 1,
|
|
1008
|
+
minWidth: 24,
|
|
1009
|
+
style: panelStyle,
|
|
1010
|
+
},
|
|
1011
|
+
[
|
|
1012
|
+
ui.column({ gap: 1 }, [
|
|
1013
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1014
|
+
ui.text(title, { style: sectionLabelStyle }),
|
|
1015
|
+
ui.spacer({ flex: 1 }),
|
|
1016
|
+
ui.badge(value, { variant }),
|
|
1017
|
+
]),
|
|
1018
|
+
...body,
|
|
1019
|
+
]),
|
|
1020
|
+
],
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
const statusCellStyle = (status: ServiceStatus): TextStyle => {
|
|
1024
|
+
if (status === "healthy") return { fg: palette.success, bold: true };
|
|
1025
|
+
if (status === "warning") return { fg: palette.warning, bold: true };
|
|
1026
|
+
return { fg: palette.error, bold: true };
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const tableColumns: readonly TableColumn<Service>[] = [
|
|
1030
|
+
{
|
|
1031
|
+
key: "name",
|
|
1032
|
+
header: "Service",
|
|
1033
|
+
flex: 2,
|
|
1034
|
+
minWidth: 30,
|
|
1035
|
+
sortable: true,
|
|
1036
|
+
render: (_, row) =>
|
|
1037
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1038
|
+
ui.text(row.id === state.selectedId ? "▸" : "·", {
|
|
1039
|
+
style: row.id === state.selectedId ? accentStyle : quietStyle,
|
|
1040
|
+
}),
|
|
1041
|
+
ui.icon(serviceIcon(row.status)),
|
|
1042
|
+
ui.text(row.name, {
|
|
1043
|
+
style: { bold: row.id === state.selectedId || row.id === state.pinnedId },
|
|
1044
|
+
}),
|
|
1045
|
+
ui.text(tierSymbol(row.tier), { style: quietStyle }),
|
|
1046
|
+
row.id === state.pinnedId ? ui.icon("ui.star") : null,
|
|
1047
|
+
]),
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
key: "region",
|
|
1051
|
+
header: "Region",
|
|
1052
|
+
width: TABLE_REGION_WIDTH,
|
|
1053
|
+
overflow: "clip",
|
|
1054
|
+
render: (_, row) => ui.text(fixedLabel(row.region, 10, 10), { style: metaStyle }),
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
key: "status",
|
|
1058
|
+
header: "Health",
|
|
1059
|
+
width: TABLE_STATUS_WIDTH,
|
|
1060
|
+
overflow: "clip",
|
|
1061
|
+
render: (_, row) =>
|
|
1062
|
+
ui.text(
|
|
1063
|
+
`${statusCellGlyph(row.status)} ${fixedLabel(row.status.toUpperCase(), TABLE_STATUS_LABEL_WIDTH, TABLE_STATUS_LABEL_WIDTH)}`,
|
|
1064
|
+
{ style: statusCellStyle(row.status) },
|
|
1065
|
+
),
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
key: "latencyMs",
|
|
1069
|
+
header: "P95 ms",
|
|
1070
|
+
width: 8,
|
|
1071
|
+
align: "right",
|
|
1072
|
+
sortable: true,
|
|
1073
|
+
render: (_, row) =>
|
|
1074
|
+
ui.text(`${row.latencyMs}`.padStart(3, " "), { style: { bold: row.latencyMs >= 110 } }),
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
key: "errorRate",
|
|
1078
|
+
header: "Err %",
|
|
1079
|
+
width: 8,
|
|
1080
|
+
align: "right",
|
|
1081
|
+
sortable: true,
|
|
1082
|
+
render: (_, row) =>
|
|
1083
|
+
ui.text(row.errorRate.toFixed(2).padStart(5, " "), {
|
|
1084
|
+
style: { bold: row.errorRate >= 1.4 },
|
|
1085
|
+
}),
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
key: "trafficRpm",
|
|
1089
|
+
header: "RPM",
|
|
1090
|
+
width: 9,
|
|
1091
|
+
align: "right",
|
|
1092
|
+
sortable: true,
|
|
1093
|
+
render: (_, row) => ui.text(formatTrafficFixed(row.trafficRpm)),
|
|
1094
|
+
},
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
const selectedBadge = statusBadge(selected?.status ?? "healthy");
|
|
1098
|
+
const selectedErrorBudget = clamp((selected?.errorRate ?? 0) / 5, 0, 1);
|
|
1099
|
+
const selectedLatencyBudget = clamp((selected?.latencyMs ?? 0) / 180, 0, 1);
|
|
1100
|
+
const selectedPanelName = selected ? fixedLabel(selected.name, 18, 18) : "";
|
|
1101
|
+
const selectedPanelStatus = fixedLabel(selectedBadge.text, 8, 8);
|
|
1102
|
+
const selectedPanelRegion = selected ? fixedLabel(selected.region, 10, 10) : "";
|
|
1103
|
+
const selectedPanelTier = selected ? fixedLabel(selected.tier, 8, 8) : "";
|
|
1104
|
+
const selectedOwner = selected ? fixedLabel(serviceOwner(selected), 13, 13) : "";
|
|
1105
|
+
const selectedRunbook = selected ? fixedLabel(serviceRunbook(selected), 6, 6) : "";
|
|
1106
|
+
const selectedSlo = selected ? fixedLabel(serviceSlo(selected), 6, 6) : "";
|
|
1107
|
+
|
|
1108
|
+
const inspectorGuidance: Readonly<{
|
|
1109
|
+
title: string;
|
|
1110
|
+
message: string;
|
|
1111
|
+
variant: BadgeVariant;
|
|
1112
|
+
}> =
|
|
1113
|
+
!selected || selected.status === "healthy"
|
|
1114
|
+
? {
|
|
1115
|
+
title: "Nominal Service",
|
|
1116
|
+
message: "No escalation required. Continue observing baseline telemetry.",
|
|
1117
|
+
variant: "success",
|
|
1118
|
+
}
|
|
1119
|
+
: selected.status === "warning"
|
|
1120
|
+
? {
|
|
1121
|
+
title: "Watch Closely",
|
|
1122
|
+
message: "Track latency and error trend. Prepare mitigation if burn accelerates.",
|
|
1123
|
+
variant: "warning",
|
|
1124
|
+
}
|
|
1125
|
+
: {
|
|
1126
|
+
title: "Escalate Immediately",
|
|
1127
|
+
message: "Page on-call SRE, shift traffic, and inspect queue + dependency health.",
|
|
1128
|
+
variant: "error",
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const incidentBadgeLabel = (severity: Incident["severity"]): string =>
|
|
1132
|
+
fixedLabel(incidentBadge(severity).text, INCIDENT_BADGE_WIDTH, INCIDENT_BADGE_WIDTH);
|
|
1133
|
+
|
|
1134
|
+
const helpShortcutRow = (keys: string | readonly string[], description: string): VNode =>
|
|
1135
|
+
ui.row({ gap: 2, items: "center" }, [
|
|
1136
|
+
ui.badge(fixedLabel(shortcutLabel(keys), 19, 19), { variant: "info" }),
|
|
1137
|
+
ui.text(description, { style: metaStyle }),
|
|
1138
|
+
]);
|
|
1139
|
+
|
|
1140
|
+
const mainContent = ui.column({ flex: 1, p: 1, gap: 1, items: "stretch", style: rootStyle }, [
|
|
1141
|
+
ui.box({ border: "rounded", px: PANEL_PADDING_X, py: PANEL_PADDING_Y, style: stripStyle }, [
|
|
1142
|
+
ui.column({ gap: 1 }, [
|
|
1143
|
+
ui.row({ items: "center", gap: 1, wrap: true }, [
|
|
1144
|
+
ui.row({ gap: 2, items: "center" }, [
|
|
1145
|
+
ui.text("🛰"),
|
|
1146
|
+
ui.text(PRODUCT_NAME, { variant: "heading" }),
|
|
1147
|
+
ui.tag(`Env ${PRODUCT_ENVIRONMENT}`, { variant: "warning" }),
|
|
1148
|
+
ui.status(state.paused ? "away" : "online", {
|
|
1149
|
+
label: state.paused ? "Paused" : "Streaming",
|
|
1150
|
+
}),
|
|
1151
|
+
]),
|
|
1152
|
+
ui.spacer({ flex: 1 }),
|
|
1153
|
+
ui.badge(`Cluster Health ${clusterHealthFixedLabel}`, { variant: overallBadge.variant }),
|
|
1154
|
+
]),
|
|
1155
|
+
ui.text(PRODUCT_TAGLINE, { style: sectionLabelStyle }),
|
|
1156
|
+
ui.row({ justify: "between", items: "center", gap: 1, wrap: true }, [
|
|
1157
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1158
|
+
ui.text(`Cluster ${PRODUCT_CLUSTER}`, { style: metaStyle }),
|
|
1159
|
+
ui.text("·", { style: quietStyle }),
|
|
1160
|
+
ui.text(`Refresh ${refreshLabel}`, { style: metaStyle }),
|
|
1161
|
+
ui.text("·", { style: quietStyle }),
|
|
1162
|
+
ui.text(`Rate ${rateLabel}`, { style: metaStyle }),
|
|
1163
|
+
]),
|
|
1164
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1165
|
+
SHOWCASE_MODE ? ui.tag("Showcase Mode", { variant: "info" }) : null,
|
|
1166
|
+
ui.tag(`Theme ${themeLabel}`, { variant: themeSpec.badge }),
|
|
1167
|
+
ui.tag(`Pinned ${pinnedLabel}`, { variant: pinned ? "info" : "default" }),
|
|
1168
|
+
]),
|
|
1169
|
+
]),
|
|
108
1170
|
]),
|
|
109
1171
|
]),
|
|
110
1172
|
|
|
111
|
-
ui.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}),
|
|
1173
|
+
ui.box({ border: "rounded", px: PANEL_PADDING_X, py: PANEL_PADDING_Y, style: stripStyle }, [
|
|
1174
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1175
|
+
ui.icon(summaryBanner.icon),
|
|
1176
|
+
ui.badge(
|
|
1177
|
+
`[ ${fixedLabel(summaryBanner.title, SUMMARY_ALERT_LABEL_WIDTH, SUMMARY_ALERT_LABEL_WIDTH)} ]`,
|
|
1178
|
+
{
|
|
1179
|
+
variant: summaryBanner.variant,
|
|
1180
|
+
},
|
|
1181
|
+
),
|
|
1182
|
+
ui.text(summaryBanner.message, {
|
|
1183
|
+
textOverflow: "ellipsis",
|
|
1184
|
+
maxWidth: 92,
|
|
1185
|
+
}),
|
|
1186
|
+
]),
|
|
1187
|
+
]),
|
|
1188
|
+
|
|
1189
|
+
ui.box({ border: "rounded", px: PANEL_PADDING_X, py: PANEL_PADDING_Y, style: stripStyle }, [
|
|
1190
|
+
ui.column({ gap: 1 }, [
|
|
1191
|
+
ui.row({ gap: 2, items: "center", wrap: true }, [
|
|
1192
|
+
toolbarAction(
|
|
1193
|
+
state.paused ? "ui.play" : "status.dot",
|
|
1194
|
+
"toggle-pause",
|
|
1195
|
+
"⏯ Stream",
|
|
1196
|
+
togglePauseAction,
|
|
136
1197
|
),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
1198
|
+
toolbarAction("ui.refresh", "cycle-theme", "🎨 Theme", cycleThemeAction),
|
|
1199
|
+
toolbarAction("status.question", "help", "❓ Help", openHelpAction),
|
|
1200
|
+
toolbarAction("ui.close", "clear-incidents", "🧹 Clear Events", clearIncidentsAction),
|
|
1201
|
+
]),
|
|
1202
|
+
ui.row({ gap: 2, items: "center", wrap: true }, [
|
|
1203
|
+
ui.button({
|
|
1204
|
+
id: "cycle-filter",
|
|
1205
|
+
label: `🧭 Filter ${filterLabel(state.filter)}`,
|
|
1206
|
+
onPress: cycleFilterAction,
|
|
1207
|
+
}),
|
|
1208
|
+
ui.button({ id: "cycle-sort", label: "⇅ Sort Field", onPress: cycleSortAction }),
|
|
1209
|
+
ui.button({
|
|
1210
|
+
id: "toggle-order",
|
|
1211
|
+
label: "⇵ Sort Direction",
|
|
1212
|
+
onPress: toggleSortDirectionAction,
|
|
1213
|
+
}),
|
|
1214
|
+
ui.button({ id: "toggle-pin", label: "📌 Pin Service", onPress: togglePinAction }),
|
|
1215
|
+
ui.button({ id: "toggle-debug", label: "🧪 Debug", onPress: toggleDebugAction }),
|
|
1216
|
+
]),
|
|
1217
|
+
]),
|
|
1218
|
+
]),
|
|
140
1219
|
|
|
1220
|
+
ui.row({ gap: 1, wrap: true, items: "stretch" }, [
|
|
1221
|
+
pulseCard("Sync Interval", syncValueLabel, cadenceVariant, [
|
|
1222
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1223
|
+
state.paused
|
|
1224
|
+
? ui.icon("ui.pause")
|
|
1225
|
+
: ui.spinner({
|
|
1226
|
+
variant: LIVE_SPINNER_VARIANT,
|
|
1227
|
+
label: cadencePulse,
|
|
1228
|
+
style: { fg: palette.accent.primary },
|
|
1229
|
+
}),
|
|
1230
|
+
ui.text(state.paused ? `Stale ${staleLabel}` : `Rate ${updateRateLabel}`, {
|
|
1231
|
+
style: metaStyle,
|
|
1232
|
+
}),
|
|
1233
|
+
]),
|
|
1234
|
+
ui.progress(freshnessRatio, {
|
|
1235
|
+
variant: "blocks",
|
|
1236
|
+
width: KPI_PROGRESS_WIDTH,
|
|
1237
|
+
style: freshnessBarStyle,
|
|
1238
|
+
trackStyle: kpiTrackStyle,
|
|
1239
|
+
}),
|
|
1240
|
+
ui.text(`Cadence target ${cadenceLabel}`, { style: quietStyle }),
|
|
1241
|
+
]),
|
|
1242
|
+
pulseCard("Latency SLO", latencyValueLabel, latencyDeltaVariant, [
|
|
1243
|
+
ui.sparkline(state.fleetLatencyHistory, { width: KPI_SPARKLINE_WIDTH, min: 0, max: 220 }),
|
|
1244
|
+
ui.progress(latencyRatio, {
|
|
1245
|
+
variant: "minimal",
|
|
1246
|
+
width: KPI_PROGRESS_WIDTH,
|
|
1247
|
+
style: latencyBarStyle,
|
|
1248
|
+
trackStyle: kpiTrackStyle,
|
|
1249
|
+
}),
|
|
1250
|
+
ui.text(`${signedDelta(latencyDelta)} ms / cycle`, { style: quietStyle }),
|
|
1251
|
+
]),
|
|
1252
|
+
pulseCard("Error Budget Burn", errorValueLabel, errorDeltaVariant, [
|
|
1253
|
+
ui.sparkline(state.fleetErrorHistory, { width: KPI_SPARKLINE_WIDTH, min: 0, max: 10 }),
|
|
1254
|
+
ui.progress(errorRatio, {
|
|
1255
|
+
variant: "minimal",
|
|
1256
|
+
width: KPI_PROGRESS_WIDTH,
|
|
1257
|
+
style: errorBarStyle,
|
|
1258
|
+
trackStyle: kpiTrackStyle,
|
|
1259
|
+
}),
|
|
1260
|
+
ui.text(`${signedDelta(errorDelta, 2)}% / cycle`, { style: quietStyle }),
|
|
1261
|
+
]),
|
|
1262
|
+
pulseCard("Request Throughput", throughputValueLabel, trafficDeltaVariant, [
|
|
1263
|
+
ui.sparkline(state.fleetTrafficHistory, { width: KPI_SPARKLINE_WIDTH }),
|
|
1264
|
+
ui.progress(trafficRatio, {
|
|
1265
|
+
variant: "blocks",
|
|
1266
|
+
width: KPI_PROGRESS_WIDTH,
|
|
1267
|
+
style: throughputBarStyle,
|
|
1268
|
+
trackStyle: kpiTrackStyle,
|
|
1269
|
+
}),
|
|
1270
|
+
ui.text(`${signedDelta(trafficDelta)} rpm / cycle`, { style: quietStyle }),
|
|
1271
|
+
]),
|
|
1272
|
+
]),
|
|
1273
|
+
|
|
1274
|
+
ui.row({ flex: 3, gap: 1, items: "stretch" }, [
|
|
141
1275
|
panel(
|
|
142
|
-
"
|
|
1276
|
+
"Fleet Services",
|
|
143
1277
|
[
|
|
144
|
-
ui.
|
|
145
|
-
ui.
|
|
146
|
-
|
|
147
|
-
ui.
|
|
148
|
-
ui.
|
|
149
|
-
|
|
1278
|
+
ui.row({ items: "center", gap: 1, wrap: true }, [
|
|
1279
|
+
ui.row({ gap: 2, items: "center" }, [
|
|
1280
|
+
ui.status("busy", { label: `Critical ${String(downCount).padStart(2, " ")}` }),
|
|
1281
|
+
ui.status("away", { label: `Degraded ${String(warningCount).padStart(2, " ")}` }),
|
|
1282
|
+
ui.status("online", { label: `Healthy ${String(healthyCount).padStart(2, " ")}` }),
|
|
1283
|
+
]),
|
|
1284
|
+
ui.spacer({ flex: 1 }),
|
|
1285
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1286
|
+
ui.badge(`Filter ${filterBadgeLabel}`, {
|
|
1287
|
+
variant:
|
|
1288
|
+
state.filter === "all"
|
|
1289
|
+
? "default"
|
|
1290
|
+
: state.filter === "down"
|
|
1291
|
+
? "error"
|
|
1292
|
+
: "warning",
|
|
150
1293
|
}),
|
|
1294
|
+
ui.badge(`Sort ${sortPanelLabel}`, { variant: "info" }),
|
|
151
1295
|
]),
|
|
152
|
-
ui.divider({ char: "-" }),
|
|
153
|
-
ui.text("Active incidents", { fg: colors.muted }),
|
|
154
|
-
...incidents.map((line) => ui.text(`- ${line}`)),
|
|
155
1296
|
]),
|
|
1297
|
+
visible.length === 0
|
|
1298
|
+
? ui.empty("No services match this filter.", {
|
|
1299
|
+
description: "Press f or use the Filter control to return to ALL.",
|
|
1300
|
+
icon: "status.question",
|
|
1301
|
+
})
|
|
1302
|
+
: ui.table<Service>({
|
|
1303
|
+
id: "service-table",
|
|
1304
|
+
columns: tableColumns,
|
|
1305
|
+
data: visible,
|
|
1306
|
+
getRowKey: (row) => row.id,
|
|
1307
|
+
selection: selected ? [selected.id] : [],
|
|
1308
|
+
selectionMode: "single",
|
|
1309
|
+
onSelectionChange: (keys) => {
|
|
1310
|
+
const nextId = keys[0];
|
|
1311
|
+
if (!nextId) return;
|
|
1312
|
+
app.update((s) => (s.selectedId === nextId ? s : { ...s, selectedId: nextId }));
|
|
1313
|
+
},
|
|
1314
|
+
onRowPress: (row) => {
|
|
1315
|
+
app.update((s) => (s.selectedId === row.id ? s : { ...s, selectedId: row.id }));
|
|
1316
|
+
},
|
|
1317
|
+
onRowDoublePress: (row) => {
|
|
1318
|
+
app.update((s) => ({
|
|
1319
|
+
...s,
|
|
1320
|
+
selectedId: row.id,
|
|
1321
|
+
pinnedId: s.pinnedId === row.id ? null : row.id,
|
|
1322
|
+
}));
|
|
1323
|
+
},
|
|
1324
|
+
sortColumn: state.sort,
|
|
1325
|
+
sortDirection: state.sortDirection,
|
|
1326
|
+
onSort: (column, direction) => {
|
|
1327
|
+
const sort = toSortKey(column);
|
|
1328
|
+
if (!sort) return;
|
|
1329
|
+
app.update((s) => {
|
|
1330
|
+
if (s.sort === sort && s.sortDirection === direction) return s;
|
|
1331
|
+
return withResolvedSelection({
|
|
1332
|
+
...s,
|
|
1333
|
+
sort,
|
|
1334
|
+
sortDirection: direction,
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1337
|
+
},
|
|
1338
|
+
borderStyle: { variant: "rounded", color: palette.border.default },
|
|
1339
|
+
}),
|
|
156
1340
|
],
|
|
157
|
-
|
|
1341
|
+
3,
|
|
1342
|
+
panelStyle,
|
|
158
1343
|
),
|
|
159
1344
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1345
|
+
ui.column({ flex: 2, gap: 1, items: "stretch" }, [
|
|
1346
|
+
panel(
|
|
1347
|
+
"Service Inspector",
|
|
1348
|
+
[
|
|
1349
|
+
selected
|
|
1350
|
+
? ui.column({ gap: 1 }, [
|
|
1351
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1352
|
+
ui.icon(serviceIcon(selected.status)),
|
|
1353
|
+
ui.text(selectedPanelName, { style: { bold: true } }),
|
|
1354
|
+
ui.spacer({ flex: 1 }),
|
|
1355
|
+
ui.badge(selectedPanelStatus, { variant: selectedBadge.variant }),
|
|
1356
|
+
selected.id === state.pinnedId ? ui.icon("ui.star") : null,
|
|
1357
|
+
]),
|
|
1358
|
+
ui.row({ gap: 1, wrap: true }, [
|
|
1359
|
+
ui.tag(`🧭 Region ${selectedPanelRegion}`, { variant: "info" }),
|
|
1360
|
+
ui.tag(`🧱 Tier ${selectedPanelTier}`, { variant: "default" }),
|
|
1361
|
+
ui.tag(`📶 Traffic ${formatTrafficFixed(selected.trafficRpm)} rpm`, {
|
|
1362
|
+
variant: "warning",
|
|
1363
|
+
}),
|
|
1364
|
+
]),
|
|
1365
|
+
ui.row({ gap: 1, wrap: true }, [
|
|
1366
|
+
ui.tag(`👤 Owner ${selectedOwner}`, { variant: "default" }),
|
|
1367
|
+
ui.tag(`📘 Runbook ${selectedRunbook}`, {
|
|
1368
|
+
variant: inspectorGuidance.variant === "error" ? "error" : "info",
|
|
1369
|
+
}),
|
|
1370
|
+
ui.tag(`🎯 SLO ${selectedSlo}`, { variant: "info" }),
|
|
1371
|
+
]),
|
|
1372
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1373
|
+
ui.badge(fixedLabel(inspectorGuidance.title, 20, 20), {
|
|
1374
|
+
variant: inspectorGuidance.variant,
|
|
1375
|
+
}),
|
|
1376
|
+
ui.text(inspectorGuidance.message, {
|
|
1377
|
+
style: metaStyle,
|
|
1378
|
+
textOverflow: "ellipsis",
|
|
1379
|
+
maxWidth: 52,
|
|
1380
|
+
}),
|
|
1381
|
+
]),
|
|
1382
|
+
ui.divider({ char: "·" }),
|
|
1383
|
+
ui.progress(selected.cpuPct / 100, {
|
|
1384
|
+
label: "CPU",
|
|
1385
|
+
variant: "bar",
|
|
1386
|
+
showPercent: true,
|
|
1387
|
+
}),
|
|
1388
|
+
ui.progress(selected.memoryPct / 100, {
|
|
1389
|
+
label: "Memory",
|
|
1390
|
+
variant: "bar",
|
|
1391
|
+
showPercent: true,
|
|
1392
|
+
}),
|
|
1393
|
+
ui.gauge((selected.saturation ?? 0) / 100, {
|
|
1394
|
+
label: "Saturation",
|
|
1395
|
+
variant: "compact",
|
|
1396
|
+
thresholds: [
|
|
1397
|
+
{ value: 0.72, variant: "warning" },
|
|
1398
|
+
{ value: 0.9, variant: "error" },
|
|
1399
|
+
],
|
|
1400
|
+
}),
|
|
1401
|
+
ui.gauge(selectedErrorBudget, {
|
|
1402
|
+
label: "Error budget burn",
|
|
1403
|
+
variant: "compact",
|
|
1404
|
+
thresholds: [
|
|
1405
|
+
{ value: 0.45, variant: "warning" },
|
|
1406
|
+
{ value: 0.75, variant: "error" },
|
|
1407
|
+
],
|
|
1408
|
+
}),
|
|
1409
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1410
|
+
ui.text("P95 trend", { style: metaStyle }),
|
|
1411
|
+
ui.sparkline(selected.history, {
|
|
1412
|
+
width: SELECTED_HISTORY_WIDTH,
|
|
1413
|
+
min: 0,
|
|
1414
|
+
max: 240,
|
|
1415
|
+
}),
|
|
1416
|
+
]),
|
|
1417
|
+
ui.text(`Latency SLO utilization ${Math.round(selectedLatencyBudget * 100)}%`, {
|
|
1418
|
+
style: quietStyle,
|
|
1419
|
+
}),
|
|
1420
|
+
])
|
|
1421
|
+
: ui.empty("No selected service", {
|
|
1422
|
+
description: "Choose a row in Fleet Services to inspect details.",
|
|
1423
|
+
icon: "status.question",
|
|
1424
|
+
}),
|
|
1425
|
+
],
|
|
1426
|
+
3,
|
|
1427
|
+
panelStyle,
|
|
1428
|
+
),
|
|
175
1429
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
border: "rounded",
|
|
180
|
-
px: 1,
|
|
181
|
-
py: 0,
|
|
182
|
-
style: { bg: colors.panelAlt, fg: colors.muted },
|
|
183
|
-
},
|
|
1430
|
+
panel(
|
|
1431
|
+
"Escalation Runbook",
|
|
184
1432
|
[
|
|
185
|
-
ui.
|
|
186
|
-
ui.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ui.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1433
|
+
ui.column({ gap: 1 }, [
|
|
1434
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1435
|
+
ui.badge(fixedLabel(runbookPlan.title, 22, 22), { variant: runbookPlan.variant }),
|
|
1436
|
+
ui.tag(`Runbook ${selectedRunbook || "RB-000"}`, {
|
|
1437
|
+
variant: runbookPlan.variant === "error" ? "error" : "info",
|
|
1438
|
+
}),
|
|
1439
|
+
]),
|
|
1440
|
+
ui.text(runbookPlan.summary, {
|
|
1441
|
+
style: metaStyle,
|
|
1442
|
+
textOverflow: "ellipsis",
|
|
1443
|
+
maxWidth: 56,
|
|
1444
|
+
}),
|
|
1445
|
+
ui.divider({ char: "·" }),
|
|
1446
|
+
ui.text(`1 ${runbookPlan.step1}`, {
|
|
1447
|
+
style: metaStyle,
|
|
1448
|
+
textOverflow: "ellipsis",
|
|
1449
|
+
maxWidth: 60,
|
|
1450
|
+
}),
|
|
1451
|
+
ui.text(`2 ${runbookPlan.step2}`, {
|
|
1452
|
+
style: metaStyle,
|
|
1453
|
+
textOverflow: "ellipsis",
|
|
1454
|
+
maxWidth: 60,
|
|
1455
|
+
}),
|
|
1456
|
+
ui.text(`3 ${runbookPlan.step3}`, {
|
|
1457
|
+
style: metaStyle,
|
|
1458
|
+
textOverflow: "ellipsis",
|
|
1459
|
+
maxWidth: 60,
|
|
1460
|
+
}),
|
|
1461
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1462
|
+
ui.text("Current fleet load", { style: metaStyle }),
|
|
1463
|
+
ui.miniChart(
|
|
1464
|
+
[
|
|
1465
|
+
{ label: "CPU", value: avgCpu, max: 100 },
|
|
1466
|
+
{ label: "MEM", value: avgMem, max: 100 },
|
|
1467
|
+
{ label: "LAT", value: avgLatency, max: 220 },
|
|
1468
|
+
],
|
|
1469
|
+
{ variant: "pills" },
|
|
1470
|
+
),
|
|
1471
|
+
]),
|
|
196
1472
|
]),
|
|
197
1473
|
],
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1474
|
+
2,
|
|
1475
|
+
panelStyle,
|
|
1476
|
+
),
|
|
1477
|
+
]),
|
|
1478
|
+
]),
|
|
1479
|
+
|
|
1480
|
+
panel(
|
|
1481
|
+
state.debug ? "Active Events + Debug" : "Active Events",
|
|
1482
|
+
[
|
|
1483
|
+
ui.column({ gap: 1 }, [
|
|
1484
|
+
ui.text("Newest events first. Feed updates continuously while stream is active.", {
|
|
1485
|
+
style: metaStyle,
|
|
1486
|
+
}),
|
|
1487
|
+
...Array.from({ length: INCIDENT_VISIBLE_ROWS }, (_, index) => {
|
|
1488
|
+
const incident = state.incidents[index];
|
|
1489
|
+
if (!incident) return ui.text(" ", { style: quietStyle });
|
|
1490
|
+
const badge = incidentBadge(incident.severity);
|
|
1491
|
+
return ui.row({ key: `incident-row-${incident.id}`, gap: 1, items: "center" }, [
|
|
1492
|
+
ui.icon(incidentIcon(incident.severity)),
|
|
1493
|
+
ui.badge(incidentBadgeLabel(incident.severity), { variant: badge.variant }),
|
|
1494
|
+
ui.text(`[${incident.at}] ${incident.message}`, {
|
|
1495
|
+
textOverflow: "ellipsis",
|
|
1496
|
+
maxWidth: INCIDENT_TEXT_MAX_WIDTH,
|
|
1497
|
+
}),
|
|
1498
|
+
]);
|
|
1499
|
+
}),
|
|
1500
|
+
state.debug
|
|
1501
|
+
? ui.column({ gap: 1 }, [
|
|
1502
|
+
ui.divider({ char: "-" }),
|
|
1503
|
+
ui.callout("Render loop instrumentation", {
|
|
1504
|
+
variant: "info",
|
|
1505
|
+
title: "Debug Counters",
|
|
1506
|
+
}),
|
|
1507
|
+
ui.row({ gap: 2, items: "center", wrap: true }, [
|
|
1508
|
+
ui.text(`Ticks ${state.ticks}`),
|
|
1509
|
+
ui.text(`Applied ${state.updatesApplied}`),
|
|
1510
|
+
ui.text(`Rate ${updateRate.toFixed(2)} Hz`),
|
|
1511
|
+
ui.text(`Last update age ${stalenessMs} ms`),
|
|
1512
|
+
]),
|
|
1513
|
+
])
|
|
1514
|
+
: ui.text("Press d to open render/debug counters.", { style: quietStyle }),
|
|
1515
|
+
]),
|
|
1516
|
+
],
|
|
1517
|
+
1,
|
|
1518
|
+
panelStyle,
|
|
1519
|
+
),
|
|
1520
|
+
|
|
1521
|
+
ui.box({ border: "rounded", px: PANEL_PADDING_X, py: PANEL_PADDING_Y, style: stripStyle }, [
|
|
1522
|
+
ui.column({ gap: 1 }, [
|
|
1523
|
+
ui.row({ justify: "between", items: "center", gap: 1 }, [
|
|
1524
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1525
|
+
ui.icon(state.paused ? "ui.pause" : "ui.play"),
|
|
1526
|
+
ui.text(state.paused ? "Live stream paused" : "Live stream active"),
|
|
1527
|
+
selected
|
|
1528
|
+
? ui.tag(`Selected ${selectedLabel}`, {
|
|
1529
|
+
variant:
|
|
1530
|
+
selected.status === "down"
|
|
1531
|
+
? "error"
|
|
1532
|
+
: selected.status === "warning"
|
|
1533
|
+
? "warning"
|
|
1534
|
+
: "info",
|
|
1535
|
+
})
|
|
1536
|
+
: null,
|
|
210
1537
|
]),
|
|
1538
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1539
|
+
ui.badge(`Filter ${filterBadgeLabel}`, {
|
|
1540
|
+
variant:
|
|
1541
|
+
state.filter === "all" ? "default" : state.filter === "down" ? "error" : "warning",
|
|
1542
|
+
}),
|
|
1543
|
+
ui.badge(`Sort ${sortBadgeLabel}`, { variant: "info" }),
|
|
1544
|
+
ui.badge(`${incidentCountLabel} events`, {
|
|
1545
|
+
variant: downCount > 0 ? "error" : warningCount > 0 ? "warning" : "success",
|
|
1546
|
+
}),
|
|
1547
|
+
]),
|
|
1548
|
+
]),
|
|
1549
|
+
ui.divider({ char: "·" }),
|
|
1550
|
+
ui.row({ gap: 1, items: "center" }, [
|
|
1551
|
+
ui.kbd(["up", "down"]),
|
|
1552
|
+
ui.text("select", { style: metaStyle }),
|
|
1553
|
+
ui.text("·", { style: quietStyle }),
|
|
1554
|
+
ui.kbd("f"),
|
|
1555
|
+
ui.text("filter", { style: metaStyle }),
|
|
1556
|
+
ui.text("·", { style: quietStyle }),
|
|
1557
|
+
ui.kbd("s"),
|
|
1558
|
+
ui.text("sort", { style: metaStyle }),
|
|
1559
|
+
ui.text("·", { style: quietStyle }),
|
|
1560
|
+
ui.kbd("o"),
|
|
1561
|
+
ui.text("order", { style: metaStyle }),
|
|
1562
|
+
ui.text("·", { style: quietStyle }),
|
|
1563
|
+
ui.kbd("t"),
|
|
1564
|
+
ui.text("theme", { style: metaStyle }),
|
|
1565
|
+
ui.text("·", { style: quietStyle }),
|
|
1566
|
+
ui.kbd("h"),
|
|
1567
|
+
ui.text("help", { style: metaStyle }),
|
|
1568
|
+
ui.text("·", { style: quietStyle }),
|
|
1569
|
+
ui.kbd(["p", "space"]),
|
|
1570
|
+
ui.text("pause", { style: metaStyle }),
|
|
1571
|
+
ui.text("·", { style: quietStyle }),
|
|
1572
|
+
ui.kbd("q"),
|
|
1573
|
+
ui.text("quit", { style: metaStyle }),
|
|
211
1574
|
]),
|
|
1575
|
+
]),
|
|
1576
|
+
]),
|
|
212
1577
|
]);
|
|
1578
|
+
|
|
1579
|
+
return ui.layers([
|
|
1580
|
+
mainContent,
|
|
1581
|
+
state.helpOpen
|
|
1582
|
+
? ui.modal({
|
|
1583
|
+
id: "help-modal",
|
|
1584
|
+
title: `${PRODUCT_NAME} Commands`,
|
|
1585
|
+
width: 90,
|
|
1586
|
+
frameStyle: {
|
|
1587
|
+
background: palette.bg.elevated,
|
|
1588
|
+
foreground: palette.fg.primary,
|
|
1589
|
+
border: palette.border.default,
|
|
1590
|
+
},
|
|
1591
|
+
backdrop: "none",
|
|
1592
|
+
initialFocus: "help-close",
|
|
1593
|
+
returnFocusTo: "help",
|
|
1594
|
+
content: ui.column({ gap: 1 }, [
|
|
1595
|
+
ui.box({ border: "rounded", px: 1, py: 0, style: stripStyle }, [
|
|
1596
|
+
ui.column({ gap: 1 }, [
|
|
1597
|
+
ui.row({ gap: 1, items: "center", wrap: true }, [
|
|
1598
|
+
ui.icon("status.info"),
|
|
1599
|
+
ui.text("Keyboard + Mouse Controls", { style: sectionLabelStyle }),
|
|
1600
|
+
ui.spacer({ flex: 1 }),
|
|
1601
|
+
ui.text("Esc closes help", { style: quietStyle }),
|
|
1602
|
+
]),
|
|
1603
|
+
ui.text(PRODUCT_MISSION, { style: metaStyle }),
|
|
1604
|
+
]),
|
|
1605
|
+
]),
|
|
1606
|
+
ui.divider({ char: "·" }),
|
|
1607
|
+
ui.column(
|
|
1608
|
+
{ gap: 1 },
|
|
1609
|
+
HELP_SHORTCUTS.map((shortcut) =>
|
|
1610
|
+
helpShortcutRow(shortcut.keys, shortcut.description),
|
|
1611
|
+
),
|
|
1612
|
+
),
|
|
1613
|
+
]),
|
|
1614
|
+
actions: [
|
|
1615
|
+
ui.button({
|
|
1616
|
+
id: "help-close",
|
|
1617
|
+
label: "Close (Esc)",
|
|
1618
|
+
onPress: closeHelpAction,
|
|
1619
|
+
}),
|
|
1620
|
+
],
|
|
1621
|
+
onClose: closeHelpAction,
|
|
1622
|
+
})
|
|
1623
|
+
: null,
|
|
1624
|
+
]);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
let telemetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1628
|
+
let telemetryRunning = false;
|
|
1629
|
+
let telemetryNextAt = 0;
|
|
1630
|
+
|
|
1631
|
+
function clearTelemetryTimer(): void {
|
|
1632
|
+
if (telemetryTimer === null) return;
|
|
1633
|
+
clearTimeout(telemetryTimer);
|
|
1634
|
+
telemetryTimer = null;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function scheduleTelemetryTick(nowMs = Date.now()): void {
|
|
1638
|
+
if (!telemetryRunning) return;
|
|
1639
|
+
if (telemetryNextAt <= 0) telemetryNextAt = nowMs + TELEMETRY_CADENCE_MS;
|
|
1640
|
+
const delayMs = Math.max(0, telemetryNextAt - nowMs);
|
|
1641
|
+
telemetryTimer = setTimeout(runTelemetryTick, delayMs);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function runTelemetryTick(): void {
|
|
1645
|
+
if (!telemetryRunning) return;
|
|
1646
|
+
const nowMs = Date.now();
|
|
1647
|
+
if (telemetryNextAt <= 0) telemetryNextAt = nowMs;
|
|
1648
|
+
if (nowMs - telemetryNextAt > TELEMETRY_MAX_DRIFT_MS) {
|
|
1649
|
+
telemetryNextAt = nowMs;
|
|
1650
|
+
}
|
|
1651
|
+
telemetryNextAt += TELEMETRY_CADENCE_MS;
|
|
1652
|
+
app.update((state) => simulateTick(state, nowMs));
|
|
1653
|
+
scheduleTelemetryTick(nowMs);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function startTelemetryLoop(): void {
|
|
1657
|
+
if (telemetryRunning) return;
|
|
1658
|
+
telemetryRunning = true;
|
|
1659
|
+
telemetryNextAt = 0;
|
|
1660
|
+
scheduleTelemetryTick();
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function stopTelemetryLoop(): void {
|
|
1664
|
+
telemetryRunning = false;
|
|
1665
|
+
telemetryNextAt = 0;
|
|
1666
|
+
clearTelemetryTimer();
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
let dashboardStopRequested = false;
|
|
1670
|
+
|
|
1671
|
+
function requestDashboardStop(): void {
|
|
1672
|
+
stopTelemetryLoop();
|
|
1673
|
+
if (dashboardStopRequested) return;
|
|
1674
|
+
dashboardStopRequested = true;
|
|
1675
|
+
|
|
1676
|
+
// Defer app.stop() outside event/key handlers to avoid reentrant-stop fatals.
|
|
1677
|
+
setTimeout(() => {
|
|
1678
|
+
void app.stop();
|
|
1679
|
+
}, 0);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
app.onEvent((ev) => {
|
|
1683
|
+
if (ev.kind === "engine") {
|
|
1684
|
+
const raw = ev.event;
|
|
1685
|
+
if (raw.kind === "resize") {
|
|
1686
|
+
startTelemetryLoop();
|
|
1687
|
+
}
|
|
1688
|
+
if (raw.kind === "key" && raw.action === "down") {
|
|
1689
|
+
// Fallback quit handling for terminals that deliver printable keys in
|
|
1690
|
+
// non-standard key paths/modifier combos.
|
|
1691
|
+
if (raw.key === 81 || raw.key === 113 || (raw.mods & 0b0010) !== 0) {
|
|
1692
|
+
if (raw.key === 81 || raw.key === 113 || raw.key === 67 || raw.key === 99) {
|
|
1693
|
+
requestDashboardStop();
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (raw.kind === "text" && raw.codepoint >= 0 && raw.codepoint <= 0x10ffff) {
|
|
1698
|
+
const ch = String.fromCodePoint(raw.codepoint).toLowerCase();
|
|
1699
|
+
if (ch === "q") requestDashboardStop();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (ev.kind === "fatal") stopTelemetryLoop();
|
|
213
1703
|
});
|
|
214
1704
|
|
|
215
1705
|
app.keys({
|
|
216
|
-
q:
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
down: () =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const next: Filter =
|
|
241
|
-
s.filter === "all"
|
|
242
|
-
? "warning"
|
|
243
|
-
: s.filter === "warning"
|
|
244
|
-
? "healthy"
|
|
245
|
-
: s.filter === "healthy"
|
|
246
|
-
? "down"
|
|
247
|
-
: "all";
|
|
248
|
-
return { ...s, filter: next, selected: 0 };
|
|
249
|
-
}),
|
|
250
|
-
enter: () =>
|
|
251
|
-
app.update((s) => {
|
|
252
|
-
const list = filterServices(s.filter);
|
|
253
|
-
const svc = list[s.selected];
|
|
254
|
-
return { ...s, pinned: svc ? svc.name : s.pinned };
|
|
255
|
-
}),
|
|
256
|
-
"?": () => app.update((s) => ({ ...s, showHelp: !s.showHelp })),
|
|
1706
|
+
q: requestDashboardStop,
|
|
1707
|
+
"shift+q": requestDashboardStop,
|
|
1708
|
+
"ctrl+c": requestDashboardStop,
|
|
1709
|
+
up: (ctx) => {
|
|
1710
|
+
if (ctx.focusedId === "service-table") return;
|
|
1711
|
+
ctx.update((s) => moveSelection(s, -1));
|
|
1712
|
+
},
|
|
1713
|
+
down: (ctx) => {
|
|
1714
|
+
if (ctx.focusedId === "service-table") return;
|
|
1715
|
+
ctx.update((s) => moveSelection(s, 1));
|
|
1716
|
+
},
|
|
1717
|
+
k: () => app.update((s) => moveSelection(s, -1)),
|
|
1718
|
+
j: () => app.update((s) => moveSelection(s, 1)),
|
|
1719
|
+
f: cycleFilterAction,
|
|
1720
|
+
s: cycleSortAction,
|
|
1721
|
+
o: toggleSortDirectionAction,
|
|
1722
|
+
t: cycleThemeAction,
|
|
1723
|
+
p: togglePauseAction,
|
|
1724
|
+
space: togglePauseAction,
|
|
1725
|
+
enter: togglePinAction,
|
|
1726
|
+
d: toggleDebugAction,
|
|
1727
|
+
c: clearIncidentsAction,
|
|
1728
|
+
h: openHelpAction,
|
|
1729
|
+
escape: closeHelpAction,
|
|
257
1730
|
});
|
|
258
1731
|
|
|
259
|
-
|
|
1732
|
+
try {
|
|
1733
|
+
await app.start();
|
|
1734
|
+
} finally {
|
|
1735
|
+
stopTelemetryLoop();
|
|
1736
|
+
}
|