From bac809952a4172ee37ffe53bd9a73d66fceee102 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 14:38:19 +0200 Subject: [PATCH] fix(web): validate restored dashboard tile shapes --- .../src/lib/client/dashboard-refresh.test.ts | 18 ++- apps/web/src/lib/client/dashboard-refresh.ts | 111 +++++++++++++++++- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts index f5712d6..a232560 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -213,7 +213,21 @@ describe("dashboard refresh lifecycle", () => { tiles: [ {}, { - item: { id: "infra-ram", detail: "live" }, + item: { + id: "incomplete", + detail: "live", + }, + savedAt: 1_000, + tile: { kind: "telemetry", id: "incomplete" }, + }, + { + item: { + id: "infra-ram", + label: "Infra RAM", + value: { kind: "percent", value: 42 }, + detail: "live", + severity: "ok", + }, savedAt: 1_000, tile: { kind: "telemetry", id: "infra-ram" }, }, @@ -242,6 +256,8 @@ describe("dashboard refresh lifecycle", () => { tile: { kind: "telemetry", id: "infra-ram" }, item: { id: "infra-ram", + label: "Infra RAM", + value: { kind: "percent", value: 42 }, detail: "live - stale 15s", severity: "stale", }, diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts index ba68dcc..aaf3cd3 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -346,7 +346,7 @@ function isDashboardTileSnapshotRecord( typeof value.savedAt === "number" && Number.isFinite(value.savedAt) && isDashboardTileReference(value.tile) && - isDashboardTileSnapshotItem(value.item) + isDashboardTileSnapshotItem(value.tile, value.item) ); } @@ -369,19 +369,118 @@ function isDashboardTileReference( } function isDashboardTileSnapshotItem( + tile: DashboardTileReference, value: unknown, ): value is DashboardTileSnapshotItem { - return ( - isSnapshotRecord(value) && - typeof value.id === "string" && - (value.detail === undefined || typeof value.detail === "string") - ); + if (!isSnapshotRecord(value) || value.id !== tile.id) return false; + + switch (tile.kind) { + case "telemetry": + return ( + typeof value.label === "string" && + isMetricValue(value.value) && + isSeverity(value.severity) && + isOptionalString(value.detail) && + isOptionalString(value.description) && + isOptionalString(value.icon) && + isOptionalNumberArray(value.sparkline) + ); + case "service": + return ( + typeof value.label === "string" && + typeof value.description === "string" && + isSeverity(value.severity) && + isOptionalString(value.detail) && + isOptionalString(value.icon) && + isOptionalLink(value.link) + ); + case "module": + return ( + (value.kind === "summary" || + value.kind === "weather" || + value.kind === "custom") && + isOptionalString(value.title) && + isOptionalString(value.label) && + isOptionalString(value.value) && + isOptionalString(value.detail) && + isOptionalString(value.icon) && + (value.severity === undefined || isSeverity(value.severity)) + ); + case "status": + return ( + typeof value.label === "string" && + typeof value.value === "string" && + isOptionalLink(value.link) && + (value.severity === undefined || isSeverity(value.severity)) + ); + } } function isSnapshotRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +function isMetricValue(value: unknown): boolean { + if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false; + + if (value.kind === "text") { + return ( + typeof value.value === "string" && + isOptionalString(value.unit) + ); + } + + const numericKinds = ["bytes", "latency", "number", "percent", "temperature"]; + if (!numericKinds.includes(value.kind)) return false; + if (typeof value.value !== "number" || !Number.isFinite(value.value)) return false; + if (value.kind === "percent" && (value.value < 0 || value.value > 100)) { + return false; + } + + const precision = value.precision; + return ( + isOptionalString(value.unit) && + (precision === undefined || + (typeof precision === "number" && + Number.isInteger(precision) && + precision >= 0 && + precision <= 4)) + ); +} + +function isSeverity(value: unknown): boolean { + return ( + value === "neutral" || + value === "ok" || + value === "warning" || + value === "danger" || + value === "stale" || + value === "unavailable" + ); +} + +function isOptionalString(value: unknown): boolean { + return value === undefined || typeof value === "string"; +} + +function isOptionalNumberArray(value: unknown): boolean { + return ( + value === undefined || + (Array.isArray(value) && + value.every((item) => typeof item === "number" && Number.isFinite(item))) + ); +} + +function isOptionalLink(value: unknown): boolean { + return ( + value === undefined || + (isSnapshotRecord(value) && + typeof value.href === "string" && + isOptionalString(value.label) && + (value.external === undefined || typeof value.external === "boolean")) + ); +} + function staleDashboardTileDetail( detail: string | undefined, ageMs: number,