diff --git a/src/lib/model/fixtures/dimensionlab.ts b/src/lib/model/fixtures/dimensionlab.ts index 6c06e78..c2b4808 100644 --- a/src/lib/model/fixtures/dimensionlab.ts +++ b/src/lib/model/fixtures/dimensionlab.ts @@ -548,7 +548,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = { datasource: uptimeMonitor(27), detail: "fallback - exporter health from Prometheus", }), - ]), + ], "grid"), ], statusStrips: [ { @@ -635,12 +635,17 @@ function service(seed: ServiceSeed): ServiceEntry { }; } -function group(id: string, title: string, services: ServiceEntry[]): ServiceGroup { +function group( + id: string, + title: string, + services: ServiceEntry[], + layout: ServiceGroup["layout"] = "list", +): ServiceGroup { return { id, - title, - layout: "list", + layout, services, + title, }; } diff --git a/src/lib/server/dashboard.test.ts b/src/lib/server/dashboard.test.ts index 06c45b8..b15fa6c 100644 --- a/src/lib/server/dashboard.test.ts +++ b/src/lib/server/dashboard.test.ts @@ -47,6 +47,68 @@ describe("dashboard runtime loader", () => { expect(store.listRevisions()).toHaveLength(1); }); + test("refreshes an existing initial seed when the bundled seed changes", async () => { + const store = await createTestStore(); + const oldSeed = olderDimensionLabSeed(); + store.seedDashboardIfEmpty(oldSeed, { + actor: "initial-seed", + message: "load initial dashboard document", + }); + + const runtime = loadDashboardRuntime(store, { + refreshSeedDocument: true, + seedDocument: dimensionLabDashboardFixture, + seedIfEmpty: true, + }); + + expect(runtime.state).toBe("ready"); + if (runtime.state !== "ready") throw new Error("expected ready dashboard"); + expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).toContain( + "auto-refresh", + ); + expect(runtime.document.serviceGroups.flatMap((group) => group.services).map((service) => service.id)).toContain( + "prompt-registry", + ); + expect(store.listRevisions()).toHaveLength(2); + expect(store.getActiveDashboard()?.revision.actor).toBe("initial-seed"); + }); + + test("does not refresh a dashboard after a user-authored revision", async () => { + const store = await createTestStore(); + const oldSeed = olderDimensionLabSeed(); + store.seedDashboardIfEmpty(oldSeed, { + actor: "initial-seed", + message: "load initial dashboard document", + }); + store.commitDashboard( + { + ...oldSeed, + metadata: { + ...oldSeed.metadata, + title: "Custom Dashboard", + }, + }, + { + actor: "agent", + message: "customize dashboard", + }, + ); + + const runtime = loadDashboardRuntime(store, { + refreshSeedDocument: true, + seedDocument: dimensionLabDashboardFixture, + seedIfEmpty: true, + }); + + expect(runtime.state).toBe("ready"); + if (runtime.state !== "ready") throw new Error("expected ready dashboard"); + expect(runtime.document.metadata.title).toBe("Custom Dashboard"); + expect(runtime.document.statusStrips[0]?.items.map((item) => item.id)).not.toContain( + "auto-refresh", + ); + expect(store.listRevisions()).toHaveLength(2); + }); + test("returns empty state when no dashboard is active and seeding is disabled", async () => { const store = await createTestStore(); @@ -69,3 +131,20 @@ async function createTestStore() { return store; } + +function olderDimensionLabSeed() { + const document = structuredClone(dimensionLabDashboardFixture); + document.statusStrips = document.statusStrips.map((strip) => ({ + ...strip, + items: strip.items.filter((item) => item.id !== "auto-refresh"), + })); + document.serviceGroups = document.serviceGroups.map((group) => + group.id === "ai-automation" + ? { + ...group, + services: group.services.filter((service) => service.id !== "prompt-registry"), + } + : group, + ); + return document; +} diff --git a/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts index d2bfc39..7821ee0 100644 --- a/src/lib/server/dashboard.ts +++ b/src/lib/server/dashboard.ts @@ -43,6 +43,7 @@ export interface DashboardRuntimeInvalid { } export interface DashboardRuntimeOptions { + refreshSeedDocument?: boolean; seedIfEmpty?: boolean; seedDocument?: DashboardDocument; } @@ -54,8 +55,20 @@ export function loadDashboardRuntime( const dashboardStore = store || createDashboardStore(); try { + const seedDocument = options.seedDocument || dimensionLabDashboardFixture; const active = dashboardStore.getActiveDashboard(); if (active) { + if ( + options.refreshSeedDocument && + shouldRefreshSeedDashboard(active, seedDocument) + ) { + const refreshed = dashboardStore.commitDashboard(seedDocument, { + actor: "initial-seed", + message: "refresh bundled dashboard document", + }); + return readyRuntimeState(refreshed.document, refreshed.id); + } + return readyRuntimeState(active.document, active.currentRevisionId); } @@ -69,7 +82,7 @@ export function loadDashboardRuntime( } const seeded = dashboardStore.seedDashboardIfEmpty( - options.seedDocument || dimensionLabDashboardFixture, + seedDocument, { actor: "initial-seed", message: "load initial dashboard document", @@ -113,3 +126,22 @@ function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid { errors, }; } + +function shouldRefreshSeedDashboard( + active: { document: DashboardDocument; revision: { actor: string } }, + seedDocument: DashboardDocument, +): boolean { + if (active.revision.actor !== "initial-seed") return false; + if (!isBundledDimensionLabSeed(active.document, seedDocument)) return false; + return JSON.stringify(active.document) !== JSON.stringify(seedDocument); +} + +function isBundledDimensionLabSeed( + document: DashboardDocument, + seedDocument: DashboardDocument, +): boolean { + return ( + document.metadata.title === seedDocument.metadata.title && + document.metadata.description === seedDocument.metadata.description + ); +} diff --git a/src/lib/ui/components/DashboardFrame.svelte b/src/lib/ui/components/DashboardFrame.svelte index 4ed42e1..5f1f0e4 100644 --- a/src/lib/ui/components/DashboardFrame.svelte +++ b/src/lib/ui/components/DashboardFrame.svelte @@ -113,12 +113,12 @@ overflow: hidden; } - .dashboard-frame__panels :global(.service-panel[data-model-id="runtime-health"]) { + .dashboard-frame__panels :global(.service-panel[data-layout="grid"]) { grid-column: 1 / -1; } .dashboard-frame__panels - :global(.service-panel[data-model-id="runtime-health"] .panel__body) { + :global(.service-panel[data-layout="grid"] .panel__body) { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -147,7 +147,7 @@ } .dashboard-frame__panels - :global(.service-panel[data-model-id="runtime-health"] .panel__body) { + :global(.service-panel[data-layout="grid"] .panel__body) { grid-template-columns: 1fr; } } diff --git a/src/lib/ui/components/ServicePanel.svelte b/src/lib/ui/components/ServicePanel.svelte index 0b997d7..864e09a 100644 --- a/src/lib/ui/components/ServicePanel.svelte +++ b/src/lib/ui/components/ServicePanel.svelte @@ -7,7 +7,7 @@ let { group }: { group: UiServiceGroup } = $props(); -