From 4ce42fb52b0148e5a9910856a3975d70118df3ad Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 14:25:43 +0200 Subject: [PATCH 1/3] perf(web): restore last known dashboard tiles --- apps/web/src/App.test.tsx | 41 +++- apps/web/src/App.tsx | 98 ++++++++- .../src/lib/client/dashboard-refresh.test.ts | 86 ++++++++ apps/web/src/lib/client/dashboard-refresh.ts | 195 ++++++++++++++++++ 4 files changed, 412 insertions(+), 8 deletions(-) diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 6124538..4db0cbc 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -2,7 +2,11 @@ import { renderToString } from "react-dom/server"; import { describe, expect, test } from "vitest"; import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; -import { AppStateView, dashboardHydrationTiles } from "./App"; +import { + AppStateView, + dashboardHydrationTiles, + restoreDashboardTileSnapshots, +} from "./App"; describe("React app dashboard state view", () => { test("renders loading dashboard state", () => { @@ -120,4 +124,39 @@ describe("React app dashboard state view", () => { ), ); }); + + test("applies restored tile snapshots without marking them as loading", () => { + const restored = restoreDashboardTileSnapshots( + { + state: "ready", + document: dimensionLabDashboardFixture, + schemaVersion: "dashboard.v1", + currentRevisionId: "revision-a", + }, + [ + { + ageMs: 60_000, + response: { + state: "ready", + tile: { kind: "telemetry", id: "infra-ram" }, + item: { + ...dimensionLabDashboardFixture.telemetry[0], + detail: "cached - stale 60s", + severity: "stale", + }, + }, + }, + ], + ); + + expect(restored.restoredItemIds).toEqual(new Set(["telemetry:infra-ram"])); + if (restored.dashboard.state !== "ready") { + throw new Error("Expected dashboard to be ready"); + } + expect(restored.dashboard.document.telemetry[0]).toMatchObject({ + id: "infra-ram", + detail: "cached - stale 60s", + severity: "stale", + }); + }); }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index c2f890e..bea1a69 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -9,10 +9,13 @@ import type { import type { DashboardRuntimeState } from "$lib/server/dashboard"; import { attachDashboardRefreshLifecycle, + createDashboardTileSnapshotStore, createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, shouldPauseDashboardRefresh, + type DashboardTileSnapshotStoreContext, + type RestoredDashboardTileSnapshot, } from "$lib/client/dashboard-refresh"; import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer"; import { @@ -47,6 +50,7 @@ type DashboardTileResponse = type DashboardTileHydrationResult = "aborted" | "failed" | "ready"; const dashboardTileHydrationConcurrency = 6; +type DashboardTileSnapshotStore = ReturnType; export function AppStateView({ dashboard, @@ -164,6 +168,9 @@ export default function App() { let hydrationRun = 0; const requestAborter = createDashboardRequestAborter(); const tileBackoff = createDashboardTileBackoff(); + const tileSnapshotStore = createDashboardTileSnapshotStore( + getTileSnapshotStorage(), + ); function refreshPaused() { return shouldPauseDashboardRefresh({ @@ -195,19 +202,42 @@ export default function App() { const nextDashboard = (await response.json()) as DashboardRuntimeState; if (cancelled || shellSignal.aborted || refreshPaused()) return; - setDashboard(nextDashboard); + const restored = restoreDashboardTileSnapshots( + nextDashboard, + nextDashboard.state === "ready" + ? tileSnapshotStore.restore({ + currentRevisionId: nextDashboard.currentRevisionId, + schemaVersion: nextDashboard.schemaVersion, + }) + : [], + ); + setDashboard(restored.dashboard); const currentRun = ++hydrationRun; if ( - nextDashboard.state === "ready" && - nextDashboard.liveDatasourceHydration?.enabled !== false + restored.dashboard.state === "ready" && + restored.dashboard.liveDatasourceHydration?.enabled !== false ) { - const tiles = dashboardHydrationTiles(nextDashboard.document).filter( + const tiles = dashboardHydrationTiles(restored.dashboard.document).filter( (tile) => tileBackoff.canAttempt(dashboardTileKey(tile)), ); const tileSignal = requestAborter.beginTileRun(); - setHydratingItemIds(new Set(tiles.map(dashboardTileKey))); - hydrateDashboardTiles(tiles, currentRun, tileSignal); + setHydratingItemIds( + new Set( + tiles + .map(dashboardTileKey) + .filter((key) => !restored.restoredItemIds.has(key)), + ), + ); + hydrateDashboardTiles( + tiles, + currentRun, + tileSignal, + { + currentRevisionId: restored.dashboard.currentRevisionId, + schemaVersion: restored.dashboard.schemaVersion, + }, + ); } else { setHydratingItemIds(new Set()); } @@ -236,12 +266,18 @@ export default function App() { tiles: DashboardTileReference[], run: number, signal: AbortSignal, + snapshotContext: DashboardTileSnapshotStoreContext, ) { void runDashboardHydrationQueue({ concurrency: dashboardTileHydrationConcurrency, hydrate: async (tile) => { const key = dashboardTileKey(tile); - const result = await hydrateDashboardTile(tile, run, signal); + const result = await hydrateDashboardTile( + tile, + run, + signal, + snapshotContext, + ); if (result === "ready") { tileBackoff.recordSuccess(key); @@ -258,6 +294,7 @@ export default function App() { tile: DashboardTileReference, run: number, signal: AbortSignal, + snapshotContext: DashboardTileSnapshotStoreContext, ): Promise { const key = dashboardTileKey(tile); @@ -286,6 +323,10 @@ export default function App() { } : current, ); + tileSnapshotStore.saveReadyTile({ + ...snapshotContext, + response: readyTileResponse, + }); return "ready"; } catch (error) { if (!isAbortError(error) && !cancelled) { @@ -352,6 +393,14 @@ function getThemeStorage(): Storage | undefined { } } +function getTileSnapshotStorage(): Storage | undefined { + try { + return window.localStorage; + } catch { + return undefined; + } +} + function isAbortError(error: unknown): boolean { return ( typeof error === "object" && @@ -361,6 +410,41 @@ function isAbortError(error: unknown): boolean { ); } +export function restoreDashboardTileSnapshots( + dashboard: DashboardRuntimeState, + snapshots: RestoredDashboardTileSnapshot[], +): { + dashboard: DashboardRuntimeState; + restoredItemIds: Set; +} { + if (dashboard.state !== "ready" || snapshots.length === 0) { + return { + dashboard, + restoredItemIds: new Set(), + }; + } + + return snapshots.reduce( + (current, snapshot) => ({ + dashboard: { + ...current.dashboard, + document: applyDashboardTile( + current.dashboard.document, + snapshot.response as Extract, + ), + }, + restoredItemIds: new Set([ + ...current.restoredItemIds, + dashboardTileKey(snapshot.response.tile), + ]), + }), + { + dashboard, + restoredItemIds: new Set(), + }, + ); +} + function markHydratingItems( dashboard: UiDashboardPreview, hydratingItemIds?: ReadonlySet, diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts index 1cdb872..69dd9b6 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vitest"; import { attachDashboardRefreshLifecycle, + createDashboardTileSnapshotStore, createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, @@ -131,6 +132,67 @@ describe("dashboard refresh lifecycle", () => { backoff.recordSuccess("telemetry:infra-ram"); expect(backoff.canAttempt("telemetry:infra-ram", 107_000)).toBe(true); }); + + test("stores only ready tile snapshots and restores them for matching revisions", () => { + const storage = createMemoryStorage(); + let now = 1_000; + const store = createDashboardTileSnapshotStore(storage, { + now: () => now, + }); + + store.saveReadyTile({ + currentRevisionId: "revision-a", + response: { + state: "ready", + tile: { kind: "telemetry", id: "infra-ram" }, + item: { + id: "infra-ram", + label: "Infra RAM", + value: { kind: "percent", value: 42 }, + detail: "live", + severity: "ok", + }, + }, + schemaVersion: "dashboard.v1", + }); + store.saveTile({ + currentRevisionId: "revision-a", + response: { + state: "not_found", + tile: { kind: "telemetry", id: "missing" }, + message: "Missing", + }, + schemaVersion: "dashboard.v1", + }); + + now = 61_000; + + const restored = store.restore({ + currentRevisionId: "revision-a", + schemaVersion: "dashboard.v1", + }); + + expect(restored).toEqual([ + { + ageMs: 60_000, + response: { + state: "ready", + tile: { kind: "telemetry", id: "infra-ram" }, + item: { + id: "infra-ram", + label: "Infra RAM", + value: { kind: "percent", value: 42 }, + detail: "live - stale 60s", + severity: "stale", + }, + }, + }, + ]); + expect(store.restore({ + currentRevisionId: "revision-b", + schemaVersion: "dashboard.v1", + })).toEqual([]); + }); }); async function waitFor(predicate: () => boolean) { @@ -141,3 +203,27 @@ async function waitFor(predicate: () => boolean) { throw new Error("condition was not met"); } + +function createMemoryStorage(): Storage { + const values = new Map(); + return { + get length() { + return values.size; + }, + clear() { + values.clear(); + }, + getItem(key) { + return values.get(key) ?? null; + }, + key(index) { + return [...values.keys()][index] ?? null; + }, + removeItem(key) { + values.delete(key); + }, + setItem(key, value) { + values.set(key, value); + }, + }; +} diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts index 998de5f..4d0582f 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -3,6 +3,39 @@ export interface DashboardRefreshPauseState { online: boolean; } +export type DashboardTileReference = + | { kind: "telemetry"; id: string } + | { kind: "service"; groupId: string; id: string } + | { kind: "module"; id: string } + | { kind: "status"; stripId: string; id: string }; + +type SnapshotMetricValue = Record; + +export type DashboardTileSnapshotItem = { + detail?: string; + id: string; + label?: string; + severity?: string; + value?: SnapshotMetricValue | string; +} & Record; + +export type DashboardTileSnapshotResponse = + | { + state: "ready"; + tile: DashboardTileReference; + item: DashboardTileSnapshotItem; + } + | { + state: "not_found"; + tile: DashboardTileReference; + message: string; + } + | { + state: "disabled"; + tile: DashboardTileReference; + message: string; + }; + export function shouldPauseDashboardRefresh( state: DashboardRefreshPauseState, ): boolean { @@ -145,3 +178,165 @@ export function createDashboardTileBackoff() { }, }; } + +interface DashboardTileSnapshotRecord { + item: DashboardTileSnapshotItem; + savedAt: number; + tile: DashboardTileReference; +} + +interface DashboardTileSnapshotPayload { + currentRevisionId: string; + schemaVersion: string; + tiles: DashboardTileSnapshotRecord[]; + version: 1; +} + +export interface DashboardTileSnapshotStoreContext { + currentRevisionId: string; + schemaVersion: string; +} + +export interface DashboardTileSnapshotStoreOptions { + now?: () => number; +} + +export interface RestoredDashboardTileSnapshot { + ageMs: number; + response: Extract; +} + +const dashboardTileSnapshotStorageKey = "dimensionlab.dashboard.tiles.v1"; + +export function createDashboardTileSnapshotStore( + storage: Storage | undefined, + options: DashboardTileSnapshotStoreOptions = {}, +) { + const now = options.now || Date.now; + + function read(): DashboardTileSnapshotPayload | null { + if (!storage) return null; + + try { + const serialized = storage.getItem(dashboardTileSnapshotStorageKey); + if (!serialized) return null; + + const payload = JSON.parse(serialized) as Partial; + if ( + payload.version !== 1 || + typeof payload.currentRevisionId !== "string" || + typeof payload.schemaVersion !== "string" || + !Array.isArray(payload.tiles) + ) { + return null; + } + + return payload as DashboardTileSnapshotPayload; + } catch { + return null; + } + } + + function write(payload: DashboardTileSnapshotPayload): void { + if (!storage) return; + + try { + storage.setItem(dashboardTileSnapshotStorageKey, JSON.stringify(payload)); + } catch { + // Best-effort warm-start cache; quota and privacy failures are non-fatal. + } + } + + function matchingPayload( + context: DashboardTileSnapshotStoreContext, + ): DashboardTileSnapshotPayload { + const payload = read(); + if ( + payload && + payload.currentRevisionId === context.currentRevisionId && + payload.schemaVersion === context.schemaVersion + ) { + return payload; + } + + return { + currentRevisionId: context.currentRevisionId, + schemaVersion: context.schemaVersion, + tiles: [], + version: 1, + }; + } + + function saveReadyTile( + input: DashboardTileSnapshotStoreContext & { + response: Extract; + }, + ): void { + const payload = matchingPayload(input); + const key = dashboardTileSnapshotKey(input.response.tile); + const nextRecord: DashboardTileSnapshotRecord = { + item: input.response.item, + savedAt: now(), + tile: input.response.tile, + }; + payload.tiles = [ + nextRecord, + ...payload.tiles.filter((record) => + dashboardTileSnapshotKey(record.tile) !== key + ), + ]; + write(payload); + } + + return { + restore(context: DashboardTileSnapshotStoreContext): RestoredDashboardTileSnapshot[] { + const payload = read(); + if ( + !payload || + payload.currentRevisionId !== context.currentRevisionId || + payload.schemaVersion !== context.schemaVersion + ) { + return []; + } + + const restoredAt = now(); + return payload.tiles.map((record) => ({ + ageMs: Math.max(0, restoredAt - record.savedAt), + response: { + state: "ready", + tile: record.tile, + item: { + ...record.item, + detail: staleDashboardTileDetail(record.item.detail, restoredAt - record.savedAt), + severity: "stale", + }, + }, + })); + }, + saveReadyTile, + saveTile(input: DashboardTileSnapshotStoreContext & { + response: DashboardTileSnapshotResponse; + }): void { + if (input.response.state === "ready") { + saveReadyTile({ + currentRevisionId: input.currentRevisionId, + response: input.response, + schemaVersion: input.schemaVersion, + }); + } + }, + }; +} + +function dashboardTileSnapshotKey(tile: DashboardTileReference): string { + return JSON.stringify(tile); +} + +function staleDashboardTileDetail( + detail: string | undefined, + ageMs: number, +): string | undefined { + if (!detail) return detail; + const ageSeconds = Math.max(0, Math.floor(ageMs / 1_000)); + return ageSeconds > 0 ? `${detail} - stale ${ageSeconds}s` : detail; +} -- 2.49.1 From 88c448258dd7446f76bcda89e190d22bcb66c59f Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 14:32:09 +0200 Subject: [PATCH 2/3] fix(web): ignore malformed dashboard tile snapshots --- apps/web/src/App.tsx | 5 ++ .../src/lib/client/dashboard-refresh.test.ts | 56 +++++++++++++++++++ apps/web/src/lib/client/dashboard-refresh.ts | 52 ++++++++++++++++- 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index bea1a69..f8a30e2 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -45,6 +45,11 @@ type DashboardTileResponse = state: "not_found"; tile: DashboardTileReference; message: string; + } + | { + state: "disabled"; + tile: DashboardTileReference; + message: string; }; type DashboardTileHydrationResult = "aborted" | "failed" | "ready"; diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts index 69dd9b6..f5712d6 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -164,6 +164,15 @@ describe("dashboard refresh lifecycle", () => { }, schemaVersion: "dashboard.v1", }); + store.saveTile({ + currentRevisionId: "revision-a", + response: { + state: "disabled", + tile: { kind: "module", id: "disabled-module" }, + message: "Disabled", + }, + schemaVersion: "dashboard.v1", + }); now = 61_000; @@ -193,6 +202,53 @@ describe("dashboard refresh lifecycle", () => { schemaVersion: "dashboard.v1", })).toEqual([]); }); + + test("ignores malformed stored tile snapshots", () => { + const storage = createMemoryStorage(); + storage.setItem( + "dimensionlab.dashboard.tiles.v1", + JSON.stringify({ + currentRevisionId: "revision-a", + schemaVersion: "dashboard.v1", + tiles: [ + {}, + { + item: { id: "infra-ram", detail: "live" }, + savedAt: 1_000, + tile: { kind: "telemetry", id: "infra-ram" }, + }, + { + item: { id: "broken-detail", detail: 42 }, + savedAt: 1_000, + tile: { kind: "telemetry", id: "broken-detail" }, + }, + ], + version: 1, + }), + ); + + const store = createDashboardTileSnapshotStore(storage, { + now: () => 16_000, + }); + + expect(store.restore({ + currentRevisionId: "revision-a", + schemaVersion: "dashboard.v1", + })).toEqual([ + { + ageMs: 15_000, + response: { + state: "ready", + tile: { kind: "telemetry", id: "infra-ram" }, + item: { + id: "infra-ram", + detail: "live - stale 15s", + severity: "stale", + }, + }, + }, + ]); + }); }); async function waitFor(predicate: () => boolean) { diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts index 4d0582f..ba68dcc 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -231,7 +231,12 @@ export function createDashboardTileSnapshotStore( return null; } - return payload as DashboardTileSnapshotPayload; + return { + currentRevisionId: payload.currentRevisionId, + schemaVersion: payload.schemaVersion, + tiles: payload.tiles.filter(isDashboardTileSnapshotRecord), + version: 1, + }; } catch { return null; } @@ -332,6 +337,51 @@ function dashboardTileSnapshotKey(tile: DashboardTileReference): string { return JSON.stringify(tile); } +function isDashboardTileSnapshotRecord( + value: unknown, +): value is DashboardTileSnapshotRecord { + if (!isSnapshotRecord(value)) return false; + + return ( + typeof value.savedAt === "number" && + Number.isFinite(value.savedAt) && + isDashboardTileReference(value.tile) && + isDashboardTileSnapshotItem(value.item) + ); +} + +function isDashboardTileReference( + value: unknown, +): value is DashboardTileReference { + if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false; + + switch (value.kind) { + case "telemetry": + case "module": + return typeof value.id === "string"; + case "service": + return typeof value.groupId === "string" && typeof value.id === "string"; + case "status": + return typeof value.stripId === "string" && typeof value.id === "string"; + default: + return false; + } +} + +function isDashboardTileSnapshotItem( + value: unknown, +): value is DashboardTileSnapshotItem { + return ( + isSnapshotRecord(value) && + typeof value.id === "string" && + (value.detail === undefined || typeof value.detail === "string") + ); +} + +function isSnapshotRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + function staleDashboardTileDetail( detail: string | undefined, ageMs: number, -- 2.49.1 From bac809952a4172ee37ffe53bd9a73d66fceee102 Mon Sep 17 00:00:00 2001 From: vince Date: Sat, 20 Jun 2026 14:38:19 +0200 Subject: [PATCH 3/3] 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, -- 2.49.1