fix(dashboard): address observability review findings

This commit is contained in:
vince 2026-06-19 03:52:20 +02:00
parent 2d2b905d18
commit bfb9a91e49
11 changed files with 188 additions and 10 deletions

View file

@ -548,7 +548,7 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
datasource: uptimeMonitor(27), datasource: uptimeMonitor(27),
detail: "fallback - exporter health from Prometheus", detail: "fallback - exporter health from Prometheus",
}), }),
]), ], "grid"),
], ],
statusStrips: [ 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 { return {
id, id,
title, layout,
layout: "list",
services, services,
title,
}; };
} }

View file

@ -47,6 +47,68 @@ describe("dashboard runtime loader", () => {
expect(store.listRevisions()).toHaveLength(1); 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 () => { test("returns empty state when no dashboard is active and seeding is disabled", async () => {
const store = await createTestStore(); const store = await createTestStore();
@ -69,3 +131,20 @@ async function createTestStore() {
return store; 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 { export interface DashboardRuntimeOptions {
refreshSeedDocument?: boolean;
seedIfEmpty?: boolean; seedIfEmpty?: boolean;
seedDocument?: DashboardDocument; seedDocument?: DashboardDocument;
} }
@ -54,8 +55,20 @@ export function loadDashboardRuntime(
const dashboardStore = store || createDashboardStore(); const dashboardStore = store || createDashboardStore();
try { try {
const seedDocument = options.seedDocument || dimensionLabDashboardFixture;
const active = dashboardStore.getActiveDashboard(); const active = dashboardStore.getActiveDashboard();
if (active) { 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); return readyRuntimeState(active.document, active.currentRevisionId);
} }
@ -69,7 +82,7 @@ export function loadDashboardRuntime(
} }
const seeded = dashboardStore.seedDashboardIfEmpty( const seeded = dashboardStore.seedDashboardIfEmpty(
options.seedDocument || dimensionLabDashboardFixture, seedDocument,
{ {
actor: "initial-seed", actor: "initial-seed",
message: "load initial dashboard document", message: "load initial dashboard document",
@ -113,3 +126,22 @@ function invalidRuntimeState(errors: string[]): DashboardRuntimeInvalid {
errors, 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; 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; grid-column: 1 / -1;
} }
.dashboard-frame__panels .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)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@ -147,7 +147,7 @@
} }
.dashboard-frame__panels .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; grid-template-columns: 1fr;
} }
} }

View file

@ -7,7 +7,7 @@
let { group }: { group: UiServiceGroup } = $props(); let { group }: { group: UiServiceGroup } = $props();
</script> </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}> <Panel title={group.title}>
{#if group.summary?.length} {#if group.summary?.length}
<StatusStrip id={`${group.id}:summary`} items={group.summary} compact /> <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( expect(dashboard.serviceGroups.map((group) => group.id)).toEqual(
dimensionLabDashboardFixture.layout.serviceGroups, dimensionLabDashboardFixture.layout.serviceGroups,
); );
expect(dashboard.serviceGroups.find((group) => group.id === "runtime-health")?.layout).toBe(
"grid",
);
expect(dashboard.modules.map((module) => module.id)).toEqual([ expect(dashboard.modules.map((module) => module.id)).toEqual([
"weather-amsterdam", "weather-amsterdam",
"runtime-health-summary", "runtime-health-summary",

View file

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

View file

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

View file

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

View file

@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import { invalidateAll } from "$app/navigation";
import { import {
DashboardFrame, DashboardFrame,
SystemState, SystemState,
dashboardDocumentToUiDashboard, dashboardDocumentToUiDashboard,
} from "$lib/ui"; } from "$lib/ui";
import { onDestroy } from "svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -29,6 +32,30 @@
let pageDescription = $derived( let pageDescription = $derived(
dashboard?.subtitle || stateSubtitle || stateMessage, 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> </script>
<svelte:head> <svelte:head>

View file

@ -64,10 +64,33 @@ test.describe("dashboard page QA gate", () => {
const metrics = await page.evaluate(() => { const metrics = await page.evaluate(() => {
const footer = document.querySelector("[data-model-id='footer-status']"); const footer = document.querySelector("[data-model-id='footer-status']");
const runtime = document.querySelector("[data-model-id='runtime-health']"); 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 { 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, scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight, scrollHeight: document.documentElement.scrollHeight,
serviceRowCount: document.querySelectorAll(".service-row").length,
telemetryCardCount: document.querySelectorAll(".telemetry-card").length,
footerBottom: footer?.getBoundingClientRect().bottom ?? 0, footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
runtimeBottom: runtime?.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.scrollHeight).toBeLessThanOrEqual(956);
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956); expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).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) => { test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {