diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index 4db0cbc..6124538 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -2,11 +2,7 @@ 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, - restoreDashboardTileSnapshots, -} from "./App"; +import { AppStateView, dashboardHydrationTiles } from "./App"; describe("React app dashboard state view", () => { test("renders loading dashboard state", () => { @@ -124,39 +120,4 @@ 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 f8a30e2..c2f890e 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -9,13 +9,10 @@ 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 { @@ -45,17 +42,11 @@ type DashboardTileResponse = state: "not_found"; tile: DashboardTileReference; message: string; - } - | { - state: "disabled"; - tile: DashboardTileReference; - message: string; }; type DashboardTileHydrationResult = "aborted" | "failed" | "ready"; const dashboardTileHydrationConcurrency = 6; -type DashboardTileSnapshotStore = ReturnType; export function AppStateView({ dashboard, @@ -173,9 +164,6 @@ export default function App() { let hydrationRun = 0; const requestAborter = createDashboardRequestAborter(); const tileBackoff = createDashboardTileBackoff(); - const tileSnapshotStore = createDashboardTileSnapshotStore( - getTileSnapshotStorage(), - ); function refreshPaused() { return shouldPauseDashboardRefresh({ @@ -207,42 +195,19 @@ export default function App() { const nextDashboard = (await response.json()) as DashboardRuntimeState; if (cancelled || shellSignal.aborted || refreshPaused()) return; - const restored = restoreDashboardTileSnapshots( - nextDashboard, - nextDashboard.state === "ready" - ? tileSnapshotStore.restore({ - currentRevisionId: nextDashboard.currentRevisionId, - schemaVersion: nextDashboard.schemaVersion, - }) - : [], - ); - setDashboard(restored.dashboard); + setDashboard(nextDashboard); const currentRun = ++hydrationRun; if ( - restored.dashboard.state === "ready" && - restored.dashboard.liveDatasourceHydration?.enabled !== false + nextDashboard.state === "ready" && + nextDashboard.liveDatasourceHydration?.enabled !== false ) { - const tiles = dashboardHydrationTiles(restored.dashboard.document).filter( + const tiles = dashboardHydrationTiles(nextDashboard.document).filter( (tile) => tileBackoff.canAttempt(dashboardTileKey(tile)), ); const tileSignal = requestAborter.beginTileRun(); - setHydratingItemIds( - new Set( - tiles - .map(dashboardTileKey) - .filter((key) => !restored.restoredItemIds.has(key)), - ), - ); - hydrateDashboardTiles( - tiles, - currentRun, - tileSignal, - { - currentRevisionId: restored.dashboard.currentRevisionId, - schemaVersion: restored.dashboard.schemaVersion, - }, - ); + setHydratingItemIds(new Set(tiles.map(dashboardTileKey))); + hydrateDashboardTiles(tiles, currentRun, tileSignal); } else { setHydratingItemIds(new Set()); } @@ -271,18 +236,12 @@ 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, - snapshotContext, - ); + const result = await hydrateDashboardTile(tile, run, signal); if (result === "ready") { tileBackoff.recordSuccess(key); @@ -299,7 +258,6 @@ export default function App() { tile: DashboardTileReference, run: number, signal: AbortSignal, - snapshotContext: DashboardTileSnapshotStoreContext, ): Promise { const key = dashboardTileKey(tile); @@ -328,10 +286,6 @@ export default function App() { } : current, ); - tileSnapshotStore.saveReadyTile({ - ...snapshotContext, - response: readyTileResponse, - }); return "ready"; } catch (error) { if (!isAbortError(error) && !cancelled) { @@ -398,14 +352,6 @@ function getThemeStorage(): Storage | undefined { } } -function getTileSnapshotStorage(): Storage | undefined { - try { - return window.localStorage; - } catch { - return undefined; - } -} - function isAbortError(error: unknown): boolean { return ( typeof error === "object" && @@ -415,41 +361,6 @@ 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 a232560..1cdb872 100644 --- a/apps/web/src/lib/client/dashboard-refresh.test.ts +++ b/apps/web/src/lib/client/dashboard-refresh.test.ts @@ -1,7 +1,6 @@ import { describe, expect, test } from "vitest"; import { attachDashboardRefreshLifecycle, - createDashboardTileSnapshotStore, createDashboardTileBackoff, createDashboardRequestAborter, runDashboardHydrationQueue, @@ -132,139 +131,6 @@ 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", - }); - store.saveTile({ - currentRevisionId: "revision-a", - response: { - state: "disabled", - tile: { kind: "module", id: "disabled-module" }, - message: "Disabled", - }, - 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([]); - }); - - 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: "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" }, - }, - { - 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", - label: "Infra RAM", - value: { kind: "percent", value: 42 }, - detail: "live - stale 15s", - severity: "stale", - }, - }, - }, - ]); - }); }); async function waitFor(predicate: () => boolean) { @@ -275,27 +141,3 @@ 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 aaf3cd3..998de5f 100644 --- a/apps/web/src/lib/client/dashboard-refresh.ts +++ b/apps/web/src/lib/client/dashboard-refresh.ts @@ -3,39 +3,6 @@ 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 { @@ -178,314 +145,3 @@ 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 { - currentRevisionId: payload.currentRevisionId, - schemaVersion: payload.schemaVersion, - tiles: payload.tiles.filter(isDashboardTileSnapshotRecord), - version: 1, - }; - } 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 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.tile, 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( - tile: DashboardTileReference, - value: unknown, -): value is DashboardTileSnapshotItem { - 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, -): string | undefined { - if (!detail) return detail; - const ageSeconds = Math.max(0, Math.floor(ageMs / 1_000)); - return ageSeconds > 0 ? `${detail} - stale ${ageSeconds}s` : detail; -}