feat(dashboard): add live observability overview #23

Merged
vince merged 2 commits from codex/issue-1-observability-dashboard into main 2026-06-19 03:57:29 +02:00
11 changed files with 188 additions and 10 deletions
Showing only changes of commit bfb9a91e49 - Show all commits

View file

@ -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,
};
}

View file

@ -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;
}

View file

@ -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
);
}

View file

@ -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;
}
}

View file

@ -7,7 +7,7 @@
let { group }: { group: UiServiceGroup } = $props();
</script>
<div class="service-panel" data-model-id={group.id}>
<div class="service-panel" data-layout={group.layout || "list"} data-model-id={group.id}>
<Panel title={group.title}>
{#if group.summary?.length}
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact />

View file

@ -25,6 +25,9 @@ describe("dashboard model renderer", () => {
expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
dimensionLabDashboardFixture.layout.serviceGroups,
);
expect(dashboard.serviceGroups.find((group) => group.id === "runtime-health")?.layout).toBe(
"grid",
);
expect(dashboard.modules.map((module) => module.id)).toEqual([
"weather-amsterdam",
"runtime-health-summary",

View file

@ -58,6 +58,7 @@ function telemetryToUi(card: TelemetryCard): UiTelemetryCard {
function serviceGroupToUi(group: ServiceGroup): UiServiceGroup {
return {
id: group.id,
layout: group.layout,
title: group.title,
services: group.services.map(serviceToUi),
};

View file

@ -52,6 +52,7 @@ export interface UiServiceRow {
export interface UiServiceGroup {
id: string;
layout?: "grid" | "list";
title: string;
services: UiServiceRow[];
summary?: UiStatusItem[];

View file

@ -2,7 +2,10 @@ import { loadDashboardRuntime } from "$lib/server/dashboard";
import { resolveDashboardDatasources } from "$lib/server/datasources";
export async function load() {
const dashboard = loadDashboardRuntime(undefined, { seedIfEmpty: true });
const dashboard = loadDashboardRuntime(undefined, {
refreshSeedDocument: true,
seedIfEmpty: true,
});
if (dashboard.state !== "ready") {
return { dashboard };

View file

@ -1,9 +1,12 @@
<script lang="ts">
import { browser } from "$app/environment";
import { invalidateAll } from "$app/navigation";
import {
DashboardFrame,
SystemState,
dashboardDocumentToUiDashboard,
} from "$lib/ui";
import { onDestroy } from "svelte";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
@ -29,6 +32,30 @@
let pageDescription = $derived(
dashboard?.subtitle || stateSubtitle || stateMessage,
);
let refreshTimer: number | undefined;
$effect(() => {
if (!browser) return;
if (refreshTimer) {
window.clearInterval(refreshTimer);
refreshTimer = undefined;
}
const refreshIntervalSeconds =
data.dashboard.state === "ready"
? data.dashboard.document.metadata.refreshIntervalSeconds
: undefined;
if (!refreshIntervalSeconds) return;
refreshTimer = window.setInterval(() => {
void invalidateAll();
}, refreshIntervalSeconds * 1000);
});
onDestroy(() => {
if (refreshTimer) window.clearInterval(refreshTimer);
});
</script>
<svelte:head>

View file

@ -64,10 +64,33 @@ test.describe("dashboard page QA gate", () => {
const metrics = await page.evaluate(() => {
const footer = document.querySelector("[data-model-id='footer-status']");
const runtime = document.querySelector("[data-model-id='runtime-health']");
const visibleItems = [
...document.querySelectorAll(".telemetry-card"),
...document.querySelectorAll(".service-row"),
...document.querySelectorAll(".footer-cell"),
].map((element) => {
const rect = element.getBoundingClientRect();
return {
bottom: rect.bottom,
height: rect.height,
id: element.getAttribute("data-model-id"),
top: rect.top,
width: rect.width,
};
});
return {
clippedItems: visibleItems.filter((item) =>
item.top < 0 ||
item.bottom > window.innerHeight ||
item.width <= 0 ||
item.height <= 0
),
footerCellCount: document.querySelectorAll(".footer-cell").length,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
serviceRowCount: document.querySelectorAll(".service-row").length,
telemetryCardCount: document.querySelectorAll(".telemetry-card").length,
footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
runtimeBottom: runtime?.getBoundingClientRect().bottom ?? 0,
};
@ -77,6 +100,10 @@ test.describe("dashboard page QA gate", () => {
expect(metrics.scrollHeight).toBeLessThanOrEqual(956);
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
expect(metrics.telemetryCardCount).toBe(16);
expect(metrics.serviceRowCount).toBe(28);
expect(metrics.footerCellCount).toBe(5);
expect(metrics.clippedItems).toEqual([]);
});
test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {