feat: render dashboard from model #20

Merged
vince merged 3 commits from codex/issue-7-model-renderer into main 2026-06-18 19:33:29 +02:00
11 changed files with 372 additions and 139 deletions
Showing only changes of commit fede33e8a7 - Show all commits

View file

@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest"; import { afterEach, describe, expect, test } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; import { createDashboardStore, type DashboardStore } from "./db/dashboard-store";
import { loadDashboardRuntime } from "./dashboard"; import { loadDashboardRuntime } from "./dashboard";
@ -21,13 +22,30 @@ describe("dashboard runtime loader", () => {
const runtime = loadDashboardRuntime(store); const runtime = loadDashboardRuntime(store);
expect(runtime.title).toBe(dimensionLabDashboardFixture.metadata.title); expect(runtime.state).toBe("ready");
expect(runtime.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle); if (runtime.state !== "ready") throw new Error("expected ready dashboard");
expect(runtime.status).toBe("building"); 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.schemaVersion).toBe(dimensionLabDashboardFixture.schemaVersion);
expect(runtime.currentRevisionId).toBe(store.getActiveDashboard()?.currentRevisionId); expect(runtime.currentRevisionId).toBe(store.getActiveDashboard()?.currentRevisionId);
expect(store.listRevisions()).toHaveLength(1); 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);
});
}); });
async function createTestStore() { async function createTestStore() {

View file

@ -1,31 +1,42 @@
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
import type { DashboardDocument } from "$lib/model";
import { import {
createDashboardStore, createDashboardStore,
DashboardPersistenceValidationError,
type DashboardStore, type DashboardStore,
} from "$lib/server/db/dashboard-store"; } from "$lib/server/db/dashboard-store";
import { UnsupportedDashboardModelVersionError } from "$lib/server/db/model-migrations";
export type PlaceholderStatus = "ready" | "building"; export type DashboardRuntimeState =
| DashboardRuntimeEmpty
| DashboardRuntimeInvalid
| DashboardRuntimeReady;
export interface PlaceholderDashboard { export interface DashboardRuntimeReady {
state: "ready";
document: DashboardDocument;
schemaVersion: string;
currentRevisionId: string;
}
export interface DashboardRuntimeEmpty {
state: "empty";
title: string; title: string;
subtitle: string; subtitle: string;
status: PlaceholderStatus;
message: string; message: string;
schemaVersion?: string;
currentRevisionId?: string;
} }
export function loadPlaceholderDashboard(): PlaceholderDashboard { export interface DashboardRuntimeInvalid {
return { state: "invalid";
title: "Dashboard Runtime", title: string;
subtitle: "Runtime scaffold", subtitle: string;
status: "building", message: string;
message: errors: string[];
"SvelteKit is running. Later issues will replace this placeholder with the validated dashboard model.",
};
} }
export function loadDashboardRuntime(store?: DashboardStore): PlaceholderDashboard { export function loadDashboardRuntime(
store?: DashboardStore,
): DashboardRuntimeState {
const dashboardStore = store || createDashboardStore(); const dashboardStore = store || createDashboardStore();
try { try {
@ -37,16 +48,42 @@ export function loadDashboardRuntime(store?: DashboardStore): PlaceholderDashboa
const document = active?.document || seeded.document; const document = active?.document || seeded.document;
const currentRevisionId = active?.currentRevisionId || seeded.id; const currentRevisionId = active?.currentRevisionId || seeded.id;
if (!document) {
return {
state: "empty",
title: "No Dashboard Model",
subtitle: "No active document",
message: "No validated dashboard document is active yet.",
};
}
return { return {
title: document.metadata.title, state: "ready",
subtitle: document.metadata.subtitle || "", document,
status: "building",
message:
"Active dashboard document loaded from SQLite. Later issues will replace this placeholder with model-driven rendering.",
schemaVersion: document.schemaVersion, schemaVersion: document.schemaVersion,
currentRevisionId, currentRevisionId,
}; };
} catch (error) {
if (error instanceof DashboardPersistenceValidationError) {
return invalidRuntimeState(error.failure.errors);
}
if (error instanceof UnsupportedDashboardModelVersionError) {
return invalidRuntimeState([error.message]);
}
throw error;
} finally { } finally {
if (!store) dashboardStore.close(); if (!store) dashboardStore.close();
} }
} }
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,
};
}

View file

@ -16,6 +16,7 @@
<a <a
class="footer-cell" class="footer-cell"
data-severity={item.severity || "neutral"} data-severity={item.severity || "neutral"}
data-model-id={item.id}
href={item.link.href} href={item.link.href}
target={target} target={target}
rel={rel} rel={rel}
@ -24,7 +25,7 @@
{@render cellContent()} {@render cellContent()}
</a> </a>
{:else} {:else}
<div class="footer-cell" data-severity={item.severity || "neutral"}> <div class="footer-cell" data-severity={item.severity || "neutral"} data-model-id={item.id}>
{@render cellContent()} {@render cellContent()}
</div> </div>
{/if} {/if}

