fix(web): ignore malformed dashboard tile snapshots

This commit is contained in:
vince 2026-06-20 14:32:09 +02:00
parent 4ce42fb52b
commit 88c448258d
3 changed files with 112 additions and 1 deletions

View file

@ -45,6 +45,11 @@ type DashboardTileResponse =
state: "not_found";
tile: DashboardTileReference;
message: string;
}
| {
state: "disabled";
tile: DashboardTileReference;
message: string;
};
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";

View file

@ -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) {

View file

@ -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<string, unknown> {
return typeof value === "object" && value !== null;
}
function staleDashboardTileDetail(
detail: string | undefined,
ageMs: number,