diff --git a/src/lib/server/dashboard.test.ts b/src/lib/server/dashboard.test.ts index 91a5ffb..06c45b8 100644 --- a/src/lib/server/dashboard.test.ts +++ b/src/lib/server/dashboard.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; import { loadDashboardRuntime } from "./dashboard"; @@ -19,15 +20,43 @@ describe("dashboard runtime loader", () => { test("seeds and loads the active dashboard from sqlite", async () => { const store = await createTestStore(); - const runtime = loadDashboardRuntime(store); + const runtime = loadDashboardRuntime(store, { seedIfEmpty: true }); - expect(runtime.title).toBe(dimensionLabDashboardFixture.metadata.title); - expect(runtime.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle); - expect(runtime.status).toBe("building"); + expect(runtime.state).toBe("ready"); + if (runtime.state !== "ready") throw new Error("expected ready dashboard"); + expect(runtime.document.metadata.title).toBe(dimensionLabDashboardFixture.metadata.title); + expect(runtime.document.metadata.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle); expect(runtime.schemaVersion).toBe(dimensionLabDashboardFixture.schemaVersion); expect(runtime.currentRevisionId).toBe(store.getActiveDashboard()?.currentRevisionId); expect(store.listRevisions()).toHaveLength(1); }); + + test("loads an existing active dashboard without reseeding", async () => { + const store = await createTestStore(); + const seed = store.commitDashboard(genericDashboardFixture, { + actor: "test", + message: "existing dashboard", + }); + + const runtime = loadDashboardRuntime(store); + + expect(runtime.state).toBe("ready"); + if (runtime.state !== "ready") throw new Error("expected ready dashboard"); + expect(runtime.currentRevisionId).toBe(seed.id); + expect(runtime.document.metadata.title).toBe(genericDashboardFixture.metadata.title); + expect(store.listRevisions()).toHaveLength(1); + }); + + test("returns empty state when no dashboard is active and seeding is disabled", async () => { + const store = await createTestStore(); + + const runtime = loadDashboardRuntime(store); + + expect(runtime.state).toBe("empty"); + if (runtime.state !== "empty") throw new Error("expected empty dashboard"); + expect(runtime.title).toBe("No Dashboard Model"); + expect(store.listRevisions()).toHaveLength(0); + }); }); async function createTestStore() { diff --git a/src/lib/server/dashboard.ts b/src/lib/server/dashboard.ts index 56380a4..d2bfc39 100644 --- a/src/lib/server/dashboard.ts +++ b/src/lib/server/dashboard.ts @@ -1,52 +1,115 @@ import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import type { DashboardDocument } from "$lib/model"; import { createDashboardStore, + DashboardPersistenceValidationError, type DashboardStore, } from "$lib/server/db/dashboard-store"; +import { UnsupportedDashboardModelVersionError } from "$lib/server/db/model-migrations"; -export type PlaceholderStatus = "ready" | "building"; +export type DashboardRuntimeState = + | DashboardRuntimeEmpty + | DashboardRuntimeInvalid + | DashboardRuntimeLoading + | DashboardRuntimeReady; -export interface PlaceholderDashboard { +export interface DashboardRuntimeReady { + state: "ready"; + document: DashboardDocument; + schemaVersion: string; + currentRevisionId: string; +} + +export interface DashboardRuntimeEmpty { + state: "empty"; title: string; subtitle: string; - status: PlaceholderStatus; message: string; - schemaVersion?: string; - currentRevisionId?: string; } -export function loadPlaceholderDashboard(): PlaceholderDashboard { - return { - title: "Dashboard Runtime", - subtitle: "Runtime scaffold", - status: "building", - message: - "SvelteKit is running. Later issues will replace this placeholder with the validated dashboard model.", - }; +export interface DashboardRuntimeLoading { + state: "loading"; + title: string; + subtitle: string; + message: string; } -export function loadDashboardRuntime(store?: DashboardStore): PlaceholderDashboard { +export interface DashboardRuntimeInvalid { + state: "invalid"; + title: string; + subtitle: string; + message: string; + errors: string[]; +} + +export interface DashboardRuntimeOptions { + seedIfEmpty?: boolean; + seedDocument?: DashboardDocument; +} + +export function loadDashboardRuntime( + store?: DashboardStore, + options: DashboardRuntimeOptions = {}, +): DashboardRuntimeState { const dashboardStore = store || createDashboardStore(); try { - const seeded = dashboardStore.seedDashboardIfEmpty(dimensionLabDashboardFixture, { - actor: "initial-seed", - message: "load initial dashboard document", - }); const active = dashboardStore.getActiveDashboard(); - const document = active?.document || seeded.document; - const currentRevisionId = active?.currentRevisionId || seeded.id; + if (active) { + return readyRuntimeState(active.document, active.currentRevisionId); + } - return { - title: document.metadata.title, - subtitle: document.metadata.subtitle || "", - status: "building", - message: - "Active dashboard document loaded from SQLite. Later issues will replace this placeholder with model-driven rendering.", - schemaVersion: document.schemaVersion, - currentRevisionId, - }; + if (!options.seedIfEmpty) { + return { + state: "empty", + title: "No Dashboard Model", + subtitle: "No active document", + message: "No validated dashboard document is active yet.", + }; + } + + const seeded = dashboardStore.seedDashboardIfEmpty( + options.seedDocument || dimensionLabDashboardFixture, + { + actor: "initial-seed", + message: "load initial dashboard document", + }, + ); + + return readyRuntimeState(seeded.document, seeded.id); + } catch (error) { + if (error instanceof DashboardPersistenceValidationError) { + return invalidRuntimeState(error.failure.errors); + } + + if (error instanceof UnsupportedDashboardModelVersionError) { + return invalidRuntimeState([error.message]); + } + + throw error; } finally { if (!store) dashboardStore.close(); } } + +function readyRuntimeState( + document: DashboardDocument, + currentRevisionId: string, +): DashboardRuntimeReady { + return { + state: "ready", + document, + schemaVersion: document.schemaVersion, + currentRevisionId, + }; +} + +function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid { + return { + state: "invalid", + title: "Invalid Dashboard Model", + subtitle: "Validation failed", + message: "The active dashboard document could not be validated.", + errors, + }; +} diff --git a/src/lib/ui/components/DashboardFrame.svelte b/src/lib/ui/components/DashboardFrame.svelte index 0a76f67..4a4fff8 100644 --- a/src/lib/ui/components/DashboardFrame.svelte +++ b/src/lib/ui/components/DashboardFrame.svelte @@ -44,7 +44,7 @@ {/each} - + + + diff --git a/src/routes/page.test.ts b/src/routes/page.test.ts index 5c9d0b7..c302884 100644 --- a/src/routes/page.test.ts +++ b/src/routes/page.test.ts @@ -1,17 +1,16 @@ import { render } from "svelte/server"; import { describe, expect, test } from "vitest"; +import { genericDashboardFixture } from "$lib/model/fixtures/generic"; import Page from "./+page.svelte"; -describe("home page design-system preview", () => { - test("renders the runtime dashboard data from the server load", () => { +describe("home page model renderer", () => { + test("renders the active dashboard model from the server load", () => { const { body } = render(Page, { props: { data: { dashboard: { - title: "Runtime Surface", - subtitle: "Loaded from persistence", - status: "building", - message: "Runtime model is available.", + state: "ready", + document: genericDashboardFixture, schemaVersion: "dashboard.v1", currentRevisionId: "revision-1234567890", }, @@ -19,10 +18,63 @@ describe("home page design-system preview", () => { }, }); - expect(body).toContain("Runtime Surface"); - expect(body).toContain("Loaded from persistence"); - expect(body).toContain("revision"); - expect(body).toContain("primary"); - expect(body).toContain("secondary"); + expect(body).toContain("Operations Console"); + expect(body).toContain("Service Uptime"); + expect(body).toContain("Identity"); + expect(body).toContain("data-model-id=\"service-uptime\""); + expect(body).not.toContain("primary"); + expect(body).not.toContain("secondary"); + }); + + test("renders invalid model state without crashing", () => { + const { body } = render(Page, { + props: { + data: { + dashboard: { + state: "invalid", + title: "Invalid Dashboard", + subtitle: "Validation failed", + message: "Dashboard document is invalid.", + errors: ["/metadata/title is required"], + }, + }, + }, + }); + + expect(body).toContain("Invalid Dashboard"); + expect(body).toContain("Validation failed"); + expect(body).toContain("Dashboard document is invalid."); + }); + + test("renders empty and loading model states without crashing", () => { + const empty = render(Page, { + props: { + data: { + dashboard: { + state: "empty", + title: "No Dashboard Model", + subtitle: "No active document", + message: "No validated dashboard document is active yet.", + }, + }, + }, + }); + const loading = render(Page, { + props: { + data: { + dashboard: { + state: "loading", + title: "Loading Dashboard", + subtitle: "Fetching active model", + message: "Waiting for the active dashboard document.", + }, + }, + }, + }); + + expect(empty.body).toContain("No Dashboard Model"); + expect(empty.body).toContain("No active document"); + expect(loading.body).toContain("Loading Dashboard"); + expect(loading.body).toContain("Fetching active model"); }); });