View file

@ -5,7 +5,7 @@
let { module }: { module: UiModuleBlock } = $props(); let { module }: { module: UiModuleBlock } = $props();
</script> </script>
<aside class="module-card" data-severity={module.severity || "neutral"}> <aside class="module-card" data-severity={module.severity || "neutral"} data-model-id={module.id}>
<div> <div>
{#if module.title} {#if module.title}
<h2>{module.title}</h2> <h2>{module.title}</h2>

View file

@ -24,6 +24,7 @@
<a <a
class="service-row" class="service-row"
data-severity={service.severity} data-severity={service.severity}
data-model-id={service.id}
href={service.link.href} href={service.link.href}
target={target} target={target}
rel={rel} rel={rel}
@ -32,7 +33,7 @@
{@render rowContent()} {@render rowContent()}
</a> </a>
{:else} {:else}
<article class="service-row" data-severity={service.severity}> <article class="service-row" data-severity={service.severity} data-model-id={service.id}>
{@render rowContent()} {@render rowContent()}
</article> </article>
{/if} {/if}

View file

@ -9,7 +9,7 @@
let value = $derived(formatMetricValue(card.value)); let value = $derived(formatMetricValue(card.value));
</script> </script>
<article class="telemetry-card" data-severity={card.severity}> <article class="telemetry-card" data-severity={card.severity} data-model-id={card.id}>
<header> <header>
{#if card.icon} {#if card.icon}
<IconGlyph name={card.icon} size="sm" /> <IconGlyph name={card.icon} size="sm" />

View file

@ -23,6 +23,7 @@ export { default as TelemetryGrid } from "./components/TelemetryGrid.svelte";
export { default as TelemetryStrip } from "./components/TelemetryStrip.svelte"; export { default as TelemetryStrip } from "./components/TelemetryStrip.svelte";
export { default as WeatherModule } from "./components/WeatherModule.svelte"; export { default as WeatherModule } from "./components/WeatherModule.svelte";
export { dashboardPreviewFixtures } from "./fixtures"; export { dashboardPreviewFixtures } from "./fixtures";
export { dashboardDocumentToUiDashboard } from "./model-renderer";
export type { export type {
UiDashboardPreview, UiDashboardPreview,
UiLink, UiLink,

View file

@ -0,0 +1,90 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import {
type DashboardDocument,
} from "$lib/model";
import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { dashboardDocumentToUiDashboard } from "./model-renderer";
describe("dashboard model renderer", () => {
test("projects the Dimension Lab model into UI component props", () => {
const dashboard = dashboardDocumentToUiDashboard(dimensionLabDashboardFixture);
expect(dashboard.title).toBe(dimensionLabDashboardFixture.metadata.title);
expect(dashboard.subtitle).toBe(dimensionLabDashboardFixture.metadata.subtitle);
expect(dashboard.telemetry.map((card) => card.id)).toEqual(
dimensionLabDashboardFixture.layout.telemetry,
);
expect(dashboard.telemetry[0]).toMatchObject({
label: "Infra RAM",
icon: "mdi:memory",
severity: "ok",
});
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
dimensionLabDashboardFixture.layout.serviceGroups,
);
expect(dashboard.modules.map((module) => module.id)).toEqual([
"weather-amsterdam",
"runtime-health-summary",
]);
expect(dashboard.statusItems.map((item) => item.id)).toEqual([
"footer-status:system-status",
"footer-status:last-sync",
"footer-status:uptime",
"footer-status:load-avg",
]);
});
test("projects a second fixture through the same mapper", () => {
const dashboard = dashboardDocumentToUiDashboard(genericDashboardFixture);
expect(dashboard.title).toBe("Operations Console");
expect(dashboard.telemetry.map((card) => card.id)).toEqual([
"service-uptime",
"queue-depth",
]);
expect(dashboard.serviceGroups[0]?.services.map((service) => service.id)).toEqual([
"identity",
"scheduler",
]);
expect(dashboard.modules[0]).toMatchObject({
id: "ambient",
title: "Environment",
icon: "mdi:radar",
});
});
test("returns intentional empty arrays for missing optional sections", () => {
const emptyModel: DashboardDocument = {
...genericDashboardFixture,
layout: {
density: "compact",
telemetry: [],
serviceGroups: [],
statusStrips: [],
modules: [],
},
telemetry: [],
serviceGroups: [],
statusStrips: [],
modules: [],
};
const dashboard = dashboardDocumentToUiDashboard(emptyModel);
expect(dashboard.telemetry).toEqual([]);
expect(dashboard.serviceGroups).toEqual([]);
expect(dashboard.statusItems).toEqual([]);
expect(dashboard.modules).toEqual([]);
});
test("does not hardcode environment-specific content in mapper source", () => {
const source = readFileSync(join(process.cwd(), "src/lib/ui/model-renderer.ts"), "utf8").toLowerCase();
expect(source).not.toContain("dimension");
expect(source).not.toContain("vaultwarden");
expect(source).not.toContain("forgejo");
});
});

View file

@ -0,0 +1,109 @@
import type {
DashboardDocument,
DashboardModule,
ServiceEntry,
ServiceGroup,
StatusItem,
StatusStrip,
TelemetryCard,
} from "$lib/model";
import type {
UiDashboardPreview,
UiModuleBlock,
UiServiceGroup,
UiServiceRow,
UiStatusItem,
UiTelemetryCard,
} from "./types";
export function dashboardDocumentToUiDashboard(
document: DashboardDocument,
): UiDashboardPreview {
return {
eyebrow: document.schemaVersion,
title: document.metadata.title,
subtitle: document.metadata.subtitle,
telemetry: orderedItems(document.layout.telemetry, document.telemetry).map(
telemetryToUi,
),
serviceGroups: orderedItems(
document.layout.serviceGroups,
document.serviceGroups,
).map(serviceGroupToUi),
modules: orderedItems(
document.layout.modules || [],
document.modules || [],
).map(moduleToUi),
statusItems: orderedItems(
document.layout.statusStrips,
document.statusStrips,
).flatMap(statusStripToUiItems),
};
}
function telemetryToUi(card: TelemetryCard): UiTelemetryCard {
return {
id: card.id,
label: card.label,
description: card.description,
icon: card.icon,
value: card.value,
detail: card.detail,
severity: card.severity,
sparkline: card.sparkline,
};
}
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
return {
id: group.id,
title: group.title,
services: group.services.map(serviceToUi),
};
}
function serviceToUi(service: ServiceEntry): UiServiceRow {
return {
id: service.id,
label: service.label,
description: service.description,
icon: service.icon,
severity: service.severity,
detail: service.detail,
link: service.link,
};
}
function moduleToUi(module: DashboardModule): UiModuleBlock {
return {
id: module.id,
title: module.title,
label: module.label,
value: module.value,
detail: module.detail,
icon: module.icon,
severity: module.severity,
};
}
function statusStripToUiItems(strip: StatusStrip): UiStatusItem[] {
return strip.items.map((item) => statusItemToUi(strip.id, item));
}
function statusItemToUi(stripId: string, item: StatusItem): UiStatusItem {
return {
id: `${stripId}:${item.id}`,
label: item.label,
value: item.value,
severity: item.severity,
link: item.link,
};
}
function orderedItems<T extends { id: string }>(ids: string[], items: T[]): T[] {
const byId = new Map(items.map((item) => [item.id, item]));
return ids.flatMap((id) => {
const item = byId.get(id);
return item ? [item] : [];
});
}

View file

@ -1,124 +1,80 @@
<script lang="ts"> <script lang="ts">
import { DashboardFrame, dashboardPreviewFixtures } from "$lib/ui"; import {
DashboardFrame,
SystemState,
dashboardDocumentToUiDashboard,
} from "$lib/ui";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
const fixtureKeys = ["runtime", "primary", "secondary"] as const;
let selectedFixture: (typeof fixtureKeys)[number] = $state("runtime");
let runtimeDashboard = $derived({
...dashboardPreviewFixtures.primary,
eyebrow: data.dashboard.schemaVersion || "Runtime",
title: data.dashboard.title,
subtitle: data.dashboard.subtitle,
modules: [
{
id: "runtime-state",
title: "Runtime",
value: data.dashboard.status,
detail: data.dashboard.message,
icon: "mdi:database-check-outline",
severity: "loading" as const,
},
],
statusItems: [
{
id: "runtime-status",
label: "Runtime",
value: data.dashboard.status,
severity: "loading" as const,
},
{
id: "schema-version",
label: "Schema",
value: data.dashboard.schemaVersion || "pending",
severity: "neutral" as const,
},
{
id: "revision",
label: "Revision",
value: data.dashboard.currentRevisionId?.slice(0, 12) || "pending",
severity: "stale" as const,
},
],
});
let dashboard = $derived( let dashboard = $derived(
selectedFixture === "runtime" data.dashboard.state === "ready"
? runtimeDashboard ? dashboardDocumentToUiDashboard(data.dashboard.document)
: dashboardPreviewFixtures[selectedFixture], : null,
);
let stateTitle = $derived(
data.dashboard.state === "ready" ? "" : data.dashboard.title,
);
let stateSubtitle = $derived(
data.dashboard.state === "ready" ? "" : data.dashboard.subtitle,
);
let stateMessage = $derived(
data.dashboard.state === "ready" ? "" : data.dashboard.message,
);
let stateErrors = $derived(
data.dashboard.state === "invalid" ? data.dashboard.errors : [],
);
let pageTitle = $derived(dashboard?.title || stateTitle);
let pageDescription = $derived(
dashboard?.subtitle || stateSubtitle || stateMessage,
); );
</script> </script>
<svelte:head> <svelte:head>
<title>{dashboard.title}</title> <title>{pageTitle}</title>
<meta <meta name="description" content={pageDescription} />
name="description"
content="Standalone SvelteKit runtime for a model-driven system overview dashboard."
/>
</svelte:head> </svelte:head>
<div class="preview-shell"> {#if dashboard}
<div class="fixture-switcher" aria-label="Preview fixture">
{#each fixtureKeys as key}
<button
type="button"
class:active={selectedFixture === key}
aria-pressed={selectedFixture === key}
onclick={() => {
selectedFixture = key;
}}
>
{key}
</button>
{/each}
</div>
<DashboardFrame {dashboard} /> <DashboardFrame {dashboard} />
</div> {:else}
<main class="state-shell" data-dashboard-state={data.dashboard.state}>
<SystemState
title={stateTitle}
detail={`${stateSubtitle}: ${stateMessage}`}
severity={data.dashboard.state === "invalid" ? "danger" : "stale"}
icon={data.dashboard.state === "invalid" ? "mdi:file-alert-outline" : "mdi:tray"}
/>
{#if stateErrors.length}
<ul aria-label="Validation errors">
{#each stateErrors as error}
<li>{error}</li>
{/each}
</ul>
{/if}
</main>
{/if}
<style> <style>
.preview-shell { .state-shell {
position: relative; display: grid;
min-height: 100vh; min-height: 100vh;
align-content: center;
gap: var(--ui-space-3);
padding: var(--ui-space-4);
} }
.fixture-switcher { ul {
position: fixed; display: grid;
z-index: var(--ui-z-overlay); max-width: 56rem;
top: var(--ui-space-3); gap: var(--ui-space-2);
right: var(--ui-space-3); margin: 0;
display: inline-grid;
grid-auto-flow: column;
gap: 1px;
border: var(--ui-border); border: var(--ui-border);
background: var(--ui-color-line); background: rgba(6, 8, 7, 0.82);
}
button {
min-width: 5.5rem;
min-height: 2rem;
border: 0;
background: rgba(2, 3, 2, 0.92);
color: var(--ui-color-muted); color: var(--ui-color-muted);
cursor: pointer; font-size: 0.76rem;
font-size: 0.72rem; list-style-position: inside;
font-weight: 800; padding: var(--ui-space-3);
letter-spacing: 0;
text-transform: uppercase;
}
button.active {
background: var(--ui-color-accent);
color: var(--ui-color-canvas);
}
@media (max-width: 780px) {
.fixture-switcher {
position: sticky;
top: 0;
right: auto;
display: grid;
grid-template-columns: repeat(2, 1fr);
}
} }
</style> </style>

View file

@ -1,17 +1,16 @@
import { render } from "svelte/server"; import { render } from "svelte/server";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import Page from "./+page.svelte"; import Page from "./+page.svelte";
describe("home page design-system preview", () => { describe("home page model renderer", () => {
test("renders the runtime dashboard data from the server load", () => { test("renders the active dashboard model from the server load", () => {
const { body } = render(Page, { const { body } = render(Page, {
props: { props: {
data: { data: {
dashboard: { dashboard: {
title: "Runtime Surface", state: "ready",
subtitle: "Loaded from persistence", document: genericDashboardFixture,
status: "building",
message: "Runtime model is available.",
schemaVersion: "dashboard.v1", schemaVersion: "dashboard.v1",
currentRevisionId: "revision-1234567890", currentRevisionId: "revision-1234567890",
}, },
@ -19,10 +18,31 @@ describe("home page design-system preview", () => {
}, },
}); });
expect(body).toContain("Runtime Surface"); expect(body).toContain("Operations Console");
expect(body).toContain("Loaded from persistence"); expect(body).toContain("Service Uptime");
expect(body).toContain("revision"); expect(body).toContain("Identity");
expect(body).toContain("primary"); expect(body).toContain("data-model-id=\"service-uptime\"");
expect(body).toContain("secondary"); 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.");
}); });
}); });