diff --git a/.forgejo/workflows/dimensionlab-website.yml b/.forgejo/workflows/dimensionlab-website.yml
new file mode 100644
index 0000000..198a8aa
--- /dev/null
+++ b/.forgejo/workflows/dimensionlab-website.yml
@@ -0,0 +1,85 @@
+name: Dimension Lab website
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+concurrency:
+ group: dimensionlab-website-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+ ci:
+ runs-on: docker
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: https://data.forgejo.org/actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: false
+
+ - name: Initialize submodules
+ run: |
+ git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
+ git submodule update --init --recursive
+
+ - name: Install Bun
+ run: |
+ curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14"
+ "$HOME/.bun/bin/bun" --version
+
+ - name: Check, test, and build
+ run: |
+ export BUN_INSTALL="$HOME/.bun"
+ export PATH="$BUN_INSTALL/bin:$PATH"
+ bun install --frozen-lockfile
+ bun run check
+ bun run test
+ bun run build
+
+ deploy:
+ needs: ci
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ runs-on: deploy
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ run: |
+ if [ -d .git ]; then
+ git remote set-url origin git@git.dimensionlab.net:vince/dimensionlab-website.git
+ else
+ git init
+ git remote add origin git@git.dimensionlab.net:vince/dimensionlab-website.git
+ fi
+ git fetch --force --prune --depth=1 origin "$GITHUB_SHA"
+ git checkout --force --detach "$GITHUB_SHA"
+ git clean -ffdx
+
+ - name: Initialize submodules
+ run: |
+ git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
+ git submodule update --init --recursive
+
+ - name: Verify Podman deployment socket
+ run: |
+ command -v podman
+ command -v systemctl
+ unit="$(timeout 15s podman inspect dimensionlab-website --format '{{ index .Config.Labels "PODMAN_SYSTEMD_UNIT" }}')"
+ test "$unit" = "dimensionlab-website.service"
+
+ - name: Deploy production website
+ env:
+ DEPLOY_CONTAINER_CLI: podman
+ DEPLOY_EVENT_NAME: ${{ github.event_name }}
+ DEPLOY_REF: ${{ github.ref }}
+ DEPLOY_RESTART_STRATEGY: quadlet-container
+ DEPLOY_SHA: ${{ github.sha }}
+ run: scripts/deploy-dimensionlab-website.sh
diff --git a/README.md b/README.md
index 00d9708..85177c5 100644
--- a/README.md
+++ b/README.md
@@ -161,3 +161,27 @@ The env file must provide `AGENT_CONFIG_TOKEN`. Runtime defaults inside the
image set `HOST=0.0.0.0`, `PORT=3000`,
`DATABASE_URL=file:/data/dimensionlab.sqlite`, and
`DASHBOARD_MIGRATIONS_DIR=/repo/apps/web/drizzle`.
+
+### Forgejo Actions Deployment
+
+Merges to `main` run `.forgejo/workflows/dimensionlab-website.yml`. Pull
+requests run check, test, and build only; the deploy job is guarded to run only
+for `push` events on `refs/heads/main`.
+
+The workflow uses two runner classes. Pull request CI runs on the containerized
+`docker` runner. Production deployment runs on a separate host runner with the
+`deploy:host` label so the guarded deploy script can use the user's rootless
+`podman` and `systemctl --user` commands directly. The deploy job uses a
+shell-only `git fetch` checkout so the host runner does not need a Node runtime
+for checkout actions.
+
+```yaml
+runner:
+ labels:
+ - deploy:host
+```
+
+The deploy job also performs a host preflight against the
+`dimensionlab-website.service` Podman label before it builds or restarts the
+production container. Do not give the general pull request runner deployment
+socket access; keep deploy privileges on the dedicated `deploy` runner.
diff --git a/apps/web/Containerfile b/apps/web/Containerfile
index 9ec2baa..32387a4 100644
--- a/apps/web/Containerfile
+++ b/apps/web/Containerfile
@@ -11,7 +11,7 @@ RUN bunx turbo prune @dimensionlab/web --docker
FROM base AS deps
COPY --from=pruner /repo/out/json/ ./
-RUN bun install --frozen-lockfile
+RUN bun install --frozen-lockfile --ignore-scripts
FROM deps AS build
diff --git a/apps/web/package.json b/apps/web/package.json
index 790663c..015fc55 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -8,8 +8,8 @@
"build": "rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build",
"preview": "HOST=0.0.0.0 PORT=4173 bun build/index.js",
"check": "tsc --noEmit",
- "test": "vitest run",
- "test:unit": "vitest run",
+ "test": "bun --bun vitest run",
+ "test:unit": "bun --bun vitest run",
"test:e2e": "env -u NO_COLOR playwright test",
"db:generate": "drizzle-kit generate",
"db:check": "drizzle-kit check"
diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx
index fbb133d..c1add67 100644
--- a/apps/web/src/App.test.tsx
+++ b/apps/web/src/App.test.tsx
@@ -1,6 +1,13 @@
import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
-import { AppStateView } from "./App";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
+import {
+ AppStateView,
+ dashboardTileMatchKey,
+ dashboardHydrationTiles,
+ restoreDashboardTileSnapshots,
+} from "./App";
describe("React app dashboard state view", () => {
test("renders loading dashboard state", () => {
@@ -41,4 +48,191 @@ describe("React app dashboard state view", () => {
expect(html).toContain("Dark");
expect(html).toContain("Light");
});
+
+ test("renders the dashboard shell while individual items hydrate", () => {
+ const html = renderToString(
+ ,
+ );
+
+ expect(html).toContain("Operations Console");
+ expect(html).toContain("Service Uptime");
+ expect(html).toContain("Identity");
+ expect(html).toContain("Environment");
+ expect(html).not.toContain("Loading Dashboard");
+ expect(html).toContain(
+ 'data-severity="loading" data-model-id="service-uptime"',
+ );
+ expect(html).toContain('data-severity="loading" data-model-id="identity"');
+ expect(html).toContain('data-severity="loading" data-model-id="ambient"');
+ expect(html).toContain('data-severity="loading" data-model-id="runtime:status"');
+ });
+
+ test("hydrates every status cell from the Dimension Lab shell", () => {
+ expect(dashboardHydrationTiles(dimensionLabDashboardFixture)).toContainEqual({
+ kind: "status",
+ stripId: "footer-status",
+ id: "auto-refresh",
+ });
+ });
+
+ test("prioritizes status, telemetry, modules, then services for hydration", () => {
+ const tiles = dashboardHydrationTiles(dimensionLabDashboardFixture);
+ const kinds = tiles.map((tile) => tile.kind);
+ const firstServiceIndex = kinds.indexOf("service");
+
+ expect(kinds.slice(0, 5)).toEqual([
+ "status",
+ "status",
+ "status",
+ "status",
+ "status",
+ ]);
+ expect(kinds.lastIndexOf("telemetry")).toBeLessThan(kinds.indexOf("module"));
+ expect(kinds.lastIndexOf("module")).toBeLessThan(firstServiceIndex);
+ expect(
+ tiles.filter((tile) => tile.kind === "telemetry").map((tile) => tile.id),
+ ).toEqual(
+ dimensionLabDashboardFixture.telemetry
+ .filter((card) => card.datasource?.type === "external")
+ .map((card) => card.id),
+ );
+ expect(
+ tiles.filter((tile) => tile.kind === "service").map((tile) => ({
+ groupId: tile.groupId,
+ id: tile.id,
+ })),
+ ).toEqual(
+ dimensionLabDashboardFixture.serviceGroups.flatMap((group) =>
+ group.services
+ .filter((service) => service.datasource?.type === "external")
+ .map((service) => ({
+ groupId: group.id,
+ id: service.id,
+ }))
+ ),
+ );
+ });
+
+ test("applies restored tile snapshots without marking them as loading", () => {
+ const restored = restoreDashboardTileSnapshots(
+ {
+ state: "ready",
+ document: dimensionLabDashboardFixture,
+ schemaVersion: "dashboard.v1",
+ currentRevisionId: "revision-a",
+ },
+ [
+ {
+ ageMs: 60_000,
+ response: {
+ state: "ready",
+ tile: { kind: "telemetry", id: "infra-ram" },
+ item: {
+ ...dimensionLabDashboardFixture.telemetry[0],
+ detail: "cached - stale 60s",
+ severity: "stale",
+ },
+ },
+ },
+ ],
+ );
+
+ expect(restored.restoredItemIds).toEqual(new Set(["telemetry:infra-ram"]));
+ if (restored.dashboard.state !== "ready") {
+ throw new Error("Expected dashboard to be ready");
+ }
+ expect(restored.dashboard.document.telemetry[0]).toMatchObject({
+ id: "infra-ram",
+ detail: "cached - stale 60s",
+ severity: "stale",
+ });
+ });
+
+ test("does not restore aggregate health snapshots over the fresh shell", () => {
+ const restored = restoreDashboardTileSnapshots(
+ {
+ state: "ready",
+ document: dimensionLabDashboardFixture,
+ schemaVersion: "dashboard.v1",
+ currentRevisionId: "revision-a",
+ },
+ [
+ {
+ ageMs: 60_000,
+ response: {
+ state: "ready",
+ tile: { kind: "status", stripId: "footer-status", id: "system-status" },
+ item: {
+ id: "system-status",
+ label: "System Status",
+ value: "20 services down",
+ severity: "stale",
+ },
+ },
+ },
+ {
+ ageMs: 60_000,
+ response: {
+ state: "ready",
+ tile: { kind: "module", id: "runtime-health-summary" },
+ item: {
+ id: "runtime-health-summary",
+ kind: "summary",
+ title: "Runtime Health",
+ value: "20 services down",
+ detail: "0 warnings - 8 services ok - stale 60s",
+ severity: "stale",
+ },
+ },
+ },
+ ],
+ );
+
+ expect(restored.restoredItemIds).toEqual(new Set());
+ if (restored.dashboard.state !== "ready") {
+ throw new Error("Expected dashboard to be ready");
+ }
+ expect(
+ restored.dashboard.document.statusStrips[0].items.find((item) =>
+ item.id === "system-status"
+ ),
+ ).toMatchObject({
+ id: "system-status",
+ value: "Fallback operational",
+ });
+ expect(
+ restored.dashboard.document.modules?.find((module) =>
+ module.id === "runtime-health-summary"
+ ),
+ ).toMatchObject({
+ id: "runtime-health-summary",
+ value: "fallback",
+ });
+ });
+
+ test("uses structured tile match keys for delimiter-bearing ids", () => {
+ expect(
+ dashboardTileMatchKey({ kind: "service", groupId: "a:b", id: "c" }),
+ ).not.toBe(
+ dashboardTileMatchKey({ kind: "service", groupId: "a", id: "b:c" }),
+ );
+ expect(
+ dashboardTileMatchKey({ kind: "status", stripId: "a:b", id: "c" }),
+ ).not.toBe(
+ dashboardTileMatchKey({ kind: "status", stripId: "a", id: "b:c" }),
+ );
+ });
});
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 0099173..403bef9 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -1,5 +1,30 @@
import { useEffect, useState } from "react";
+import type {
+ DashboardDocument,
+ DashboardModule,
+ ServiceEntry,
+ StatusItem,
+ TelemetryCard,
+} from "@dimensionlab/dashboard-model";
import type { DashboardRuntimeState } from "$lib/server/dashboard";
+import {
+ attachDashboardRefreshLifecycle,
+ collectVisibleDashboardModelIds,
+ createDashboardPerformanceMarks,
+ createDashboardRefreshDelay,
+ createDashboardTileSnapshotStore,
+ createDashboardTileBackoff,
+ createDashboardRequestAborter,
+ isPersistableDashboardTileSnapshot,
+ runViewportAwareDashboardHydrationQueue,
+ shouldPauseDashboardRefresh,
+ subscribeToDashboardTileEvents,
+ waitForDashboardHydrationIdle,
+ type DashboardIntersectionObserverFactory,
+ type DashboardTileReference,
+ type DashboardTileSnapshotStoreContext,
+ type RestoredDashboardTileSnapshot,
+} from "$lib/client/dashboard-refresh";
import { dashboardDocumentToUiDashboard } from "$lib/ui-adapter/model-renderer";
import {
DashboardFrame,
@@ -9,21 +34,46 @@ import {
resolveInitialUiTheme,
type UiTheme,
type UiSeverity,
+ type UiDashboardPreview,
} from "@dimensionlab/ui";
-const loadingDashboardState: DashboardRuntimeState = {
- state: "loading",
- title: "Loading Dashboard",
- subtitle: "Fetching active model",
- message: "Waiting for the active dashboard document.",
+type DashboardTileResponse =
+ | {
+ state: "ready";
+ tile: DashboardTileReference;
+ item: DashboardModule | ServiceEntry | StatusItem | TelemetryCard;
+ }
+ | {
+ state: "not_found";
+ tile: DashboardTileReference;
+ message: string;
+ }
+ | {
+ state: "disabled";
+ tile: DashboardTileReference;
+ message: string;
+ };
+
+type DashboardTilesBatchResponse = {
+ state: "ready";
+ tiles: DashboardTileResponse[];
};
+type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
+
+const dashboardTileHydrationConcurrency = 6;
+const dashboardFallbackRefreshIntervalMs = 30_000;
+const dashboardViewportObservationTimeoutMs = 80;
+type DashboardTileSnapshotStore = ReturnType;
+
export function AppStateView({
dashboard,
onThemeChange,
theme,
+ hydratingItemIds,
}: {
dashboard: DashboardRuntimeState;
+ hydratingItemIds?: ReadonlySet;
onThemeChange?: (theme: UiTheme) => void;
theme?: UiTheme;
}) {
@@ -33,9 +83,14 @@ export function AppStateView({
) : null;
if (dashboard.state === "ready") {
+ const uiDashboard = markHydratingItems(
+ dashboardDocumentToUiDashboard(dashboard.document),
+ hydratingItemIds,
+ );
+
return (
);
@@ -89,7 +144,10 @@ export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
export default function App() {
const [dashboard, setDashboard] =
- useState(loadingDashboardState);
+ useState(null);
+ const [hydratingItemIds, setHydratingItemIds] = useState>(
+ () => new Set(),
+ );
const [theme, setTheme] = useState(() => {
if (typeof window === "undefined") return "dark";
@@ -97,6 +155,8 @@ export default function App() {
});
useEffect(() => {
+ if (!dashboard) return;
+
const metadata = resolveDocumentMetadata(dashboard);
document.title = metadata.title;
@@ -119,47 +179,404 @@ export default function App() {
useEffect(() => {
let cancelled = false;
let refreshTimer: number | undefined;
+ let unsubscribeTileEvents: (() => void) | undefined;
+ let lastRefreshIntervalMs = dashboardFallbackRefreshIntervalMs;
+ let hydrationRun = 0;
+ const requestAborter = createDashboardRequestAborter();
+ const performanceMarks = createDashboardPerformanceMarks();
+ const refreshDelay = createDashboardRefreshDelay();
+ const tileBackoff = createDashboardTileBackoff();
+ const tileSnapshotStore = createDashboardTileSnapshotStore(
+ getTileSnapshotStorage(),
+ );
- async function loadDashboard() {
- const response = await fetch("/api/dashboard");
- const nextDashboard = (await response.json()) as DashboardRuntimeState;
+ function refreshPaused() {
+ return shouldPauseDashboardRefresh({
+ visibilityState: document.visibilityState,
+ online: navigator.onLine,
+ });
+ }
- if (cancelled) return;
- setDashboard(nextDashboard);
-
- if (refreshTimer) {
- window.clearInterval(refreshTimer);
+ function clearRefreshTimer() {
+ if (refreshTimer !== undefined) {
+ window.clearTimeout(refreshTimer);
refreshTimer = undefined;
}
+ }
- const refreshIntervalSeconds =
- nextDashboard.state === "ready"
- ? nextDashboard.document.metadata.refreshIntervalSeconds
- : undefined;
+ function scheduleNextRefresh(baseDelayMs: number) {
+ clearRefreshTimer();
+ if (cancelled || refreshPaused()) return;
- if (refreshIntervalSeconds) {
- refreshTimer = window.setInterval(
- () => void loadDashboard(),
- refreshIntervalSeconds * 1000,
+ refreshTimer = window.setTimeout(() => {
+ refreshTimer = undefined;
+ void loadDashboard();
+ }, refreshDelay.nextDelayMs(baseDelayMs));
+ }
+
+ function pauseRefreshes() {
+ clearRefreshTimer();
+ unsubscribeTileEvents?.();
+ unsubscribeTileEvents = undefined;
+ requestAborter.abortActiveRequests();
+ setHydratingItemIds(new Set());
+ }
+
+ async function loadDashboard() {
+ if (cancelled || refreshPaused()) return;
+
+ clearRefreshTimer();
+ const shellSignal = requestAborter.beginShellRun();
+
+ try {
+ const response = await fetch("/api/dashboard", { signal: shellSignal });
+ const nextDashboard = (await response.json()) as DashboardRuntimeState;
+
+ if (cancelled || shellSignal.aborted || refreshPaused()) return;
+ refreshDelay.recordSuccess();
+ performanceMarks.markShellLoad();
+ const restored = restoreDashboardTileSnapshots(
+ nextDashboard,
+ nextDashboard.state === "ready"
+ ? tileSnapshotStore.restore({
+ currentRevisionId: nextDashboard.currentRevisionId,
+ schemaVersion: nextDashboard.schemaVersion,
+ })
+ : [],
+ );
+ setDashboard(restored.dashboard);
+
+ const currentRun = ++hydrationRun;
+ if (
+ restored.dashboard.state === "ready" &&
+ restored.dashboard.liveDatasourceHydration?.enabled !== false
+ ) {
+ const tiles = dashboardHydrationTiles(restored.dashboard.document).filter(
+ (tile) => tileBackoff.canAttempt(dashboardTileKey(tile)),
+ );
+ const tileSignal = requestAborter.beginTileRun();
+ setHydratingItemIds(
+ new Set(
+ tiles
+ .map(dashboardTileKey)
+ .filter((key) => !restored.restoredItemIds.has(key)),
+ ),
+ );
+ hydrateDashboardTiles(
+ tiles,
+ currentRun,
+ tileSignal,
+ {
+ currentRevisionId: restored.dashboard.currentRevisionId,
+ schemaVersion: restored.dashboard.schemaVersion,
+ },
+ );
+ subscribeDashboardTileEvents(
+ currentRun,
+ tileSignal,
+ {
+ currentRevisionId: restored.dashboard.currentRevisionId,
+ schemaVersion: restored.dashboard.schemaVersion,
+ },
+ );
+ } else {
+ setHydratingItemIds(new Set());
+ }
+
+ const refreshIntervalSeconds =
+ nextDashboard.state === "ready"
+ ? nextDashboard.document.metadata.refreshIntervalSeconds
+ : undefined;
+
+ if (refreshIntervalSeconds && !refreshPaused()) {
+ lastRefreshIntervalMs = refreshIntervalSeconds * 1000;
+ scheduleNextRefresh(lastRefreshIntervalMs);
+ }
+ } catch (error) {
+ if (!isAbortError(error) && !cancelled) {
+ refreshDelay.recordFailure();
+ console.error("Dashboard refresh failed", error);
+ scheduleNextRefresh(lastRefreshIntervalMs);
+ }
+ }
+ }
+
+ function hydrateDashboardTiles(
+ tiles: DashboardTileReference[],
+ run: number,
+ signal: AbortSignal,
+ snapshotContext: DashboardTileSnapshotStoreContext,
+ ) {
+ const modelIds = tiles.map(dashboardTileModelId);
+
+ void runViewportAwareDashboardHydrationQueue({
+ batchSize: dashboardTileHydrationConcurrency,
+ collectVisibleModelIds: async () => {
+ await waitForDashboardRenderFrame(signal);
+ return collectVisibleDashboardModelIds({
+ clearTimeout: window.clearTimeout.bind(window),
+ createObserver: getDashboardIntersectionObserverFactory(),
+ documentTarget: document,
+ modelIds,
+ setTimeout: window.setTimeout.bind(window),
+ signal,
+ timeoutMs: dashboardViewportObservationTimeoutMs,
+ });
+ },
+ concurrency: dashboardTileHydrationConcurrency,
+ getModelId: dashboardTileModelId,
+ hydrate: async (tile) => {
+ const key = dashboardTileKey(tile);
+ const result = await hydrateDashboardTile(
+ tile,
+ run,
+ signal,
+ snapshotContext,
+ );
+
+ if (result === "ready") {
+ tileBackoff.recordSuccess(key);
+ } else if (result === "failed") {
+ tileBackoff.recordFailure(key);
+ }
+ },
+ hydrateBatch: async (batch) => {
+ const results = await hydrateDashboardTileBatch(
+ batch,
+ run,
+ signal,
+ snapshotContext,
+ );
+
+ for (const { result, tile } of results) {
+ const key = dashboardTileKey(tile);
+ if (result === "ready") {
+ tileBackoff.recordSuccess(key);
+ } else if (result === "failed") {
+ tileBackoff.recordFailure(key);
+ }
+ }
+ },
+ items: tiles,
+ onAllItemsSettled: () => performanceMarks.markAllTilesSettled(),
+ onVisibleItemsSettled: () => performanceMarks.markVisibleTilesReady(),
+ signal,
+ waitForIdle: () =>
+ waitForDashboardHydrationIdle({
+ ...getDashboardIdleCallbacks(),
+ clearTimeout: window.clearTimeout.bind(window),
+ setTimeout: window.setTimeout.bind(window),
+ signal,
+ }),
+ });
+ }
+
+ function subscribeDashboardTileEvents(
+ run: number,
+ signal: AbortSignal,
+ snapshotContext: DashboardTileSnapshotStoreContext,
+ ) {
+ unsubscribeTileEvents?.();
+ unsubscribeTileEvents = subscribeToDashboardTileEvents({
+ onTile: (event) => {
+ const tileResponse = event as DashboardTileResponse;
+ if (!isDashboardTileResponse(tileResponse)) return;
+ applyDashboardTileHydrationResponse(
+ tileResponse.tile,
+ tileResponse,
+ run,
+ signal,
+ snapshotContext,
+ );
+ },
+ onUnavailable: () => {
+ unsubscribeTileEvents = undefined;
+ },
+ });
+ }
+
+ async function hydrateDashboardTileBatch(
+ tiles: DashboardTileReference[],
+ run: number,
+ signal: AbortSignal,
+ snapshotContext: DashboardTileSnapshotStoreContext,
+ ): Promise> {
+ try {
+ const response = await fetch("/api/dashboard/tiles", {
+ body: JSON.stringify({ tiles }),
+ headers: { "Content-Type": "application/json" },
+ method: "POST",
+ signal,
+ });
+ const batchResponse = (await response.json()) as DashboardTilesBatchResponse;
+
+ if (
+ cancelled ||
+ signal.aborted ||
+ run !== hydrationRun ||
+ !response.ok ||
+ batchResponse.state !== "ready"
+ ) {
+ throw new Error("Dashboard tile batch hydration failed");
+ }
+
+ const responsesByKey = new Map(
+ batchResponse.tiles.map((tileResponse) => [
+ dashboardTileMatchKey(tileResponse.tile),
+ tileResponse,
+ ]),
+ );
+
+ return tiles.map((tile) => {
+ const key = dashboardTileKey(tile);
+ try {
+ return {
+ tile,
+ result: applyDashboardTileHydrationResponse(
+ tile,
+ responsesByKey.get(dashboardTileMatchKey(tile)),
+ run,
+ signal,
+ snapshotContext,
+ ),
+ };
+ } finally {
+ finishDashboardTileHydration(key, run, signal);
+ }
+ });
+ } catch (error) {
+ if (signal.aborted || cancelled || run !== hydrationRun) {
+ return tiles.map((tile) => ({ result: "aborted", tile }));
+ }
+
+ return Promise.all(
+ tiles.map(async (tile) => ({
+ tile,
+ result: await hydrateDashboardTile(tile, run, signal, snapshotContext),
+ })),
);
}
}
+ async function hydrateDashboardTile(
+ tile: DashboardTileReference,
+ run: number,
+ signal: AbortSignal,
+ snapshotContext: DashboardTileSnapshotStoreContext,
+ ): Promise {
+ const key = dashboardTileKey(tile);
+
+ try {
+ const response = await fetch(dashboardTileUrl(tile), { signal });
+ const tileResponse = (await response.json()) as DashboardTileResponse;
+
+ if (!response.ok) {
+ return "failed";
+ }
+
+ const result = applyDashboardTileHydrationResponse(
+ tile,
+ tileResponse,
+ run,
+ signal,
+ snapshotContext,
+ );
+ if (result !== "ready") {
+ return signal.aborted || cancelled || run !== hydrationRun
+ ? "aborted"
+ : "failed";
+ }
+
+ return "ready";
+ } catch (error) {
+ if (!isAbortError(error) && !cancelled) {
+ console.error("Dashboard tile hydration failed", error);
+ return "failed";
+ }
+ return "aborted";
+ } finally {
+ finishDashboardTileHydration(key, run, signal);
+ }
+ }
+
+ function applyDashboardTileHydrationResponse(
+ tile: DashboardTileReference,
+ tileResponse: DashboardTileResponse | undefined,
+ run: number,
+ signal: AbortSignal,
+ snapshotContext: DashboardTileSnapshotStoreContext,
+ ): DashboardTileHydrationResult {
+ if (
+ cancelled ||
+ signal.aborted ||
+ run !== hydrationRun ||
+ !tileResponse ||
+ dashboardTileMatchKey(tileResponse.tile) !== dashboardTileMatchKey(tile) ||
+ tileResponse.state !== "ready"
+ ) {
+ return signal.aborted || cancelled || run !== hydrationRun
+ ? "aborted"
+ : "failed";
+ }
+
+ const readyTileResponse = tileResponse;
+ setDashboard((current) =>
+ current?.state === "ready"
+ ? {
+ ...current,
+ document: applyDashboardTile(current.document, readyTileResponse),
+ }
+ : current,
+ );
+ tileSnapshotStore.saveReadyTile({
+ ...snapshotContext,
+ response: readyTileResponse,
+ });
+ performanceMarks.markFirstTileReady();
+ return "ready";
+ }
+
+ function finishDashboardTileHydration(
+ key: string,
+ run: number,
+ signal: AbortSignal,
+ ) {
+ if (!cancelled && !signal.aborted && run === hydrationRun) {
+ setHydratingItemIds((current) => {
+ const next = new Set(current);
+ next.delete(key);
+ return next;
+ });
+ }
+ }
+
+ const detachRefreshLifecycle = attachDashboardRefreshLifecycle({
+ documentTarget: document,
+ windowTarget: window,
+ loadDashboard,
+ pauseRefreshes,
+ refreshPaused: () => cancelled || refreshPaused(),
+ });
+
void loadDashboard();
return () => {
cancelled = true;
- if (refreshTimer) window.clearInterval(refreshTimer);
+ clearRefreshTimer();
+ unsubscribeTileEvents?.();
+ requestAborter.abortActiveRequests();
+ detachRefreshLifecycle();
};
}, []);
- return (
+ return dashboard ? (
- );
+ ) : null;
}
function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
@@ -181,3 +598,278 @@ function getThemeStorage(): Storage | undefined {
return undefined;
}
}
+
+function getTileSnapshotStorage(): Storage | undefined {
+ try {
+ return window.localStorage;
+ } catch {
+ return undefined;
+ }
+}
+
+type DashboardIdleWindow = Window & {
+ cancelIdleCallback?: (handle: number) => void;
+ requestIdleCallback?: (
+ callback: () => void,
+ options?: { timeout?: number },
+ ) => number;
+};
+
+function getDashboardIdleCallbacks() {
+ const idleWindow = window as DashboardIdleWindow;
+ return {
+ cancelIdleCallback: idleWindow.cancelIdleCallback?.bind(idleWindow),
+ requestIdleCallback: idleWindow.requestIdleCallback?.bind(idleWindow),
+ };
+}
+
+function getDashboardIntersectionObserverFactory():
+ | DashboardIntersectionObserverFactory
+ | undefined {
+ if (typeof window.IntersectionObserver === "undefined") return undefined;
+
+ return (callback) =>
+ new window.IntersectionObserver((entries) => {
+ callback(entries);
+ });
+}
+
+async function waitForDashboardRenderFrame(signal: AbortSignal): Promise {
+ if (signal.aborted) return;
+
+ await new Promise((resolve) => {
+ let settled = false;
+ let frame: number | undefined;
+
+ function finish() {
+ if (settled) return;
+ settled = true;
+ if (frame !== undefined) window.cancelAnimationFrame(frame);
+ signal.removeEventListener("abort", finish);
+ resolve();
+ }
+
+ signal.addEventListener("abort", finish, { once: true });
+ frame = window.requestAnimationFrame(finish);
+ });
+}
+
+function isAbortError(error: unknown): boolean {
+ return (
+ typeof error === "object" &&
+ error !== null &&
+ "name" in error &&
+ error.name === "AbortError"
+ );
+}
+
+function isDashboardTileResponse(value: unknown): value is DashboardTileResponse {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ "state" in value &&
+ (value.state === "ready" ||
+ value.state === "not_found" ||
+ value.state === "disabled") &&
+ "tile" in value &&
+ typeof value.tile === "object" &&
+ value.tile !== null
+ );
+}
+
+export function restoreDashboardTileSnapshots(
+ dashboard: DashboardRuntimeState,
+ snapshots: RestoredDashboardTileSnapshot[],
+): {
+ dashboard: DashboardRuntimeState;
+ restoredItemIds: Set;
+} {
+ if (dashboard.state !== "ready" || snapshots.length === 0) {
+ return {
+ dashboard,
+ restoredItemIds: new Set(),
+ };
+ }
+
+ return snapshots.filter((snapshot) =>
+ isPersistableDashboardTileSnapshot(snapshot.response.tile)
+ ).reduce(
+ (current, snapshot) => ({
+ dashboard: {
+ ...current.dashboard,
+ document: applyDashboardTile(
+ current.dashboard.document,
+ snapshot.response as Extract,
+ ),
+ },
+ restoredItemIds: new Set([
+ ...current.restoredItemIds,
+ dashboardTileKey(snapshot.response.tile),
+ ]),
+ }),
+ {
+ dashboard,
+ restoredItemIds: new Set(),
+ },
+ );
+}
+
+function markHydratingItems(
+ dashboard: UiDashboardPreview,
+ hydratingItemIds?: ReadonlySet,
+): UiDashboardPreview {
+ if (!hydratingItemIds?.size) return dashboard;
+
+ return {
+ ...dashboard,
+ telemetry: dashboard.telemetry.map((card) =>
+ hydratingItemIds.has(`telemetry:${card.id}`)
+ ? {
+ ...card,
+ severity: "loading",
+ detail: "loading live telemetry",
+ }
+ : card,
+ ),
+ serviceGroups: dashboard.serviceGroups.map((group) => ({
+ ...group,
+ services: group.services.map((service) =>
+ hydratingItemIds.has(`service:${group.id}:${service.id}`)
+ ? {
+ ...service,
+ severity: "loading",
+ detail: "loading",
+ }
+ : service,
+ ),
+ })),
+ modules: dashboard.modules.map((module) =>
+ hydratingItemIds.has(`module:${module.id}`)
+ ? {
+ ...module,
+ severity: "loading",
+ detail: "loading live data",
+ }
+ : module,
+ ),
+ statusItems: dashboard.statusItems.map((item) =>
+ hydratingItemIds.has(`status:${item.id}`)
+ ? {
+ ...item,
+ severity: "loading",
+ value: "loading",
+ }
+ : item,
+ ),
+ };
+}
+
+export function dashboardHydrationTiles(
+ document: DashboardDocument,
+): DashboardTileReference[] {
+ const telemetry = document.telemetry
+ .filter((card) => card.datasource?.type === "external")
+ .map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
+ const services = document.serviceGroups.flatMap((group) =>
+ group.services
+ .filter((service) => service.datasource?.type === "external")
+ .map((service): DashboardTileReference => ({
+ kind: "service",
+ groupId: group.id,
+ id: service.id,
+ })),
+ );
+ const modules = (document.modules || [])
+ .filter((module) =>
+ module.datasource?.type === "external" ||
+ module.id === "runtime-health-summary"
+ )
+ .map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
+ const status = document.statusStrips.flatMap((strip) =>
+ strip.items
+ .map((item): DashboardTileReference => ({
+ kind: "status",
+ stripId: strip.id,
+ id: item.id,
+ })),
+ );
+
+ return [...status, ...telemetry, ...modules, ...services];
+}
+
+function dashboardTileKey(tile: DashboardTileReference): string {
+ if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
+ if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
+ return `${tile.kind}:${tile.id}`;
+}
+
+export function dashboardTileMatchKey(tile: DashboardTileReference): string {
+ return JSON.stringify(tile);
+}
+
+function dashboardTileModelId(tile: DashboardTileReference): string {
+ if (tile.kind === "status") return `${tile.stripId}:${tile.id}`;
+ return tile.id;
+}
+
+function dashboardTileUrl(tile: DashboardTileReference): string {
+ const parts = tile.kind === "status"
+ ? ["api", "dashboard", "tile", tile.kind, tile.stripId, tile.id]
+ : tile.kind === "service"
+ ? ["api", "dashboard", "tile", tile.kind, tile.groupId, tile.id]
+ : ["api", "dashboard", "tile", tile.kind, tile.id];
+ return `/${parts.map(encodeURIComponent).join("/")}`;
+}
+
+function applyDashboardTile(
+ document: DashboardDocument,
+ response: Extract,
+): DashboardDocument {
+ if (response.tile.kind === "telemetry") {
+ return {
+ ...document,
+ telemetry: document.telemetry.map((card) =>
+ card.id === response.tile.id ? response.item as TelemetryCard : card,
+ ),
+ };
+ }
+
+ if (response.tile.kind === "service") {
+ const tile = response.tile;
+ return {
+ ...document,
+ serviceGroups: document.serviceGroups.map((group) => ({
+ ...group,
+ services: group.id === tile.groupId
+ ? group.services.map((service) =>
+ service.id === tile.id ? response.item as ServiceEntry : service,
+ )
+ : group.services,
+ })),
+ };
+ }
+
+ if (response.tile.kind === "module") {
+ return {
+ ...document,
+ modules: (document.modules || []).map((module) =>
+ module.id === response.tile.id ? response.item as DashboardModule : module,
+ ),
+ };
+ }
+
+ const tile = response.tile;
+ return {
+ ...document,
+ statusStrips: document.statusStrips.map((strip) =>
+ strip.id === tile.stripId
+ ? {
+ ...strip,
+ items: strip.items.map((item) =>
+ item.id === tile.id ? response.item as StatusItem : item,
+ ),
+ }
+ : strip,
+ ),
+ };
+}
diff --git a/apps/web/src/lib/client/dashboard-refresh.test.ts b/apps/web/src/lib/client/dashboard-refresh.test.ts
new file mode 100644
index 0000000..77ea795
--- /dev/null
+++ b/apps/web/src/lib/client/dashboard-refresh.test.ts
@@ -0,0 +1,633 @@
+import { describe, expect, test } from "vitest";
+import {
+ attachDashboardRefreshLifecycle,
+ collectVisibleDashboardModelIds,
+ createDashboardPerformanceMarks,
+ createDashboardRefreshDelay,
+ dashboardPerformanceMarks,
+ createDashboardTileSnapshotStore,
+ createDashboardTileBackoff,
+ createDashboardRequestAborter,
+ runDashboardHydrationQueue,
+ runDashboardHydrationBatchQueue,
+ runViewportAwareDashboardHydrationQueue,
+ shouldPauseDashboardRefresh,
+ splitDashboardHydrationItemsByVisibility,
+ subscribeToDashboardTileEvents,
+ waitForDashboardHydrationIdle,
+ type DashboardIntersectionEntry,
+} from "./dashboard-refresh";
+
+describe("dashboard refresh lifecycle", () => {
+ test("pauses refreshes when the document is hidden or the browser is offline", () => {
+ expect(
+ shouldPauseDashboardRefresh({ visibilityState: "visible", online: true }),
+ ).toBe(false);
+ expect(
+ shouldPauseDashboardRefresh({ visibilityState: "hidden", online: true }),
+ ).toBe(true);
+ expect(
+ shouldPauseDashboardRefresh({ visibilityState: "visible", online: false }),
+ ).toBe(true);
+ });
+
+ test("aborts shell and tile requests when a new shell run starts", () => {
+ const aborter = createDashboardRequestAborter();
+ const shellSignal = aborter.beginShellRun();
+ const tileSignal = aborter.beginTileRun();
+
+ const nextShellSignal = aborter.beginShellRun();
+
+ expect(shellSignal.aborted).toBe(true);
+ expect(tileSignal.aborted).toBe(true);
+ expect(nextShellSignal.aborted).toBe(false);
+ });
+
+ test("aborts active requests when refreshes are paused", () => {
+ const aborter = createDashboardRequestAborter();
+ const shellSignal = aborter.beginShellRun();
+ const tileSignal = aborter.beginTileRun();
+
+ aborter.abortActiveRequests();
+
+ expect(shellSignal.aborted).toBe(true);
+ expect(tileSignal.aborted).toBe(true);
+ });
+
+ test("pauses on hidden, offline, and pagehide events then resumes immediately when visible", () => {
+ const documentTarget = new EventTarget();
+ const windowTarget = new EventTarget();
+ const calls: string[] = [];
+ let paused = true;
+
+ const detach = attachDashboardRefreshLifecycle({
+ documentTarget,
+ windowTarget,
+ loadDashboard: () => {
+ calls.push("load");
+ },
+ pauseRefreshes: () => {
+ calls.push("pause");
+ },
+ refreshPaused: () => paused,
+ });
+
+ documentTarget.dispatchEvent(new Event("visibilitychange"));
+ windowTarget.dispatchEvent(new Event("offline"));
+ windowTarget.dispatchEvent(new Event("pagehide"));
+
+ paused = false;
+ documentTarget.dispatchEvent(new Event("visibilitychange"));
+ windowTarget.dispatchEvent(new Event("online"));
+
+ detach();
+ documentTarget.dispatchEvent(new Event("visibilitychange"));
+ windowTarget.dispatchEvent(new Event("online"));
+
+ expect(calls).toEqual(["pause", "pause", "pause", "load", "load"]);
+ });
+
+ test("limits tile hydration concurrency", async () => {
+ let active = 0;
+ let maxActive = 0;
+ const started: number[] = [];
+ const releases = new Map void>();
+
+ const queue = runDashboardHydrationQueue({
+ concurrency: 2,
+ items: [1, 2, 3, 4],
+ signal: new AbortController().signal,
+ hydrate: async (item) => {
+ active += 1;
+ maxActive = Math.max(maxActive, active);
+ started.push(item);
+ await new Promise((resolve) => releases.set(item, resolve));
+ active -= 1;
+ },
+ });
+
+ await waitFor(() => started.length === 2);
+ expect(started).toEqual([1, 2]);
+ expect(maxActive).toBe(2);
+
+ releases.get(1)?.();
+ await waitFor(() => started.length === 3);
+ expect(started).toEqual([1, 2, 3]);
+ expect(maxActive).toBe(2);
+
+ releases.get(2)?.();
+ releases.get(3)?.();
+ await waitFor(() => started.length === 4);
+ releases.get(4)?.();
+ await queue;
+ expect(maxActive).toBe(2);
+ });
+
+ test("hydrates queued work in fixed-size batches", async () => {
+ const batches: number[][] = [];
+
+ await runDashboardHydrationBatchQueue({
+ batchSize: 3,
+ hydrateBatch: (items) => {
+ batches.push(items);
+ },
+ items: [1, 2, 3, 4, 5, 6, 7],
+ signal: new AbortController().signal,
+ });
+
+ expect(batches).toEqual([
+ [1, 2, 3],
+ [4, 5, 6],
+ [7],
+ ]);
+ });
+
+ test("hydrates visible items before deferred items and waits for idle", async () => {
+ const calls: string[] = [];
+ const idleReleases: Array<() => void> = [];
+
+ const hydration = runViewportAwareDashboardHydrationQueue({
+ collectVisibleModelIds: async () => new Set(["b"]),
+ concurrency: 1,
+ getModelId: (item) => item,
+ hydrate: async (item) => {
+ calls.push(item);
+ },
+ items: ["a", "b", "c"],
+ onAllItemsSettled: () => calls.push("all-settled"),
+ onVisibleItemsSettled: () => calls.push("visible-settled"),
+ signal: new AbortController().signal,
+ waitForIdle: async () => {
+ calls.push("idle");
+ await new Promise((resolve) => idleReleases.push(resolve));
+ },
+ });
+
+ await waitFor(() => calls.includes("idle"));
+ expect(calls).toEqual(["b", "visible-settled", "idle"]);
+
+ idleReleases.shift()?.();
+ await hydration;
+ expect(calls).toEqual([
+ "b",
+ "visible-settled",
+ "idle",
+ "a",
+ "c",
+ "all-settled",
+ ]);
+ });
+
+ test("splits visible hydration items while preserving document order", () => {
+ expect(splitDashboardHydrationItemsByVisibility({
+ getModelId: (item) => item.id,
+ items: [{ id: "status" }, { id: "telemetry" }, { id: "service" }],
+ visibleModelIds: new Set(["service", "status"]),
+ })).toEqual({
+ visible: [{ id: "status" }, { id: "service" }],
+ deferred: [{ id: "telemetry" }],
+ });
+ });
+
+ test("collects visible data-model-id elements with IntersectionObserver", async () => {
+ const elements = [
+ modelElement("status"),
+ modelElement("telemetry"),
+ modelElement("unrelated"),
+ ];
+ let callback:
+ | ((entries: DashboardIntersectionEntry[]) => void)
+ | undefined;
+ let finishObservation: (() => void) | undefined;
+ let disconnected = false;
+ const observed: string[] = [];
+
+ const visible = collectVisibleDashboardModelIds({
+ createObserver: (observerCallback) => {
+ callback = observerCallback;
+ return {
+ disconnect() {
+ disconnected = true;
+ },
+ observe(element) {
+ observed.push(element.getAttribute("data-model-id") || "");
+ },
+ };
+ },
+ documentTarget: {
+ querySelectorAll: () => elements,
+ },
+ modelIds: ["status", "telemetry"],
+ setTimeout: (handler) => {
+ finishObservation = handler;
+ return 1 as unknown as ReturnType;
+ },
+ clearTimeout: () => undefined,
+ signal: new AbortController().signal,
+ });
+
+ callback?.([
+ {
+ isIntersecting: false,
+ intersectionRatio: 0,
+ target: elements[0],
+ },
+ {
+ isIntersecting: true,
+ target: elements[1],
+ },
+ ]);
+ finishObservation?.();
+
+ expect(await visible).toEqual(new Set(["telemetry"]));
+ expect(observed).toEqual(["status", "telemetry"]);
+ expect(disconnected).toBe(true);
+ });
+
+ test("waits for requestIdleCallback when available", async () => {
+ let idleCallback: (() => void) | undefined;
+ let cancelledIdle: number | undefined;
+ const wait = waitForDashboardHydrationIdle({
+ cancelIdleCallback: (handle) => {
+ cancelledIdle = handle;
+ },
+ requestIdleCallback: (callback) => {
+ idleCallback = callback;
+ return 7;
+ },
+ signal: new AbortController().signal,
+ });
+
+ expect(cancelledIdle).toBeUndefined();
+ idleCallback?.();
+ await wait;
+ expect(cancelledIdle).toBe(7);
+ });
+
+ test("jitters refresh delays and slows repeated failures", () => {
+ const delay = createDashboardRefreshDelay({
+ jitterRatio: 0.1,
+ random: () => 1,
+ });
+
+ expect(delay.nextDelayMs(1_000)).toBe(1_100);
+ delay.recordFailure();
+ expect(delay.nextDelayMs(1_000)).toBe(2_200);
+ delay.recordFailure();
+ expect(delay.nextDelayMs(1_000)).toBe(4_400);
+ delay.recordSuccess();
+ expect(delay.nextDelayMs(1_000)).toBe(1_100);
+ });
+
+ test("marks dashboard performance milestones once per shell run", () => {
+ const marks: string[] = [];
+ const performanceMarks = createDashboardPerformanceMarks({
+ mark: (name) => marks.push(name),
+ });
+
+ performanceMarks.markShellLoad();
+ performanceMarks.markFirstTileReady();
+ performanceMarks.markFirstTileReady();
+ performanceMarks.markVisibleTilesReady();
+ performanceMarks.markAllTilesSettled();
+ performanceMarks.markShellLoad();
+ performanceMarks.markFirstTileReady();
+
+ expect(marks).toEqual([
+ dashboardPerformanceMarks.shellLoad,
+ dashboardPerformanceMarks.firstTileReady,
+ dashboardPerformanceMarks.visibleTilesReady,
+ dashboardPerformanceMarks.allTilesSettled,
+ dashboardPerformanceMarks.shellLoad,
+ dashboardPerformanceMarks.firstTileReady,
+ ]);
+ });
+
+ test("subscribes to dashboard tile events", () => {
+ const received: unknown[] = [];
+ let listener: ((event: MessageEvent) => void) | undefined;
+ let closed = false;
+
+ const unsubscribe = subscribeToDashboardTileEvents({
+ createEventSource: (url) => {
+ expect(url).toBe("/api/dashboard/events");
+ return {
+ addEventListener(_type, eventListener) {
+ listener = eventListener;
+ },
+ close() {
+ closed = true;
+ },
+ onerror: null,
+ };
+ },
+ onTile: (tile) => received.push(tile),
+ });
+
+ listener?.({ data: JSON.stringify({ state: "ready" }) } as MessageEvent);
+ expect(received).toEqual([{ state: "ready" }]);
+ unsubscribe();
+ expect(closed).toBe(true);
+ });
+
+ test("falls back when dashboard tile events are unavailable or fail", () => {
+ let unavailableCount = 0;
+
+ subscribeToDashboardTileEvents({
+ createEventSource: undefined,
+ onTile: () => undefined,
+ onUnavailable: () => {
+ unavailableCount += 1;
+ },
+ });
+
+ let errorHandler: (() => void) | null = null;
+ const unsubscribe = subscribeToDashboardTileEvents({
+ createEventSource: () => ({
+ addEventListener: () => undefined,
+ close: () => undefined,
+ get onerror() {
+ return errorHandler;
+ },
+ set onerror(handler) {
+ errorHandler = handler;
+ },
+ }),
+ onTile: () => undefined,
+ onUnavailable: () => {
+ unavailableCount += 1;
+ },
+ });
+
+ if (!errorHandler) throw new Error("expected error handler");
+ const triggerError = errorHandler as unknown as () => void;
+ triggerError();
+ unsubscribe();
+ expect(unavailableCount).toBe(2);
+ });
+
+ test("backs off failed tile keys and resets after success", () => {
+ const backoff = createDashboardTileBackoff();
+
+ backoff.recordFailure("telemetry:infra-ram", 1_000);
+ expect(backoff.canAttempt("telemetry:infra-ram", 15_999)).toBe(false);
+ expect(backoff.canAttempt("telemetry:infra-ram", 16_000)).toBe(true);
+
+ backoff.recordFailure("telemetry:infra-ram", 16_000);
+ expect(backoff.canAttempt("telemetry:infra-ram", 45_999)).toBe(false);
+ expect(backoff.canAttempt("telemetry:infra-ram", 46_000)).toBe(true);
+
+ backoff.recordFailure("telemetry:infra-ram", 46_000);
+ backoff.recordFailure("telemetry:infra-ram", 106_000);
+ expect(backoff.canAttempt("telemetry:infra-ram", 225_999)).toBe(false);
+ expect(backoff.canAttempt("telemetry:infra-ram", 226_000)).toBe(true);
+
+ backoff.recordSuccess("telemetry:infra-ram");
+ expect(backoff.canAttempt("telemetry:infra-ram", 107_000)).toBe(true);
+ });
+
+ test("stores only ready tile snapshots and restores them for matching revisions", () => {
+ const storage = createMemoryStorage();
+ let now = 1_000;
+ const store = createDashboardTileSnapshotStore(storage, {
+ now: () => now,
+ });
+
+ store.saveReadyTile({
+ currentRevisionId: "revision-a",
+ response: {
+ state: "ready",
+ tile: { kind: "telemetry", id: "infra-ram" },
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live",
+ severity: "ok",
+ },
+ },
+ schemaVersion: "dashboard.v1",
+ });
+ store.saveTile({
+ currentRevisionId: "revision-a",
+ response: {
+ state: "not_found",
+ tile: { kind: "telemetry", id: "missing" },
+ message: "Missing",
+ },
+ schemaVersion: "dashboard.v1",
+ });
+ store.saveTile({
+ currentRevisionId: "revision-a",
+ response: {
+ state: "disabled",
+ tile: { kind: "module", id: "disabled-module" },
+ message: "Disabled",
+ },
+ schemaVersion: "dashboard.v1",
+ });
+
+ now = 61_000;
+
+ const restored = store.restore({
+ currentRevisionId: "revision-a",
+ schemaVersion: "dashboard.v1",
+ });
+
+ expect(restored).toEqual([
+ {
+ ageMs: 60_000,
+ response: {
+ state: "ready",
+ tile: { kind: "telemetry", id: "infra-ram" },
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live - stale 60s",
+ severity: "stale",
+ },
+ },
+ },
+ ]);
+ expect(store.restore({
+ currentRevisionId: "revision-b",
+ schemaVersion: "dashboard.v1",
+ })).toEqual([]);
+ });
+
+ test("ignores malformed stored tile snapshots", () => {
+ const storage = createMemoryStorage();
+ storage.setItem(
+ "dimensionlab.dashboard.tiles.v1",
+ JSON.stringify({
+ currentRevisionId: "revision-a",
+ schemaVersion: "dashboard.v1",
+ tiles: [
+ {},
+ {
+ item: {
+ id: "incomplete",
+ detail: "live",
+ },
+ savedAt: 1_000,
+ tile: { kind: "telemetry", id: "incomplete" },
+ },
+ {
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live",
+ severity: "ok",
+ },
+ savedAt: 1_000,
+ tile: { kind: "telemetry", id: "infra-ram" },
+ },
+ {
+ item: { id: "broken-detail", detail: 42 },
+ savedAt: 1_000,
+ tile: { kind: "telemetry", id: "broken-detail" },
+ },
+ ],
+ version: 1,
+ }),
+ );
+
+ const store = createDashboardTileSnapshotStore(storage, {
+ now: () => 16_000,
+ });
+
+ expect(store.restore({
+ currentRevisionId: "revision-a",
+ schemaVersion: "dashboard.v1",
+ })).toEqual([
+ {
+ ageMs: 15_000,
+ response: {
+ state: "ready",
+ tile: { kind: "telemetry", id: "infra-ram" },
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live - stale 15s",
+ severity: "stale",
+ },
+ },
+ },
+ ]);
+ });
+
+ test("ignores restored aggregate health snapshots", () => {
+ const storage = createMemoryStorage();
+ storage.setItem(
+ "dimensionlab.dashboard.tiles.v1",
+ JSON.stringify({
+ currentRevisionId: "revision-a",
+ schemaVersion: "dashboard.v1",
+ tiles: [
+ {
+ item: {
+ id: "system-status",
+ label: "System Status",
+ value: "20 services down",
+ severity: "danger",
+ },
+ savedAt: 1_000,
+ tile: { kind: "status", stripId: "footer-status", id: "system-status" },
+ },
+ {
+ item: {
+ id: "runtime-health-summary",
+ kind: "summary",
+ title: "Runtime Health",
+ value: "20 services down",
+ detail: "0 warnings - 8 services ok",
+ severity: "danger",
+ },
+ savedAt: 1_000,
+ tile: { kind: "module", id: "runtime-health-summary" },
+ },
+ {
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live",
+ severity: "ok",
+ },
+ savedAt: 1_000,
+ tile: { kind: "telemetry", id: "infra-ram" },
+ },
+ ],
+ version: 1,
+ }),
+ );
+
+ const store = createDashboardTileSnapshotStore(storage, {
+ now: () => 16_000,
+ });
+
+ expect(store.restore({
+ currentRevisionId: "revision-a",
+ schemaVersion: "dashboard.v1",
+ })).toEqual([
+ {
+ ageMs: 15_000,
+ response: {
+ state: "ready",
+ tile: { kind: "telemetry", id: "infra-ram" },
+ item: {
+ id: "infra-ram",
+ label: "Infra RAM",
+ value: { kind: "percent", value: 42 },
+ detail: "live - stale 15s",
+ severity: "stale",
+ },
+ },
+ },
+ ]);
+ });
+});
+
+async function waitFor(predicate: () => boolean) {
+ for (let attempt = 0; attempt < 20; attempt += 1) {
+ if (predicate()) return;
+ await Promise.resolve();
+ }
+
+ throw new Error("condition was not met");
+}
+
+function createMemoryStorage(): Storage {
+ const values = new Map();
+ return {
+ get length() {
+ return values.size;
+ },
+ clear() {
+ values.clear();
+ },
+ getItem(key) {
+ return values.get(key) ?? null;
+ },
+ key(index) {
+ return [...values.keys()][index] ?? null;
+ },
+ removeItem(key) {
+ values.delete(key);
+ },
+ setItem(key, value) {
+ values.set(key, value);
+ },
+ };
+}
+
+function modelElement(modelId: string) {
+ return {
+ getAttribute(name: string) {
+ return name === "data-model-id" ? modelId : null;
+ },
+ };
+}
diff --git a/apps/web/src/lib/client/dashboard-refresh.ts b/apps/web/src/lib/client/dashboard-refresh.ts
new file mode 100644
index 0000000..7c50359
--- /dev/null
+++ b/apps/web/src/lib/client/dashboard-refresh.ts
@@ -0,0 +1,886 @@
+export interface DashboardRefreshPauseState {
+ visibilityState: DocumentVisibilityState;
+ online: boolean;
+}
+
+export type DashboardTileReference =
+ | { kind: "telemetry"; id: string }
+ | { kind: "service"; groupId: string; id: string }
+ | { kind: "module"; id: string }
+ | { kind: "status"; stripId: string; id: string };
+
+type SnapshotMetricValue = Record;
+
+export type DashboardTileSnapshotItem = {
+ detail?: string;
+ id: string;
+ label?: string;
+ severity?: string;
+ value?: SnapshotMetricValue | string;
+} & Record;
+
+export type DashboardTileSnapshotResponse =
+ | {
+ state: "ready";
+ tile: DashboardTileReference;
+ item: DashboardTileSnapshotItem;
+ }
+ | {
+ state: "not_found";
+ tile: DashboardTileReference;
+ message: string;
+ }
+ | {
+ state: "disabled";
+ tile: DashboardTileReference;
+ message: string;
+ };
+
+export function shouldPauseDashboardRefresh(
+ state: DashboardRefreshPauseState,
+): boolean {
+ return state.visibilityState !== "visible" || !state.online;
+}
+
+export function createDashboardRequestAborter() {
+ let shellController: AbortController | undefined;
+ let tileController: AbortController | undefined;
+
+ function abortController(controller: AbortController | undefined) {
+ if (controller && !controller.signal.aborted) {
+ controller.abort();
+ }
+ }
+
+ return {
+ beginShellRun(): AbortSignal {
+ abortController(shellController);
+ abortController(tileController);
+ shellController = new AbortController();
+ tileController = undefined;
+ return shellController.signal;
+ },
+ beginTileRun(): AbortSignal {
+ abortController(tileController);
+ tileController = new AbortController();
+ return tileController.signal;
+ },
+ abortActiveRequests(): void {
+ abortController(shellController);
+ abortController(tileController);
+ shellController = undefined;
+ tileController = undefined;
+ },
+ };
+}
+
+type DashboardRefreshEventTarget = Pick<
+ EventTarget,
+ "addEventListener" | "removeEventListener"
+>;
+
+export interface DashboardRefreshLifecycleOptions {
+ documentTarget: DashboardRefreshEventTarget;
+ windowTarget: DashboardRefreshEventTarget;
+ loadDashboard: () => void | Promise;
+ pauseRefreshes: () => void;
+ refreshPaused: () => boolean;
+}
+
+export function attachDashboardRefreshLifecycle(
+ options: DashboardRefreshLifecycleOptions,
+): () => void {
+ function handleRefreshLifecycleChange() {
+ if (options.refreshPaused()) {
+ options.pauseRefreshes();
+ return;
+ }
+
+ void options.loadDashboard();
+ }
+
+ function handlePageHide() {
+ options.pauseRefreshes();
+ }
+
+ options.documentTarget.addEventListener(
+ "visibilitychange",
+ handleRefreshLifecycleChange,
+ );
+ options.windowTarget.addEventListener("online", handleRefreshLifecycleChange);
+ options.windowTarget.addEventListener("offline", handleRefreshLifecycleChange);
+ options.windowTarget.addEventListener("pagehide", handlePageHide);
+
+ return () => {
+ options.documentTarget.removeEventListener(
+ "visibilitychange",
+ handleRefreshLifecycleChange,
+ );
+ options.windowTarget.removeEventListener("online", handleRefreshLifecycleChange);
+ options.windowTarget.removeEventListener("offline", handleRefreshLifecycleChange);
+ options.windowTarget.removeEventListener("pagehide", handlePageHide);
+ };
+}
+
+export interface DashboardHydrationQueueOptions {
+ concurrency: number;
+ hydrate: (item: TItem) => Promise | void;
+ items: TItem[];
+ signal: AbortSignal;
+}
+
+export async function runDashboardHydrationQueue(
+ options: DashboardHydrationQueueOptions,
+): Promise {
+ const concurrency = Math.max(1, Math.floor(options.concurrency));
+ let nextIndex = 0;
+
+ async function worker() {
+ while (!options.signal.aborted) {
+ const item = options.items[nextIndex];
+ nextIndex += 1;
+ if (item === undefined) return;
+
+ await options.hydrate(item);
+ }
+ }
+
+ const workerCount = Math.min(concurrency, options.items.length);
+ await Promise.all(
+ Array.from({ length: workerCount }, () => worker()),
+ );
+}
+
+export interface DashboardViewportHydrationQueueOptions {
+ batchSize?: number;
+ collectVisibleModelIds: () => Promise>;
+ concurrency: number;
+ getModelId: (item: TItem) => string;
+ hydrate: (item: TItem) => Promise | void;
+ hydrateBatch?: (items: TItem[]) => Promise | void;
+ items: TItem[];
+ onAllItemsSettled?: () => void;
+ onVisibleItemsSettled?: () => void;
+ signal: AbortSignal;
+ waitForIdle: () => Promise;
+}
+
+export async function runViewportAwareDashboardHydrationQueue(
+ options: DashboardViewportHydrationQueueOptions,
+): Promise {
+ const visibleModelIds = await options.collectVisibleModelIds();
+ if (options.signal.aborted) return;
+
+ const { visible, deferred } = splitDashboardHydrationItemsByVisibility({
+ getModelId: options.getModelId,
+ items: options.items,
+ visibleModelIds,
+ });
+
+ await runDashboardHydrationItems(options, visible);
+ if (options.signal.aborted) return;
+ options.onVisibleItemsSettled?.();
+
+ if (!deferred.length) {
+ options.onAllItemsSettled?.();
+ return;
+ }
+
+ await options.waitForIdle();
+ if (options.signal.aborted) return;
+
+ await runDashboardHydrationItems(options, deferred);
+ options.onAllItemsSettled?.();
+}
+
+async function runDashboardHydrationItems(
+ options: DashboardViewportHydrationQueueOptions,
+ items: TItem[],
+): Promise {
+ if (!options.hydrateBatch) {
+ await runDashboardHydrationQueue({
+ concurrency: options.concurrency,
+ hydrate: options.hydrate,
+ items,
+ signal: options.signal,
+ });
+ return;
+ }
+
+ await runDashboardHydrationBatchQueue({
+ batchSize: options.batchSize ?? options.concurrency,
+ hydrateBatch: options.hydrateBatch,
+ items,
+ signal: options.signal,
+ });
+}
+
+export async function runDashboardHydrationBatchQueue(options: {
+ batchSize: number;
+ hydrateBatch: (items: TItem[]) => Promise | void;
+ items: TItem[];
+ signal: AbortSignal;
+}): Promise {
+ const batchSize = Math.max(1, Math.floor(options.batchSize));
+
+ for (let index = 0; index < options.items.length; index += batchSize) {
+ if (options.signal.aborted) return;
+ await options.hydrateBatch(options.items.slice(index, index + batchSize));
+ }
+}
+
+export function splitDashboardHydrationItemsByVisibility(options: {
+ getModelId: (item: TItem) => string;
+ items: TItem[];
+ visibleModelIds: ReadonlySet;
+}): {
+ deferred: TItem[];
+ visible: TItem[];
+} {
+ const visible: TItem[] = [];
+ const deferred: TItem[] = [];
+
+ for (const item of options.items) {
+ if (options.visibleModelIds.has(options.getModelId(item))) {
+ visible.push(item);
+ } else {
+ deferred.push(item);
+ }
+ }
+
+ return { visible, deferred };
+}
+
+export interface DashboardModelElement {
+ getAttribute(name: string): string | null;
+}
+
+export interface DashboardViewportElementSource {
+ querySelectorAll(selector: string): ArrayLike;
+}
+
+export interface DashboardIntersectionEntry {
+ intersectionRatio?: number;
+ isIntersecting: boolean;
+ target: DashboardModelElement;
+}
+
+export interface DashboardIntersectionObserver {
+ disconnect(): void;
+ observe(element: DashboardModelElement): void;
+}
+
+export type DashboardIntersectionObserverFactory = (
+ callback: (entries: DashboardIntersectionEntry[]) => void,
+) => DashboardIntersectionObserver;
+
+type DashboardTimerHandle = ReturnType;
+
+export interface DashboardVisibleModelIdCollectorOptions {
+ clearTimeout?: (handle: DashboardTimerHandle) => void;
+ createObserver?: DashboardIntersectionObserverFactory;
+ documentTarget: DashboardViewportElementSource;
+ modelIds: Iterable;
+ setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
+ signal: AbortSignal;
+ timeoutMs?: number;
+}
+
+export async function collectVisibleDashboardModelIds(
+ options: DashboardVisibleModelIdCollectorOptions,
+): Promise> {
+ const targetIds = new Set(options.modelIds);
+ if (!targetIds.size || options.signal.aborted) return new Set();
+
+ const elements = Array.from(
+ options.documentTarget.querySelectorAll("[data-model-id]"),
+ ).filter((element) => {
+ const modelId = element.getAttribute("data-model-id");
+ return modelId ? targetIds.has(modelId) : false;
+ });
+
+ if (!elements.length) return new Set();
+ if (!options.createObserver) return targetIds;
+ const createObserver = options.createObserver;
+
+ const setTimer =
+ options.setTimeout ||
+ ((callback: () => void, timeoutMs: number) =>
+ globalThis.setTimeout(callback, timeoutMs));
+ const clearTimer =
+ options.clearTimeout ||
+ ((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));
+ const timeoutMs = options.timeoutMs ?? 80;
+
+ return new Promise((resolve) => {
+ const visibleModelIds = new Set();
+ let settled = false;
+ let timeoutHandle: DashboardTimerHandle | undefined;
+ let observer: DashboardIntersectionObserver | undefined;
+
+ function cleanup() {
+ if (timeoutHandle !== undefined) {
+ clearTimer(timeoutHandle);
+ }
+ observer?.disconnect();
+ options.signal.removeEventListener("abort", finish);
+ }
+
+ function finish() {
+ if (settled) return;
+ settled = true;
+ cleanup();
+ resolve(visibleModelIds);
+ }
+
+ observer = createObserver((entries) => {
+ for (const entry of entries) {
+ const modelId = entry.target.getAttribute("data-model-id");
+ if (
+ modelId &&
+ targetIds.has(modelId) &&
+ (entry.isIntersecting || (entry.intersectionRatio ?? 0) > 0)
+ ) {
+ visibleModelIds.add(modelId);
+ }
+ }
+ });
+
+ for (const element of elements) {
+ observer.observe(element);
+ }
+
+ options.signal.addEventListener("abort", finish, { once: true });
+ timeoutHandle = setTimer(finish, timeoutMs);
+ });
+}
+
+export interface DashboardHydrationIdleOptions {
+ cancelIdleCallback?: (handle: number) => void;
+ clearTimeout?: (handle: DashboardTimerHandle) => void;
+ requestIdleCallback?: (
+ callback: () => void,
+ options?: { timeout?: number },
+ ) => number;
+ setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
+ signal: AbortSignal;
+ timeoutMs?: number;
+}
+
+export async function waitForDashboardHydrationIdle(
+ options: DashboardHydrationIdleOptions,
+): Promise {
+ if (options.signal.aborted) return;
+
+ const setTimer =
+ options.setTimeout ||
+ ((callback: () => void, timeoutMs: number) =>
+ globalThis.setTimeout(callback, timeoutMs));
+ const clearTimer =
+ options.clearTimeout ||
+ ((handle: DashboardTimerHandle) => globalThis.clearTimeout(handle));
+
+ await new Promise((resolve) => {
+ let settled = false;
+ let idleHandle: number | undefined;
+ let timeoutHandle: DashboardTimerHandle | undefined;
+
+ function cleanup() {
+ if (idleHandle !== undefined) {
+ options.cancelIdleCallback?.(idleHandle);
+ }
+ if (timeoutHandle !== undefined) {
+ clearTimer(timeoutHandle);
+ }
+ options.signal.removeEventListener("abort", finish);
+ }
+
+ function finish() {
+ if (settled) return;
+ settled = true;
+ cleanup();
+ resolve();
+ }
+
+ options.signal.addEventListener("abort", finish, { once: true });
+ if (options.requestIdleCallback) {
+ idleHandle = options.requestIdleCallback(finish, {
+ timeout: options.timeoutMs ?? 1_000,
+ });
+ } else {
+ timeoutHandle = setTimer(finish, 0);
+ }
+ });
+}
+
+export interface DashboardRefreshDelayOptions {
+ failureMultiplierLimit?: number;
+ jitterRatio?: number;
+ random?: () => number;
+}
+
+export function createDashboardRefreshDelay(
+ options: DashboardRefreshDelayOptions = {},
+) {
+ const random = options.random || Math.random;
+ const jitterRatio = options.jitterRatio ?? 0.1;
+ const failureMultiplierLimit = options.failureMultiplierLimit ?? 8;
+ let consecutiveFailures = 0;
+
+ return {
+ nextDelayMs(baseDelayMs: number): number {
+ const failureMultiplier = consecutiveFailures
+ ? Math.min(2 ** consecutiveFailures, failureMultiplierLimit)
+ : 1;
+ const jitterFactor = 1 + ((random() * 2) - 1) * jitterRatio;
+ return Math.max(0, Math.round(baseDelayMs * failureMultiplier * jitterFactor));
+ },
+ recordFailure(): void {
+ consecutiveFailures += 1;
+ },
+ recordSuccess(): void {
+ consecutiveFailures = 0;
+ },
+ };
+}
+
+export interface DashboardPerformanceMarkOptions {
+ mark?: (name: string) => void;
+}
+
+export const dashboardPerformanceMarks = {
+ allTilesSettled: "dashboard:all-tiles-settled",
+ firstTileReady: "dashboard:first-tile-ready",
+ shellLoad: "dashboard:shell-load",
+ visibleTilesReady: "dashboard:visible-tiles-ready",
+} as const;
+
+export function createDashboardPerformanceMarks(
+ options: DashboardPerformanceMarkOptions = {},
+) {
+ const mark = options.mark ||
+ globalThis.performance?.mark?.bind(globalThis.performance);
+ let firstTileReadyMarked = false;
+
+ function safeMark(name: string) {
+ try {
+ mark?.(name);
+ } catch {
+ // Performance marks are diagnostics only.
+ }
+ }
+
+ return {
+ markAllTilesSettled(): void {
+ safeMark(dashboardPerformanceMarks.allTilesSettled);
+ },
+ markFirstTileReady(): void {
+ if (firstTileReadyMarked) return;
+ firstTileReadyMarked = true;
+ safeMark(dashboardPerformanceMarks.firstTileReady);
+ },
+ markShellLoad(): void {
+ firstTileReadyMarked = false;
+ safeMark(dashboardPerformanceMarks.shellLoad);
+ },
+ markVisibleTilesReady(): void {
+ safeMark(dashboardPerformanceMarks.visibleTilesReady);
+ },
+ };
+}
+
+export interface DashboardTileEventSource {
+ addEventListener(
+ type: "dashboard-tile",
+ listener: (event: MessageEvent) => void,
+ ): void;
+ close(): void;
+ onerror: (() => void) | null;
+}
+
+export interface DashboardTileEventSubscriptionOptions {
+ createEventSource?: (url: string) => DashboardTileEventSource;
+ onTile: (data: unknown) => void;
+ onUnavailable?: () => void;
+ url?: string;
+}
+
+export function subscribeToDashboardTileEvents(
+ options: DashboardTileEventSubscriptionOptions,
+): () => void {
+ const createEventSource = options.createEventSource ||
+ (typeof globalThis.EventSource !== "undefined"
+ ? (url: string) => new globalThis.EventSource(url)
+ : undefined);
+
+ if (!createEventSource) {
+ options.onUnavailable?.();
+ return () => undefined;
+ }
+
+ const source = createEventSource(options.url || "/api/dashboard/events");
+ source.addEventListener("dashboard-tile", (event) => {
+ try {
+ options.onTile(JSON.parse(event.data));
+ } catch {
+ // Ignore malformed diagnostics from an optional live transport.
+ }
+ });
+ source.onerror = () => {
+ source.close();
+ options.onUnavailable?.();
+ };
+
+ return () => source.close();
+}
+
+const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
+
+export function createDashboardTileBackoff() {
+ const failures = new Map();
+
+ return {
+ canAttempt(key: string, now = Date.now()): boolean {
+ const failure = failures.get(key);
+ return !failure || now >= failure.nextAttemptAt;
+ },
+ recordFailure(key: string, now = Date.now()): void {
+ const previousAttempts = failures.get(key)?.attempts || 0;
+ const attempts = previousAttempts + 1;
+ const delay =
+ dashboardTileBackoffDelaysMs[
+ Math.min(attempts - 1, dashboardTileBackoffDelaysMs.length - 1)
+ ];
+ failures.set(key, {
+ attempts,
+ nextAttemptAt: now + delay,
+ });
+ },
+ recordSuccess(key: string): void {
+ failures.delete(key);
+ },
+ };
+}
+
+interface DashboardTileSnapshotRecord {
+ item: DashboardTileSnapshotItem;
+ savedAt: number;
+ tile: DashboardTileReference;
+}
+
+interface DashboardTileSnapshotPayload {
+ currentRevisionId: string;
+ schemaVersion: string;
+ tiles: DashboardTileSnapshotRecord[];
+ version: 1;
+}
+
+export interface DashboardTileSnapshotStoreContext {
+ currentRevisionId: string;
+ schemaVersion: string;
+}
+
+export interface DashboardTileSnapshotStoreOptions {
+ now?: () => number;
+}
+
+export interface RestoredDashboardTileSnapshot {
+ ageMs: number;
+ response: Extract;
+}
+
+const dashboardTileSnapshotStorageKey = "dimensionlab.dashboard.tiles.v1";
+
+export function createDashboardTileSnapshotStore(
+ storage: Storage | undefined,
+ options: DashboardTileSnapshotStoreOptions = {},
+) {
+ const now = options.now || Date.now;
+
+ function read(): DashboardTileSnapshotPayload | null {
+ if (!storage) return null;
+
+ try {
+ const serialized = storage.getItem(dashboardTileSnapshotStorageKey);
+ if (!serialized) return null;
+
+ const payload = JSON.parse(serialized) as Partial;
+ if (
+ payload.version !== 1 ||
+ typeof payload.currentRevisionId !== "string" ||
+ typeof payload.schemaVersion !== "string" ||
+ !Array.isArray(payload.tiles)
+ ) {
+ return null;
+ }
+
+ return {
+ currentRevisionId: payload.currentRevisionId,
+ schemaVersion: payload.schemaVersion,
+ tiles: payload.tiles
+ .filter(isDashboardTileSnapshotRecord)
+ .filter((record) => isPersistableDashboardTileSnapshot(record.tile)),
+ version: 1,
+ };
+ } catch {
+ return null;
+ }
+ }
+
+ function write(payload: DashboardTileSnapshotPayload): void {
+ if (!storage) return;
+
+ try {
+ storage.setItem(dashboardTileSnapshotStorageKey, JSON.stringify(payload));
+ } catch {
+ // Best-effort warm-start cache; quota and privacy failures are non-fatal.
+ }
+ }
+
+ function matchingPayload(
+ context: DashboardTileSnapshotStoreContext,
+ ): DashboardTileSnapshotPayload {
+ const payload = read();
+ if (
+ payload &&
+ payload.currentRevisionId === context.currentRevisionId &&
+ payload.schemaVersion === context.schemaVersion
+ ) {
+ return payload;
+ }
+
+ return {
+ currentRevisionId: context.currentRevisionId,
+ schemaVersion: context.schemaVersion,
+ tiles: [],
+ version: 1,
+ };
+ }
+
+ function saveReadyTile(
+ input: DashboardTileSnapshotStoreContext & {
+ response: Extract;
+ },
+ ): void {
+ if (!isPersistableDashboardTileSnapshot(input.response.tile)) return;
+
+ const payload = matchingPayload(input);
+ const key = dashboardTileSnapshotKey(input.response.tile);
+ const nextRecord: DashboardTileSnapshotRecord = {
+ item: input.response.item,
+ savedAt: now(),
+ tile: input.response.tile,
+ };
+ payload.tiles = [
+ nextRecord,
+ ...payload.tiles.filter((record) =>
+ dashboardTileSnapshotKey(record.tile) !== key
+ ),
+ ];
+ write(payload);
+ }
+
+ return {
+ restore(context: DashboardTileSnapshotStoreContext): RestoredDashboardTileSnapshot[] {
+ const payload = read();
+ if (
+ !payload ||
+ payload.currentRevisionId !== context.currentRevisionId ||
+ payload.schemaVersion !== context.schemaVersion
+ ) {
+ return [];
+ }
+
+ const restoredAt = now();
+ return payload.tiles.map((record) => ({
+ ageMs: Math.max(0, restoredAt - record.savedAt),
+ response: {
+ state: "ready",
+ tile: record.tile,
+ item: {
+ ...record.item,
+ detail: staleDashboardTileDetail(record.item.detail, restoredAt - record.savedAt),
+ severity: "stale",
+ },
+ },
+ }));
+ },
+ saveReadyTile,
+ saveTile(input: DashboardTileSnapshotStoreContext & {
+ response: DashboardTileSnapshotResponse;
+ }): void {
+ if (input.response.state === "ready") {
+ saveReadyTile({
+ currentRevisionId: input.currentRevisionId,
+ response: input.response,
+ schemaVersion: input.schemaVersion,
+ });
+ }
+ },
+ };
+}
+
+function dashboardTileSnapshotKey(tile: DashboardTileReference): string {
+ return JSON.stringify(tile);
+}
+
+export function isPersistableDashboardTileSnapshot(
+ tile: DashboardTileReference,
+): boolean {
+ if (tile.kind === "status" && tile.id === "system-status") return false;
+ if (tile.kind === "module" && tile.id === "runtime-health-summary") return false;
+ return true;
+}
+
+function isDashboardTileSnapshotRecord(
+ value: unknown,
+): value is DashboardTileSnapshotRecord {
+ if (!isSnapshotRecord(value)) return false;
+
+ return (
+ typeof value.savedAt === "number" &&
+ Number.isFinite(value.savedAt) &&
+ isDashboardTileReference(value.tile) &&
+ isDashboardTileSnapshotItem(value.tile, value.item)
+ );
+}
+
+function isDashboardTileReference(
+ value: unknown,
+): value is DashboardTileReference {
+ if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;
+
+ switch (value.kind) {
+ case "telemetry":
+ case "module":
+ return typeof value.id === "string";
+ case "service":
+ return typeof value.groupId === "string" && typeof value.id === "string";
+ case "status":
+ return typeof value.stripId === "string" && typeof value.id === "string";
+ default:
+ return false;
+ }
+}
+
+function isDashboardTileSnapshotItem(
+ tile: DashboardTileReference,
+ value: unknown,
+): value is DashboardTileSnapshotItem {
+ if (!isSnapshotRecord(value) || value.id !== tile.id) return false;
+
+ switch (tile.kind) {
+ case "telemetry":
+ return (
+ typeof value.label === "string" &&
+ isMetricValue(value.value) &&
+ isSeverity(value.severity) &&
+ isOptionalString(value.detail) &&
+ isOptionalString(value.description) &&
+ isOptionalString(value.icon) &&
+ isOptionalNumberArray(value.sparkline)
+ );
+ case "service":
+ return (
+ typeof value.label === "string" &&
+ typeof value.description === "string" &&
+ isSeverity(value.severity) &&
+ isOptionalString(value.detail) &&
+ isOptionalString(value.icon) &&
+ isOptionalLink(value.link)
+ );
+ case "module":
+ return (
+ (value.kind === "summary" ||
+ value.kind === "weather" ||
+ value.kind === "custom") &&
+ isOptionalString(value.title) &&
+ isOptionalString(value.label) &&
+ isOptionalString(value.value) &&
+ isOptionalString(value.detail) &&
+ isOptionalString(value.icon) &&
+ (value.severity === undefined || isSeverity(value.severity))
+ );
+ case "status":
+ return (
+ typeof value.label === "string" &&
+ typeof value.value === "string" &&
+ isOptionalLink(value.link) &&
+ (value.severity === undefined || isSeverity(value.severity))
+ );
+ }
+}
+
+function isSnapshotRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function isMetricValue(value: unknown): boolean {
+ if (!isSnapshotRecord(value) || typeof value.kind !== "string") return false;
+
+ if (value.kind === "text") {
+ return (
+ typeof value.value === "string" &&
+ isOptionalString(value.unit)
+ );
+ }
+
+ const numericKinds = ["bytes", "latency", "number", "percent", "temperature"];
+ if (!numericKinds.includes(value.kind)) return false;
+ if (typeof value.value !== "number" || !Number.isFinite(value.value)) return false;
+ if (value.kind === "percent" && (value.value < 0 || value.value > 100)) {
+ return false;
+ }
+
+ const precision = value.precision;
+ return (
+ isOptionalString(value.unit) &&
+ (precision === undefined ||
+ (typeof precision === "number" &&
+ Number.isInteger(precision) &&
+ precision >= 0 &&
+ precision <= 4))
+ );
+}
+
+function isSeverity(value: unknown): boolean {
+ return (
+ value === "neutral" ||
+ value === "ok" ||
+ value === "warning" ||
+ value === "danger" ||
+ value === "stale" ||
+ value === "unavailable"
+ );
+}
+
+function isOptionalString(value: unknown): boolean {
+ return value === undefined || typeof value === "string";
+}
+
+function isOptionalNumberArray(value: unknown): boolean {
+ return (
+ value === undefined ||
+ (Array.isArray(value) &&
+ value.every((item) => typeof item === "number" && Number.isFinite(item)))
+ );
+}
+
+function isOptionalLink(value: unknown): boolean {
+ return (
+ value === undefined ||
+ (isSnapshotRecord(value) &&
+ typeof value.href === "string" &&
+ isOptionalString(value.label) &&
+ (value.external === undefined || typeof value.external === "boolean"))
+ );
+}
+
+function staleDashboardTileDetail(
+ detail: string | undefined,
+ ageMs: number,
+): string | undefined {
+ if (!detail) return detail;
+ const ageSeconds = Math.max(0, Math.floor(ageMs / 1_000));
+ return ageSeconds > 0 ? `${detail} - stale ${ageSeconds}s` : detail;
+}
diff --git a/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
index 31c01d9..1a97c3b 100644
--- a/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
+++ b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
@@ -128,7 +128,6 @@ const verifiedSeedIconIds = new Set([
"mdi:pulse",
"mdi:robot-outline",
"mdi:router-network",
- "mdi:text-box-search",
"mdi:thermometer",
"mdi:web",
"mdi:weather-sunny",
diff --git a/apps/web/src/lib/dashboard-seed/dimensionlab.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.ts
index 6cc544b..59162e6 100644
--- a/apps/web/src/lib/dashboard-seed/dimensionlab.ts
+++ b/apps/web/src/lib/dashboard-seed/dimensionlab.ts
@@ -431,14 +431,6 @@ export const dimensionLabDashboardFixture: DashboardDocument = {
href: "https://models.dimensionlab.net",
datasource: uptimeMonitor(7),
}),
- service({
- id: "prompt-registry",
- label: "Prompt Registry",
- description: "Shared prompts, traces, evals",
- icon: "mdi:text-box-search",
- href: "https://prompts.dimensionlab.net",
- datasource: uptimeMonitor(20),
- }),
]),
group("systems", "Systems", [
service({
diff --git a/apps/web/src/lib/server/dashboard.test.ts b/apps/web/src/lib/server/dashboard.test.ts
index 7851ebb..5d93422 100644
--- a/apps/web/src/lib/server/dashboard.test.ts
+++ b/apps/web/src/lib/server/dashboard.test.ts
@@ -66,9 +66,6 @@ describe("dashboard runtime loader", () => {
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");
});
@@ -138,13 +135,5 @@ function olderDimensionLabSeed() {
...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/apps/web/src/lib/server/dashboard.ts b/apps/web/src/lib/server/dashboard.ts
index 47ebfa2..33afa3a 100644
--- a/apps/web/src/lib/server/dashboard.ts
+++ b/apps/web/src/lib/server/dashboard.ts
@@ -18,6 +18,9 @@ export interface DashboardRuntimeReady {
document: DashboardDocument;
schemaVersion: string;
currentRevisionId: string;
+ liveDatasourceHydration?: {
+ enabled: boolean;
+ };
}
export interface DashboardRuntimeEmpty {
diff --git a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
index 61acdd3..d24b48b 100644
--- a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
+++ b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
@@ -3,7 +3,7 @@ import {
DASHBOARD_SCHEMA_VERSION,
type DashboardDocument,
} from "@dimensionlab/dashboard-model";
-import { resolveDashboardDatasources } from ".";
+import { resolveDashboardDatasources, resolveDashboardTile } from ".";
describe("dashboard datasource resolution", () => {
test("hydrates telemetry, service health, weather, and summary data from live adapters", async () => {
@@ -124,6 +124,100 @@ describe("dashboard datasource resolution", () => {
expect(resolved).not.toBe(testDocument);
expect(testDocument.telemetry[0].value.value).toBe(1);
});
+
+ test("shares service health snapshots across aggregate tile hydration", async () => {
+ let resolveFetch: ((response: Response) => void) | undefined;
+ const fetch = vi.fn((input: RequestInfo | URL) => {
+ const url = String(input);
+ if (url !== "https://service.example/health") {
+ throw new Error(`Unhandled test request: ${url}`);
+ }
+
+ return new Promise((resolve) => {
+ resolveFetch = resolve;
+ });
+ });
+ const document = testDashboard();
+
+ const moduleTile = resolveDashboardTile(
+ document,
+ { kind: "module", id: "runtime-health-summary" },
+ { fetch },
+ );
+ const statusTile = resolveDashboardTile(
+ document,
+ { kind: "status", stripId: "footer", id: "system-status" },
+ { fetch },
+ );
+
+ await Promise.resolve();
+ expect(fetch).toHaveBeenCalledTimes(1);
+
+ resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
+ expect(await moduleTile).toMatchObject({
+ state: "ready",
+ item: {
+ id: "runtime-health-summary",
+ severity: "ok",
+ value: "all systems operational",
+ },
+ });
+ expect(await statusTile).toMatchObject({
+ state: "ready",
+ item: {
+ id: "system-status",
+ severity: "ok",
+ value: "All systems operational",
+ },
+ });
+ });
+
+ test("isolates service health snapshots by datasource fetch context", async () => {
+ const firstFetch = vi.fn(async () =>
+ jsonResponse({
+ status: "UP",
+ ping: 42,
+ })
+ );
+ const secondFetch = vi.fn(async () =>
+ jsonResponse({
+ status: "DOWN",
+ ping: 0,
+ })
+ );
+ const document = testDashboard();
+ document.serviceGroups[0].services[0] = {
+ ...document.serviceGroups[0].services[0],
+ id: "api-isolated",
+ datasource: {
+ type: "external",
+ adapter: "http-status",
+ reference: "GET https://service.example/isolated",
+ },
+ };
+
+ await resolveDashboardTile(
+ document,
+ { kind: "status", stripId: "footer", id: "system-status" },
+ { fetch: firstFetch },
+ );
+ const second = await resolveDashboardTile(
+ document,
+ { kind: "status", stripId: "footer", id: "system-status" },
+ { fetch: secondFetch },
+ );
+
+ expect(firstFetch).toHaveBeenCalledTimes(1);
+ expect(secondFetch).toHaveBeenCalledTimes(1);
+ expect(second).toMatchObject({
+ state: "ready",
+ item: {
+ id: "system-status",
+ severity: "danger",
+ value: "1 service down",
+ },
+ });
+ });
});
const testDocument = testDashboard();
diff --git a/apps/web/src/lib/server/datasources/index.ts b/apps/web/src/lib/server/datasources/index.ts
index d69db86..62df13a 100644
--- a/apps/web/src/lib/server/datasources/index.ts
+++ b/apps/web/src/lib/server/datasources/index.ts
@@ -10,8 +10,38 @@ import type {
TelemetryCard,
} from "@dimensionlab/dashboard-model";
+export type DashboardTileReference =
+ | { kind: "telemetry"; id: string }
+ | { kind: "service"; groupId: string; id: string }
+ | { kind: "module"; id: string }
+ | { kind: "status"; stripId: string; id: string };
+
+export type DashboardTileItem =
+ | DashboardModule
+ | ServiceEntry
+ | StatusItem
+ | TelemetryCard;
+
+export type DashboardTileResolution =
+ | {
+ state: "ready";
+ tile: DashboardTileReference;
+ item: DashboardTileItem;
+ }
+ | {
+ state: "not_found";
+ tile: DashboardTileReference;
+ message: string;
+ }
+ | {
+ state: "disabled";
+ tile: DashboardTileReference;
+ message: string;
+ };
+
export interface DatasourceResolutionOptions {
fetch?: DatasourceFetch;
+ now?: () => number;
prometheusBaseUrl?: string;
prometheusRangeSeconds?: number;
prometheusStepSeconds?: number;
@@ -46,16 +76,90 @@ export async function resolveDashboardDatasources(
};
}
+export async function resolveDashboardTile(
+ document: DashboardDocument,
+ tile: DashboardTileReference,
+ options: DatasourceResolutionOptions = {},
+): Promise {
+ const context = datasourceContext(options);
+
+ if (tile.kind === "telemetry") {
+ const card = document.telemetry.find((item) => item.id === tile.id);
+ if (!card) return missingTile(tile);
+
+ return {
+ state: "ready",
+ tile,
+ item: await resolveTelemetryCard(card, context),
+ };
+ }
+
+ if (tile.kind === "service") {
+ const service = document.serviceGroups
+ .find((group) => group.id === tile.groupId)
+ ?.services.find((item) => item.id === tile.id);
+ if (!service) return missingTile(tile);
+
+ return {
+ state: "ready",
+ tile,
+ item: await resolveService(service, context),
+ };
+ }
+
+ if (tile.kind === "module") {
+ const module = document.modules?.find((item) => item.id === tile.id);
+ if (!module) return missingTile(tile);
+
+ const item = module.id === "runtime-health-summary"
+ ? runtimeHealthSummary(
+ module,
+ await serviceGroupsSnapshot(document, context),
+ )
+ : await resolveModule(module, context);
+
+ return { state: "ready", tile, item };
+ }
+
+ const strip = document.statusStrips.find((item) => item.id === tile.stripId);
+ const statusItem = strip?.items.find((item) => item.id === tile.id);
+ if (!strip || !statusItem) return missingTile(tile);
+
+ return {
+ state: "ready",
+ tile,
+ item: await resolveStatusTile(
+ statusItem,
+ document.metadata.refreshIntervalSeconds,
+ document,
+ context,
+ ),
+ };
+}
+
interface DatasourceContext {
fetch: DatasourceFetch;
+ fetchIdentity: number;
+ now: () => number;
prometheusBaseUrl: string;
prometheusRangeSeconds: number;
prometheusStepSeconds: number;
requestTimeoutMs: number;
+ serviceGroupsSnapshot?: Promise;
}
type DatasourceFetch = (input: string, init?: RequestInit) => Promise;
+interface ServiceGroupsSnapshotEntry {
+ expiresAt: number;
+ snapshot: Promise;
+}
+
+const serviceGroupsSnapshotTtlMs = 30_000;
+const serviceGroupsSnapshotCache = new Map();
+const datasourceFetchIdentities = new WeakMap();
+let nextDatasourceFetchIdentity = 1;
+
interface PrometheusVectorResult {
metric?: Record;
value?: [number, string];
@@ -67,8 +171,12 @@ interface PrometheusMatrixResult {
}
function datasourceContext(options: DatasourceResolutionOptions): DatasourceContext {
+ const fetch = options.fetch || globalThis.fetch;
+
return {
- fetch: options.fetch || globalThis.fetch,
+ fetch,
+ fetchIdentity: datasourceFetchIdentity(fetch),
+ now: options.now || Date.now,
prometheusBaseUrl:
options.prometheusBaseUrl ||
process.env.PROMETHEUS_BASE_URL ||
@@ -79,6 +187,68 @@ function datasourceContext(options: DatasourceResolutionOptions): DatasourceCont
};
}
+function datasourceFetchIdentity(fetch: DatasourceFetch): number {
+ const existing = datasourceFetchIdentities.get(fetch);
+ if (existing) return existing;
+
+ const next = nextDatasourceFetchIdentity;
+ nextDatasourceFetchIdentity += 1;
+ datasourceFetchIdentities.set(fetch, next);
+ return next;
+}
+
+function serviceGroupsSnapshot(
+ document: DashboardDocument,
+ context: DatasourceContext,
+): Promise {
+ if (context.serviceGroupsSnapshot) return context.serviceGroupsSnapshot;
+
+ const key = serviceGroupsSnapshotKey(document, context);
+ const now = context.now();
+ const cached = serviceGroupsSnapshotCache.get(key);
+ if (cached && cached.expiresAt > now) {
+ context.serviceGroupsSnapshot = cached.snapshot;
+ return cached.snapshot;
+ }
+
+ const snapshot = Promise.all(
+ document.serviceGroups.map((group) => resolveServiceGroup(group, context)),
+ );
+ context.serviceGroupsSnapshot = snapshot;
+ serviceGroupsSnapshotCache.set(key, {
+ expiresAt: now + serviceGroupsSnapshotTtlMs,
+ snapshot,
+ });
+ snapshot.catch(() => {
+ if (serviceGroupsSnapshotCache.get(key)?.snapshot === snapshot) {
+ serviceGroupsSnapshotCache.delete(key);
+ }
+ });
+ return snapshot;
+}
+
+function serviceGroupsSnapshotKey(
+ document: DashboardDocument,
+ context: DatasourceContext,
+): string {
+ return JSON.stringify(
+ {
+ fetchIdentity: context.fetchIdentity,
+ prometheusBaseUrl: context.prometheusBaseUrl,
+ prometheusRangeSeconds: context.prometheusRangeSeconds,
+ prometheusStepSeconds: context.prometheusStepSeconds,
+ requestTimeoutMs: context.requestTimeoutMs,
+ serviceGroups: document.serviceGroups.map((group) => ({
+ id: group.id,
+ services: group.services.map((service) => ({
+ datasource: service.datasource,
+ id: service.id,
+ })),
+ })),
+ },
+ );
+}
+
async function resolveTelemetryCard(
card: TelemetryCard,
context: DatasourceContext,
@@ -390,6 +560,66 @@ function resolveStatusItem(
return structuredClone(item);
}
+async function resolveStatusTile(
+ item: StatusItem,
+ refreshIntervalSeconds: number | undefined,
+ document: DashboardDocument,
+ context: DatasourceContext,
+): Promise {
+ if (item.id === "system-status") {
+ const resolvedGroups = await serviceGroupsSnapshot(document, context);
+ const health = serviceHealthSummary(resolvedGroups);
+ return {
+ ...structuredClone(item),
+ value: health.value,
+ severity: health.severity,
+ };
+ }
+
+ if (item.id === "last-sync") {
+ return {
+ ...structuredClone(item),
+ value: "just now",
+ severity: "ok",
+ };
+ }
+
+ if (item.id === "uptime") {
+ const uptime = await prometheusScalar(
+ 'time() - node_boot_time_seconds{job="node",host="linux-infra"}',
+ context,
+ ).catch(() => null);
+ return uptime === null
+ ? structuredClone(item)
+ : {
+ ...structuredClone(item),
+ value: formatDuration(uptime),
+ severity: "ok",
+ };
+ }
+
+ if (item.id === "load-avg") {
+ const loadAverage = await prometheusLoadAverage(context).catch(() => null);
+ return loadAverage
+ ? {
+ ...structuredClone(item),
+ value: loadAverage,
+ severity: "neutral",
+ }
+ : structuredClone(item);
+ }
+
+ if (item.id === "auto-refresh" && refreshIntervalSeconds) {
+ return {
+ ...structuredClone(item),
+ value: `${refreshIntervalSeconds}s`,
+ severity: "neutral",
+ };
+ }
+
+ return structuredClone(item);
+}
+
function serviceHealthSummary(serviceGroups: ServiceGroup[]): {
severity: Severity;
value: string;
@@ -673,3 +903,17 @@ function formatDuration(totalSeconds: number): string {
const minutes = Math.floor((seconds % 3_600) / 60);
return `${days}d ${hours}h ${minutes}m`;
}
+
+function missingTile(tile: DashboardTileReference): DashboardTileResolution {
+ return {
+ state: "not_found",
+ tile,
+ message: `Dashboard tile not found: ${tileKey(tile)}`,
+ };
+}
+
+function tileKey(tile: DashboardTileReference): string {
+ if (tile.kind === "status") return `${tile.kind}:${tile.stripId}:${tile.id}`;
+ if (tile.kind === "service") return `${tile.kind}:${tile.groupId}:${tile.id}`;
+ return `${tile.kind}:${tile.id}`;
+}
diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts
index 38a3f1b..58cf203 100644
--- a/apps/web/src/lib/workspace-boundary.test.ts
+++ b/apps/web/src/lib/workspace-boundary.test.ts
@@ -1,4 +1,6 @@
-import { existsSync, readFileSync } from "node:fs";
+import { spawnSync } from "node:child_process";
+import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
@@ -42,6 +44,8 @@ describe("workspace boundaries", () => {
"turbo run check test:unit build @dimensionlab/ui#build-storybook @dimensionlab/web#test:e2e",
);
expect(webPackage.scripts).not.toHaveProperty("test:qa");
+ expect(webPackage.scripts?.test).toBe("bun --bun vitest run");
+ expect(webPackage.scripts?.["test:unit"]).toBe("bun --bun vitest run");
expect(turboConfig.tasks).not.toHaveProperty("test:qa");
expect(turboConfig.globalDependencies).toEqual(
expect.arrayContaining(["bun.lock", "tsconfig.base.json"]),
@@ -195,7 +199,7 @@ describe("workspace boundaries", () => {
const sourceCopy = "COPY --from=pruner /repo/out/full/ ./";
const tsconfigCopy =
"COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./";
- const installCommand = "RUN bun install --frozen-lockfile";
+ const installCommand = "RUN bun install --frozen-lockfile --ignore-scripts";
expect(containerfile).toContain("AS pruner");
expect(containerfile).toContain(pruneCommand);
@@ -253,6 +257,186 @@ describe("workspace boundaries", () => {
"rm -rf build && vite build && bun build src/server/index.ts --target bun --outdir build",
);
});
+
+ test("defines Forgejo CI and main-branch deploy automation", () => {
+ const workflow = readFileSync(
+ join(root, ".forgejo/workflows/dimensionlab-website.yml"),
+ "utf8",
+ );
+
+ expect(workflow).toContain("name: Dimension Lab website");
+ expect(workflow).toContain("pull_request:");
+ expect(workflow).toContain("push:");
+ expect(workflow).toContain("branches:");
+ expect(workflow).toContain("- main");
+ expect(workflow).toContain("runs-on: docker");
+ expect(workflow).toContain("bun install --frozen-lockfile");
+ expect(workflow).toContain("bun run check");
+ expect(workflow).toContain("bun run test");
+ expect(workflow).toContain("bun run build");
+ expect(workflow).toContain("needs: ci");
+ expect(workflow).toContain("runs-on: deploy");
+ expect(workflow).toContain("github.event_name == 'push'");
+ expect(workflow).toContain("github.ref == 'refs/heads/main'");
+ expect(workflow).toContain(
+ "git remote add origin git@git.dimensionlab.net:vince/dimensionlab-website.git",
+ );
+ expect(workflow).toContain('git fetch --force --prune --depth=1 origin "$GITHUB_SHA"');
+ expect(workflow).toContain("podman inspect dimensionlab-website");
+ expect(workflow).toContain("DEPLOY_CONTAINER_CLI: podman");
+ expect(workflow).toContain("PODMAN_SYSTEMD_UNIT");
+ expect(workflow).toContain("scripts/deploy-dimensionlab-website.sh");
+ });
+
+ test("keeps production deployment behind a guarded script", () => {
+ const deployScript = readFileSync(
+ join(root, "scripts/deploy-dimensionlab-website.sh"),
+ "utf8",
+ );
+
+ expect(deployScript).toContain("refs/heads/main");
+ expect(deployScript).toContain("dimensionlab-website.service");
+ expect(deployScript).toContain("localhost/dimensionlab-website");
+ expect(deployScript).toContain("apps/web/Containerfile");
+ expect(deployScript).toContain("rollback-");
+ expect(deployScript).toContain("https://dimensionlab.net");
+ expect(deployScript).toContain("/api/dashboard/tiles");
+ expect(deployScript).toContain("--dry-run");
+ expect(deployScript).toContain("DEPLOY_RESTART_STRATEGY");
+ });
+
+ test("rolls back the latest image when production smoke checks fail", () => {
+ const result = runDeployScriptWithFakes(
+ {
+ curl: failingCurlCommand,
+ git: fakeGitCommand,
+ podman: fakePodmanCommand("dimensionlab-website.service"),
+ },
+ {
+ DEPLOY_CONTAINER_CLI: "podman",
+ DEPLOY_EVENT_NAME: "push",
+ DEPLOY_REF: "refs/heads/main",
+ DEPLOY_RESTART_STRATEGY: "quadlet-container",
+ DEPLOY_SHA: "1234567890abcdef",
+ DEPLOY_SMOKE_TIMEOUT_SECONDS: "0",
+ },
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("smoke checks failed");
+ expect(result.stderr).toContain("rolling back to localhost/dimensionlab-website:rollback-");
+ expect(result.log).toMatch(
+ /tag localhost\/dimensionlab-website:latest localhost\/dimensionlab-website:rollback-\d{14}/,
+ );
+ expect(result.log).toMatch(
+ /tag localhost\/dimensionlab-website:rollback-\d{14} localhost\/dimensionlab-website:latest/,
+ );
+ expect(result.log.match(/^stop dimensionlab-website$/gm)).toHaveLength(2);
+ });
+
+ test("rolls back the latest image when the container fails to restart", () => {
+ const result = runDeployScriptWithFakes(
+ {
+ curl: passingCurlCommand,
+ git: fakeGitCommand,
+ podman: fakePodmanCommand("dimensionlab-website.service"),
+ },
+ {
+ DEPLOY_CONTAINER_CLI: "podman",
+ DEPLOY_CONTAINER_START_TIMEOUT_SECONDS: "1",
+ DEPLOY_TEST_CONTAINER_IMAGE: "wrong",
+ DEPLOY_EVENT_NAME: "push",
+ DEPLOY_REF: "refs/heads/main",
+ DEPLOY_RESTART_STRATEGY: "quadlet-container",
+ DEPLOY_SHA: "1234567890abcdef",
+ },
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("did not restart on localhost/dimensionlab-website:latest");
+ expect(result.stderr).toContain("rolling back to localhost/dimensionlab-website:rollback-");
+ expect(result.stderr).toContain("rollback image is running");
+ expect(result.log).toMatch(
+ /tag localhost\/dimensionlab-website:rollback-\d{14} localhost\/dimensionlab-website:latest/,
+ );
+ expect(result.log.match(/^stop dimensionlab-website$/gm)).toHaveLength(2);
+ });
+
+ test.each([
+ {
+ env: { DEPLOY_EVENT_NAME: "pull_request", DEPLOY_REF: "refs/heads/main" },
+ message: "production deploys only run for push",
+ },
+ {
+ env: { DEPLOY_EVENT_NAME: "push", DEPLOY_REF: "refs/heads/codex/test" },
+ message: "expected refs/heads/main",
+ },
+ ])("refuses guarded deploy contexts before host mutations", ({ env, message }) => {
+ const result = runDeployScriptWithFakes(
+ {
+ curl: passingCurlCommand,
+ git: fakeGitCommand,
+ podman: fakePodmanCommand("dimensionlab-website.service"),
+ },
+ {
+ DEPLOY_CONTAINER_CLI: "podman",
+ DEPLOY_SHA: "1234567890abcdef",
+ ...env,
+ },
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain(message);
+ expect(result.log).toBe("");
+ });
+
+ test("refuses stop-based deploys unless the container belongs to the expected unit", () => {
+ const result = runDeployScriptWithFakes(
+ {
+ curl: passingCurlCommand,
+ git: fakeGitCommand,
+ podman: fakePodmanCommand("other.service"),
+ },
+ {
+ DEPLOY_CONTAINER_CLI: "podman",
+ DEPLOY_EVENT_NAME: "push",
+ DEPLOY_REF: "refs/heads/main",
+ DEPLOY_RESTART_STRATEGY: "quadlet-container",
+ DEPLOY_SHA: "1234567890abcdef",
+ },
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain(
+ "refusing to stop dimensionlab-website; expected PODMAN_SYSTEMD_UNIT=dimensionlab-website.service",
+ );
+ expect(result.log).not.toContain("build ");
+ expect(result.log).not.toContain("stop dimensionlab-website");
+ });
+
+ test("checks the expected unit before auto falls back to stopping the container", () => {
+ const result = runDeployScriptWithFakes(
+ {
+ curl: passingCurlCommand,
+ git: fakeGitCommand,
+ podman: fakePodmanCommand("other.service"),
+ systemctl: fakeSystemctlCommand({ active: false, show: true }),
+ },
+ {
+ DEPLOY_CONTAINER_CLI: "podman",
+ DEPLOY_EVENT_NAME: "push",
+ DEPLOY_REF: "refs/heads/main",
+ DEPLOY_RESTART_STRATEGY: "auto",
+ DEPLOY_SHA: "1234567890abcdef",
+ },
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain(
+ "refusing to stop dimensionlab-website; expected PODMAN_SYSTEMD_UNIT=dimensionlab-website.service",
+ );
+ expect(result.log).not.toContain("stop dimensionlab-website");
+ });
});
type WorkspacePackageExport =
@@ -269,3 +453,126 @@ function expectPackageExport(
): void {
expect(actual).toMatchObject(expected);
}
+
+function runDeployScriptWithFakes(
+ commands: Record,
+ env: Record,
+): { log: string; status: number | null; stderr: string; stdout: string } {
+ const tempDir = mkdtempSync(join(tmpdir(), "dimensionlab-deploy-test-"));
+ const logPath = join(tempDir, "commands.log");
+
+ for (const [name, source] of Object.entries(commands)) {
+ const commandPath = join(tempDir, name);
+ writeFileSync(commandPath, source);
+ chmodSync(commandPath, 0o755);
+ }
+
+ const result = spawnSync("bash", [join(root, "scripts/deploy-dimensionlab-website.sh")], {
+ cwd: root,
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ ...env,
+ DEPLOY_TEST_LOG: logPath,
+ PATH: `${tempDir}:${process.env.PATH ?? ""}`,
+ },
+ });
+
+ return {
+ log: existsSync(logPath) ? readFileSync(logPath, "utf8") : "",
+ status: result.status,
+ stderr: result.stderr,
+ stdout: result.stdout,
+ };
+}
+
+const fakeGitCommand = `#!/usr/bin/env bash
+case "$1" in
+ branch)
+ echo main
+ ;;
+ rev-parse)
+ echo 1234567890abcdef
+ ;;
+ config|submodule)
+ exit 0
+ ;;
+esac
+`;
+
+function fakePodmanCommand(systemdUnit: string): string {
+ return `#!/usr/bin/env bash
+state_file="$DEPLOY_TEST_LOG.state"
+[ -f "$state_file" ] || printf 'initial' > "$state_file"
+printf '%s\\n' "$*" >> "$DEPLOY_TEST_LOG"
+if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then
+ if [ "$4" = "--format" ]; then
+ case "$3" in
+ *:rollback-*)
+ echo sha256:old
+ ;;
+ *)
+ if [ "$(cat "$state_file")" = "rollback" ]; then
+ echo sha256:old
+ else
+ echo sha256:new
+ fi
+ ;;
+ esac
+ fi
+ exit 0
+fi
+if [ "$1" = "tag" ] && [ "$2" != "localhost/dimensionlab-website:latest" ]; then
+ printf 'rollback' > "$state_file"
+fi
+if [ "$1" = "inspect" ]; then
+ case "$*" in
+ *PODMAN_SYSTEMD_UNIT*)
+ echo ${systemdUnit}
+ ;;
+ *State.Running*)
+ echo true
+ ;;
+ *'.Image'*|*'{{.Image}}'*)
+ if [ "$(cat "$state_file")" = "rollback" ]; then
+ echo sha256:old
+ elif [ "\${DEPLOY_TEST_CONTAINER_IMAGE:-new}" = "wrong" ]; then
+ echo sha256:wrong
+ else
+ echo sha256:new
+ fi
+ ;;
+ esac
+fi
+`;
+}
+
+const failingCurlCommand = `#!/usr/bin/env bash
+printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
+exit 22
+`;
+
+const passingCurlCommand = `#!/usr/bin/env bash
+printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
+if [ "$*" = *'/api/dashboard/tiles'* ]; then
+ printf '{"state":"ready","tiles":[]}'
+fi
+`;
+
+function fakeSystemctlCommand(options: { active: boolean; show: boolean }): string {
+ const activeStatus = options.active ? 0 : 3;
+ const showStatus = options.show ? 0 : 1;
+
+ return `#!/usr/bin/env bash
+printf 'systemctl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
+if [ "$1" = "--user" ] && [ "$2" = "is-active" ]; then
+ exit ${activeStatus}
+fi
+if [ "$1" = "--user" ] && [ "$2" = "show" ]; then
+ exit ${showStatus}
+fi
+if [ "$1" = "--user" ] && [ "$2" = "restart" ]; then
+ exit 0
+fi
+`;
+}
diff --git a/apps/web/src/server/index.test.ts b/apps/web/src/server/index.test.ts
new file mode 100644
index 0000000..af1035d
--- /dev/null
+++ b/apps/web/src/server/index.test.ts
@@ -0,0 +1,77 @@
+import { afterAll, describe, expect, test, vi } from "vitest";
+import { handleRequest } from "./index";
+
+describe("server request routing", () => {
+ const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
+
+ afterAll(() => {
+ consoleInfo.mockRestore();
+ });
+
+ test("routes dashboard tile batch requests", async () => {
+ const response = await handleRequest(
+ new Request("https://example.test/api/dashboard/tiles", {
+ method: "POST",
+ body: JSON.stringify({
+ tiles: [
+ {
+ kind: "status",
+ stripId: "footer-status",
+ id: "auto-refresh",
+ },
+ ],
+ }),
+ }),
+ );
+
+ expect(response.status).toBe(200);
+ await expect(response.json()).resolves.toMatchObject({
+ state: "ready",
+ tiles: [
+ {
+ state: "ready",
+ tile: {
+ kind: "status",
+ stripId: "footer-status",
+ id: "auto-refresh",
+ },
+ },
+ ],
+ });
+ });
+
+ test("rejects non-post dashboard tile batch requests", async () => {
+ const response = await handleRequest(
+ new Request("https://example.test/api/dashboard/tiles", {
+ method: "GET",
+ }),
+ );
+
+ expect(response.status).toBe(405);
+ expect(response.headers.get("allow")).toBe("POST");
+ });
+
+ test("routes dashboard event stream requests", async () => {
+ const response = await handleRequest(
+ new Request("https://example.test/api/dashboard/events", {
+ method: "GET",
+ }),
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get("content-type")).toBe(
+ "text/event-stream; charset=utf-8",
+ );
+ });
+
+ test("rejects non-get dashboard event stream requests", async () => {
+ const response = await handleRequest(
+ new Request("https://example.test/api/dashboard/events", {
+ method: "POST",
+ }),
+ );
+
+ expect(response.status).toBe(405);
+ expect(response.headers.get("allow")).toBe("GET");
+ });
+});
diff --git a/apps/web/src/server/index.ts b/apps/web/src/server/index.ts
index 860d042..9cbcbd9 100644
--- a/apps/web/src/server/index.ts
+++ b/apps/web/src/server/index.ts
@@ -1,6 +1,11 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
-import { handleDashboardRoute } from "./routes/dashboard";
+import {
+ handleDashboardEventsRoute,
+ handleDashboardRoute,
+ handleDashboardTileRoute,
+ handleDashboardTilesRoute,
+} from "./routes/dashboard";
const host = process.env.HOST || "0.0.0.0";
const port = Number(process.env.PORT || 3000);
@@ -23,6 +28,21 @@ export async function handleRequest(request: Request): Promise {
return handleDashboardRoute();
}
+ if (url.pathname === "/api/dashboard/tiles") {
+ if (request.method !== "POST") return methodNotAllowed(["POST"]);
+ return handleDashboardTilesRoute(request);
+ }
+
+ if (url.pathname === "/api/dashboard/events") {
+ if (request.method !== "GET") return methodNotAllowed(["GET"]);
+ return handleDashboardEventsRoute({ signal: request.signal });
+ }
+
+ if (url.pathname.startsWith("/api/dashboard/tile/")) {
+ if (request.method !== "GET") return methodNotAllowed(["GET"]);
+ return handleDashboardTileRoute(url.pathname);
+ }
+
if (url.pathname === "/api/agent/dashboard") {
if (request.method !== "POST") return methodNotAllowed(["POST"]);
return handleAgentDashboardRoute(request);
diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts
index 21ad58d..a4cf7ef 100644
--- a/apps/web/src/server/routes/dashboard.test.ts
+++ b/apps/web/src/server/routes/dashboard.test.ts
@@ -1,11 +1,34 @@
-import { describe, expect, test } from "vitest";
+import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
-import { loadDashboardResponse } from "./dashboard";
+import {
+ createDashboardTileCache,
+ dashboardTileCacheKey,
+ handleDashboardEventsRoute,
+ handleDashboardTilesRoute,
+ handleDashboardTileRoute,
+ loadDashboardResponse,
+ loadDashboardTilesResponse,
+ loadDashboardTileResponse,
+ type DashboardTileResolutionLogEvent,
+} from "./dashboard";
describe("dashboard API route", () => {
- test("returns ready dashboard runtime state from the existing model loader", async () => {
+ const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
+
+ afterEach(() => {
+ consoleInfo.mockClear();
+ });
+
+ afterAll(() => {
+ consoleInfo.mockRestore();
+ });
+
+ test("returns the ready dashboard shell without hydrating live datasources", async () => {
+ const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
+ new Error("live datasource fetch should not run for the shell response"),
+ );
+
const response = await loadDashboardResponse({
- disableLiveDatasources: true,
refreshSeedDocument: true,
seedIfEmpty: true,
});
@@ -15,5 +38,762 @@ describe("dashboard API route", () => {
expect(response.document.metadata.title).toBe(
dimensionLabDashboardFixture.metadata.title,
);
+ expect(fetch).not.toHaveBeenCalled();
+
+ fetch.mockRestore();
+ });
+
+ test("reports when client-side live hydration is disabled", async () => {
+ const previous = process.env.DISABLE_LIVE_DATASOURCES;
+ process.env.DISABLE_LIVE_DATASOURCES = "1";
+
+ try {
+ const response = await loadDashboardResponse({
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ });
+
+ expect(response.state).toBe("ready");
+ if (response.state !== "ready") throw new Error("expected ready dashboard");
+ expect(response.liveDatasourceHydration).toEqual({ enabled: false });
+ } finally {
+ if (previous === undefined) {
+ delete process.env.DISABLE_LIVE_DATASOURCES;
+ } else {
+ process.env.DISABLE_LIVE_DATASOURCES = previous;
+ }
+ }
+ });
+
+ test("hydrates a telemetry tile independently from the dashboard shell", async () => {
+ const fetch = vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+
+ if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
+ return jsonResponse({
+ status: "success",
+ data: {
+ result: [
+ {
+ metric: { host: "linux-infra" },
+ values: [
+ [1771430000, "10"],
+ [1771430060, "20"],
+ [1771430120, "42"],
+ ],
+ },
+ ],
+ },
+ });
+ }
+
+ if (url.startsWith("https://prometheus.example/api/v1/query")) {
+ return jsonResponse({
+ status: "success",
+ data: {
+ result: [
+ {
+ metric: { host: "linux-infra" },
+ value: [1771430400, "42"],
+ },
+ ],
+ },
+ });
+ }
+
+ throw new Error(`Unhandled test request: ${url}`);
+ });
+
+ const response = await loadDashboardTileResponse(
+ { kind: "telemetry", id: "infra-ram" },
+ {
+ fetch,
+ prometheusBaseUrl: "https://prometheus.example",
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ },
+ );
+
+ expect(response.state).toBe("ready");
+ if (response.state !== "ready") throw new Error("expected ready tile");
+ expect(response.tile).toEqual({ kind: "telemetry", id: "infra-ram" });
+ expect(response.item).toMatchObject({
+ id: "infra-ram",
+ value: { kind: "percent", value: 42 },
+ severity: "ok",
+ detail: "linux-infra",
+ sparkline: [10, 20, 42],
+ });
+ expect(fetch).toHaveBeenCalledWith(
+ expect.stringContaining("/api/v1/query?"),
+ expect.objectContaining({ cache: "no-store" }),
+ );
+ expect(fetch).toHaveBeenCalledWith(
+ expect.stringContaining("/api/v1/query_range?"),
+ expect.objectContaining({ cache: "no-store" }),
+ );
+ });
+
+ test("hydrates a service tile with its service group identity", async () => {
+ const fetch = vi.fn(async () =>
+ jsonResponse({
+ status: "UP",
+ ping: 42,
+ }),
+ );
+
+ const response = await loadDashboardTileResponse(
+ { kind: "service", groupId: "essentials", id: "vaultwarden" },
+ {
+ fetch,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ },
+ );
+
+ expect(response.state).toBe("ready");
+ if (response.state !== "ready") throw new Error("expected ready tile");
+ expect(response.tile).toEqual({
+ kind: "service",
+ groupId: "essentials",
+ id: "vaultwarden",
+ });
+ expect(response.item).toMatchObject({
+ id: "vaultwarden",
+ severity: "ok",
+ detail: "42 ms",
+ });
+ });
+
+ test("caches ready tile responses until the tile ttl expires", async () => {
+ const cache = createDashboardTileCache();
+ let now = 1_000;
+ const fetch = vi.fn(async () => {
+ now += 7;
+ return jsonResponse({
+ status: "UP",
+ ping: 42,
+ });
+ });
+ const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
+
+ const first = await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ const second = await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now + 29_999,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ now += 30_001;
+ const third = await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+
+ expect(first).toEqual(second);
+ expect(third.state).toBe("ready");
+ expect(fetch).toHaveBeenCalledTimes(2);
+ });
+
+ test("logs tile duration and cache hit or miss metadata", async () => {
+ let now = 1_000;
+ const logs: DashboardTileResolutionLogEvent[] = [];
+ const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
+ const service = dimensionLabDashboardFixture.serviceGroups
+ .flatMap((group) => group.services)
+ .find((item) => item.id === tile.id);
+ if (!service) throw new Error("missing service fixture");
+
+ let cacheCalls = 0;
+ const tileCache = {
+ async resolve() {
+ cacheCalls += 1;
+ if (cacheCalls === 1) now += 7;
+ const cacheState = cacheCalls === 1 ? "miss" as const : "hit" as const;
+
+ return {
+ cache: cacheState,
+ coalesced: false,
+ response: {
+ state: "ready" as const,
+ tile,
+ item: service,
+ },
+ };
+ },
+ };
+
+ await loadDashboardTileResponse(tile, {
+ logTileResolution: (event) => logs.push(event),
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache,
+ });
+ now += 10;
+ await loadDashboardTileResponse(tile, {
+ logTileResolution: (event) => logs.push(event),
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache,
+ });
+
+ expect(logs).toEqual([
+ expect.objectContaining({
+ cache: "miss",
+ coalesced: false,
+ durationMs: 7,
+ status: "ready",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ expect.objectContaining({
+ cache: "hit",
+ coalesced: false,
+ durationMs: 0,
+ status: "ready",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ ]);
+ });
+
+ test("keeps telemetry tiles cached for fifteen seconds", async () => {
+ const cache = createDashboardTileCache();
+ let now = 1_000;
+ const fetch = telemetryFetch();
+ const tile = { kind: "telemetry", id: "infra-ram" } as const;
+
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ prometheusBaseUrl: "https://prometheus.example",
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now + 14_999,
+ prometheusBaseUrl: "https://prometheus.example",
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ now += 15_001;
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ prometheusBaseUrl: "https://prometheus.example",
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+
+ expect(fetch).toHaveBeenCalledTimes(4);
+ });
+
+ test("keeps weather module tiles cached for ten minutes", async () => {
+ const cache = createDashboardTileCache();
+ let now = 1_000;
+ const fetch = vi.fn(async () =>
+ jsonResponse({
+ current: {
+ apparent_temperature: 19,
+ temperature_2m: 20,
+ weather_code: 0,
+ wind_speed_10m: 11,
+ },
+ }),
+ );
+ const tile = { kind: "module", id: "weather-amsterdam" } as const;
+
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now + 599_999,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ now += 600_001;
+ await loadDashboardTileResponse(tile, {
+ fetch,
+ now: () => now,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+
+ expect(fetch).toHaveBeenCalledTimes(2);
+ });
+
+ test("coalesces concurrent tile requests for the same cache key", async () => {
+ const cache = createDashboardTileCache();
+ const logs: DashboardTileResolutionLogEvent[] = [];
+ let resolveFetch: ((response: Response) => void) | undefined;
+ const fetch = vi.fn(() =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ })
+ );
+ const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
+
+ const first = loadDashboardTileResponse(tile, {
+ fetch,
+ logTileResolution: (event) => logs.push(event),
+ now: () => 1_000,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+ const second = loadDashboardTileResponse(tile, {
+ fetch,
+ logTileResolution: (event) => logs.push(event),
+ now: () => 1_000,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ });
+
+ await Promise.resolve();
+ expect(fetch).toHaveBeenCalledTimes(1);
+
+ resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
+ expect(await first).toEqual(await second);
+ expect(logs).toEqual([
+ expect.objectContaining({
+ cache: "miss",
+ coalesced: false,
+ status: "ready",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ expect.objectContaining({
+ cache: "miss",
+ coalesced: true,
+ status: "ready",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ ]);
+ });
+
+ test("logs coalesced metadata when shared tile requests fail", async () => {
+ const cache = createDashboardTileCache();
+ let rejectLoad: ((error: Error) => void) | undefined;
+ const load = vi.fn(() =>
+ new Promise((_resolve, reject) => {
+ rejectLoad = reject;
+ })
+ );
+
+ const first = cache.resolve("tile-a", 30_000, 1_000, load);
+ const second = cache.resolve("tile-a", 30_000, 1_000, load);
+
+ await Promise.resolve();
+ expect(load).toHaveBeenCalledTimes(1);
+
+ rejectLoad?.(new TypeError("upstream failed"));
+ await expect(first).rejects.toThrow("upstream failed");
+ await expect(second).rejects.toMatchObject({
+ cache: "miss",
+ coalesced: true,
+ cause: expect.any(TypeError),
+ });
+ });
+
+ test("logs coalesced metadata for failed tile cache resolution", async () => {
+ const logs: DashboardTileResolutionLogEvent[] = [];
+ const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
+
+ await expect(
+ loadDashboardTileResponse(tile, {
+ logTileResolution: (event) => logs.push(event),
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: {
+ async resolve() {
+ throw {
+ cache: "miss",
+ cause: new TypeError("coalesced cache failed"),
+ coalesced: true,
+ };
+ },
+ },
+ }),
+ ).rejects.toThrow("coalesced cache failed");
+
+ expect(logs).toEqual([
+ expect.objectContaining({
+ cache: "miss",
+ coalesced: true,
+ errorCategory: "TypeError",
+ status: "error",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ ]);
+ });
+
+ test("logs error categories for failed tile cache resolution", async () => {
+ const logs: DashboardTileResolutionLogEvent[] = [];
+ const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
+
+ await expect(
+ loadDashboardTileResponse(tile, {
+ logTileResolution: (event) => logs.push(event),
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: {
+ async resolve() {
+ throw new TypeError("cache failed");
+ },
+ },
+ }),
+ ).rejects.toThrow("cache failed");
+
+ expect(logs).toEqual([
+ expect.objectContaining({
+ cache: "miss",
+ coalesced: false,
+ errorCategory: "TypeError",
+ status: "error",
+ tileKey: dashboardTileCacheKey(tile),
+ }),
+ ]);
+ });
+
+ test("uses structured tile cache keys when identifiers contain delimiters", () => {
+ expect(
+ dashboardTileCacheKey({ kind: "service", groupId: "a:b", id: "c" }),
+ ).not.toBe(
+ dashboardTileCacheKey({ kind: "service", groupId: "a", id: "b:c" }),
+ );
+ expect(
+ dashboardTileCacheKey({ kind: "status", stripId: "a:b", id: "c" }),
+ ).not.toBe(
+ dashboardTileCacheKey({ kind: "status", stripId: "a", id: "b:c" }),
+ );
+ });
+
+ test("uses thirty-second status aggregate and five-minute static status ttl buckets", async () => {
+ const cache = createDashboardTileCache();
+ const fetch = vi.fn(async () => jsonResponse({ status: "UP", ping: 42 }));
+ const serviceCheckCount = dimensionLabDashboardFixture.serviceGroups
+ .flatMap((group) => group.services).length;
+
+ await loadDashboardTileResponse(
+ { kind: "status", stripId: "footer-status", id: "system-status" },
+ {
+ fetch,
+ now: () => 1_000,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ },
+ );
+ await loadDashboardTileResponse(
+ { kind: "status", stripId: "footer-status", id: "system-status" },
+ {
+ fetch,
+ now: () => 31_001,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ },
+ );
+
+ await loadDashboardTileResponse(
+ { kind: "status", stripId: "footer-status", id: "auto-refresh" },
+ {
+ fetch,
+ now: () => 1_000,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ },
+ );
+ await loadDashboardTileResponse(
+ { kind: "status", stripId: "footer-status", id: "auto-refresh" },
+ {
+ fetch,
+ now: () => 300_999,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ },
+ );
+ await loadDashboardTileResponse(
+ { kind: "status", stripId: "footer-status", id: "auto-refresh" },
+ {
+ fetch,
+ now: () => 301_001,
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: cache,
+ },
+ );
+
+ expect(fetch).toHaveBeenCalledTimes(serviceCheckCount * 2);
+ });
+
+ test("serves tile route responses with short private cache headers", async () => {
+ const response = await handleDashboardTileRoute(
+ "/api/dashboard/tile/service/essentials/vaultwarden",
+ {
+ fetch: vi.fn(async () => jsonResponse({ status: "UP", ping: 42 })),
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: createDashboardTileCache(),
+ },
+ );
+
+ expect(response.headers.get("cache-control")).toBe(
+ "private, max-age=5, stale-while-revalidate=30",
+ );
+ });
+
+ test("serves batch tile route responses", async () => {
+ const response = await handleDashboardTilesRoute(
+ new Request("https://example.test/api/dashboard/tiles", {
+ method: "POST",
+ body: JSON.stringify({
+ tiles: [
+ {
+ kind: "status",
+ stripId: "footer-status",
+ id: "auto-refresh",
+ },
+ ],
+ }),
+ }),
+ {
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: createDashboardTileCache(),
+ },
+ );
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get("cache-control")).toBe(
+ "private, max-age=5, stale-while-revalidate=30",
+ );
+ await expect(response.json()).resolves.toMatchObject({
+ state: "ready",
+ tiles: [
+ {
+ state: "ready",
+ tile: {
+ kind: "status",
+ stripId: "footer-status",
+ id: "auto-refresh",
+ },
+ },
+ ],
+ });
+ });
+
+ test("streams ready dashboard tile events", async () => {
+ const controller = new AbortController();
+ const response = await handleDashboardEventsRoute({
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ signal: controller.signal,
+ tileCache: {
+ async resolve(key) {
+ controller.abort();
+ const tile = JSON.parse(key);
+
+ return {
+ cache: "miss",
+ coalesced: false,
+ response: {
+ state: "ready",
+ tile,
+ item: {
+ id: tile.id,
+ label: "Status",
+ severity: "ok",
+ value: "ok",
+ },
+ },
+ };
+ },
+ },
+ });
+
+ expect(response.headers.get("content-type")).toBe(
+ "text/event-stream; charset=utf-8",
+ );
+ expect(response.headers.get("cache-control")).toBe("no-cache");
+ const body = await response.text();
+ expect(body).toContain("event: dashboard-tile");
+ expect(body).toContain('"state":"ready"');
+ expect(body).toContain('"tile"');
+ });
+
+ test("rejects invalid batch tile requests", async () => {
+ const response = await handleDashboardTilesRoute(
+ new Request("https://example.test/api/dashboard/tiles", {
+ method: "POST",
+ body: JSON.stringify({
+ tiles: [{ kind: "service", id: "missing-group" }],
+ }),
+ }),
+ );
+
+ expect(response.status).toBe(400);
+ });
+
+ test("resolves batch tile responses with server concurrency capped at six", async () => {
+ let active = 0;
+ let maxActive = 0;
+ const started: string[] = [];
+ const releases = new Map void>();
+ const service = dimensionLabDashboardFixture.serviceGroups
+ .flatMap((group) => group.services)[0];
+ if (!service) throw new Error("missing service fixture");
+ const tiles = Array.from({ length: 7 }, (_, index) => ({
+ kind: "service" as const,
+ groupId: "essentials",
+ id: `service-${index}`,
+ }));
+
+ const batch = loadDashboardTilesResponse(tiles, {
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ tileCache: {
+ async resolve(key) {
+ active += 1;
+ maxActive = Math.max(maxActive, active);
+ started.push(key);
+ await new Promise((resolve) => releases.set(key, resolve));
+ active -= 1;
+
+ return {
+ cache: "miss",
+ coalesced: false,
+ response: {
+ state: "ready",
+ tile: JSON.parse(key),
+ item: service,
+ },
+ };
+ },
+ },
+ });
+
+ await waitFor(() => started.length === 6);
+ expect(maxActive).toBe(6);
+ releases.get(started[0])?.();
+ await waitFor(() => started.length === 7);
+ for (const release of releases.values()) release();
+
+ await expect(batch).resolves.toMatchObject({
+ state: "ready",
+ tiles: expect.arrayContaining([
+ expect.objectContaining({
+ state: "ready",
+ tile: tiles[0],
+ }),
+ ]),
+ });
+ expect(maxActive).toBe(6);
+ });
+
+ test("does not hydrate tile routes when live datasources are disabled", async () => {
+ const previous = process.env.DISABLE_LIVE_DATASOURCES;
+ process.env.DISABLE_LIVE_DATASOURCES = "1";
+ const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
+ new Error("live datasource fetch should not run when disabled"),
+ );
+
+ try {
+ const response = await loadDashboardTileResponse(
+ { kind: "telemetry", id: "infra-ram" },
+ {
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ },
+ );
+
+ expect(response.state).toBe("disabled");
+ expect(fetch).not.toHaveBeenCalled();
+ } finally {
+ fetch.mockRestore();
+ if (previous === undefined) {
+ delete process.env.DISABLE_LIVE_DATASOURCES;
+ } else {
+ process.env.DISABLE_LIVE_DATASOURCES = previous;
+ }
+ }
});
});
+
+function jsonResponse(payload: unknown): Response {
+ return new Response(JSON.stringify(payload), {
+ headers: { "content-type": "application/json" },
+ });
+}
+
+async function waitFor(predicate: () => boolean) {
+ for (let attempt = 0; attempt < 20; attempt += 1) {
+ if (predicate()) return;
+ await Promise.resolve();
+ }
+
+ throw new Error("condition was not met");
+}
+
+function telemetryFetch() {
+ return vi.fn(async (input: RequestInfo | URL) => {
+ const url = String(input);
+
+ if (url.startsWith("https://prometheus.example/api/v1/query_range")) {
+ return jsonResponse({
+ status: "success",
+ data: {
+ result: [
+ {
+ metric: { host: "linux-infra" },
+ values: [
+ [1771430000, "10"],
+ [1771430060, "20"],
+ [1771430120, "42"],
+ ],
+ },
+ ],
+ },
+ });
+ }
+
+ if (url.startsWith("https://prometheus.example/api/v1/query")) {
+ return jsonResponse({
+ status: "success",
+ data: {
+ result: [
+ {
+ metric: { host: "linux-infra" },
+ value: [1771430400, "42"],
+ },
+ ],
+ },
+ });
+ }
+
+ throw new Error(`Unhandled test request: ${url}`);
+ });
+}
diff --git a/apps/web/src/server/routes/dashboard.ts b/apps/web/src/server/routes/dashboard.ts
index 21c30e4..c42227c 100644
--- a/apps/web/src/server/routes/dashboard.ts
+++ b/apps/web/src/server/routes/dashboard.ts
@@ -1,21 +1,138 @@
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import {
loadDashboardRuntime,
type DashboardRuntimeOptions,
type DashboardRuntimeState,
} from "$lib/server/dashboard";
-import { resolveDashboardDatasources } from "$lib/server/datasources";
+import {
+ resolveDashboardDatasources,
+ resolveDashboardTile,
+ type DashboardTileReference,
+ type DashboardTileResolution,
+ type DatasourceResolutionOptions,
+} from "$lib/server/datasources";
export interface LoadDashboardResponseOptions
extends Pick<
DashboardRuntimeOptions,
"refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
- > {
+ >,
+ DatasourceResolutionOptions {
disableLiveDatasources?: boolean;
+ hydrateLiveDatasources?: boolean;
+ logTileResolution?: (event: DashboardTileResolutionLogEvent) => void;
+ now?: () => number;
+ tileCache?: DashboardTileCache;
+}
+
+export interface DashboardEventsRouteOptions extends LoadDashboardResponseOptions {
+ signal?: AbortSignal;
+}
+
+interface DashboardTileCacheEntry {
+ expiresAt: number;
+ response: DashboardTileResolution;
+}
+
+export interface DashboardTileCache {
+ resolve(
+ key: string,
+ ttlMs: number,
+ now: number,
+ load: () => Promise,
+ ): Promise;
+}
+
+const dashboardTileResponseHeaders = {
+ "Cache-Control": "private, max-age=5, stale-while-revalidate=30",
+};
+
+const dashboardBatchTileConcurrency = 6;
+
+const defaultDashboardTileCache = createDashboardTileCache();
+
+interface DashboardTileCacheResult {
+ cache: "hit" | "miss";
+ coalesced: boolean;
+ response: DashboardTileResolution;
+}
+
+export interface DashboardTileResolutionLogEvent {
+ cache: "bypass" | "hit" | "miss";
+ coalesced: boolean;
+ durationMs: number;
+ errorCategory?: string;
+ status: DashboardTileResolution["state"] | "error";
+ tileKey: string;
+}
+
+export interface DashboardTilesBatchResponse {
+ state: "ready";
+ tiles: DashboardTileResolution[];
+}
+
+export function createDashboardTileCache(): DashboardTileCache {
+ const entries = new Map();
+ const inFlight = new Map>();
+
+ return {
+ async resolve(key, ttlMs, now, load) {
+ const cached = entries.get(key);
+ if (cached && cached.expiresAt > now) {
+ return {
+ cache: "hit",
+ coalesced: false,
+ response: cached.response,
+ };
+ }
+
+ const active = inFlight.get(key);
+ if (active) {
+ return active
+ .then((response) => ({
+ cache: "miss" as const,
+ coalesced: true,
+ response,
+ }))
+ .catch((error) => {
+ throw new DashboardTileCacheResolutionError(error, {
+ cache: "miss",
+ coalesced: true,
+ });
+ });
+ }
+
+ const request = load()
+ .then((response) => {
+ if (response.state === "ready") {
+ entries.set(key, {
+ expiresAt: now + ttlMs,
+ response,
+ });
+ }
+
+ return response;
+ })
+ .finally(() => {
+ inFlight.delete(key);
+ });
+
+ inFlight.set(key, request);
+ return request.then((response) => ({
+ cache: "miss" as const,
+ coalesced: false,
+ response,
+ }));
+ },
+ };
}
export async function loadDashboardResponse(
options: LoadDashboardResponseOptions = {},
): Promise {
+ const liveHydrationEnabled =
+ !options.disableLiveDatasources &&
+ process.env.DISABLE_LIVE_DATASOURCES !== "1";
const dashboard = loadDashboardRuntime(undefined, {
refreshSeedDocument: options.refreshSeedDocument ?? true,
seedIfEmpty: options.seedIfEmpty ?? true,
@@ -26,16 +143,467 @@ export async function loadDashboardResponse(
return dashboard;
}
- if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
- return dashboard;
+ if (
+ !options.hydrateLiveDatasources ||
+ options.disableLiveDatasources ||
+ process.env.DISABLE_LIVE_DATASOURCES === "1"
+ ) {
+ return {
+ ...dashboard,
+ liveDatasourceHydration: {
+ enabled: liveHydrationEnabled,
+ },
+ };
}
return {
...dashboard,
- document: await resolveDashboardDatasources(dashboard.document),
+ document: await resolveDashboardDatasources(dashboard.document, options),
+ liveDatasourceHydration: {
+ enabled: false,
+ },
+ };
+}
+
+export async function loadDashboardTileResponse(
+ tile: DashboardTileReference,
+ options: LoadDashboardResponseOptions = {},
+): Promise {
+ if (
+ options.disableLiveDatasources ||
+ process.env.DISABLE_LIVE_DATASOURCES === "1"
+ ) {
+ const tileKey = dashboardTileCacheKey(tile);
+ const startedAt = options.now?.() ?? Date.now();
+ const response = {
+ state: "disabled",
+ tile,
+ message: "Live datasource hydration is disabled.",
+ } satisfies DashboardTileResolution;
+ logDashboardTileResolution(options, {
+ cache: "bypass",
+ coalesced: false,
+ durationMs: elapsedDashboardTileMs(startedAt, options),
+ status: response.state,
+ tileKey,
+ });
+ return response;
+ }
+
+ const tileKey = dashboardTileCacheKey(tile);
+ const startedAt = options.now?.() ?? Date.now();
+
+ const dashboard = loadDashboardRuntime(undefined, {
+ refreshSeedDocument: options.refreshSeedDocument ?? true,
+ seedIfEmpty: options.seedIfEmpty ?? true,
+ seedDocument: options.seedDocument,
+ });
+
+ if (dashboard.state !== "ready") {
+ const response = {
+ state: "not_found",
+ tile,
+ message: `Dashboard is not ready: ${dashboard.state}`,
+ } satisfies DashboardTileResolution;
+ logDashboardTileResolution(options, {
+ cache: "bypass",
+ coalesced: false,
+ durationMs: elapsedDashboardTileMs(startedAt, options),
+ status: response.state,
+ tileKey,
+ });
+ return response;
+ }
+
+ const cache = options.tileCache || defaultDashboardTileCache;
+ const now = options.now?.() ?? Date.now();
+
+ try {
+ const result = await cache.resolve(
+ tileKey,
+ dashboardTileTtlMs(dashboard.document, tile),
+ now,
+ () => resolveDashboardTile(dashboard.document, tile, options),
+ );
+ logDashboardTileResolution(options, {
+ cache: result.cache,
+ coalesced: result.coalesced,
+ durationMs: elapsedDashboardTileMs(startedAt, options),
+ status: result.response.state,
+ tileKey,
+ });
+ return result.response;
+ } catch (error) {
+ const cacheError = dashboardTileCacheResolutionError(error);
+ logDashboardTileResolution(options, {
+ cache: cacheError?.cache ?? "miss",
+ coalesced: cacheError?.coalesced ?? false,
+ durationMs: elapsedDashboardTileMs(startedAt, options),
+ errorCategory: dashboardTileErrorCategory(cacheError?.cause ?? error),
+ status: "error",
+ tileKey,
+ });
+ throw cacheError?.cause ?? error;
+ }
+}
+
+export async function loadDashboardTilesResponse(
+ tiles: DashboardTileReference[],
+ options: LoadDashboardResponseOptions = {},
+): Promise {
+ return {
+ state: "ready",
+ tiles: await resolveDashboardTilesBatch(tiles, options),
};
}
export async function handleDashboardRoute(): Promise {
return Response.json(await loadDashboardResponse());
}
+
+export async function handleDashboardTileRoute(
+ pathname: string,
+ options: LoadDashboardResponseOptions = {},
+): Promise {
+ const tile = parseDashboardTilePath(pathname);
+ if (!tile) {
+ return Response.json(
+ { ok: false, message: "Invalid dashboard tile route" },
+ { status: 404 },
+ );
+ }
+
+ const response = await loadDashboardTileResponse(tile, options);
+ return Response.json(response, {
+ status: response.state === "not_found" ? 404 : 200,
+ headers: dashboardTileResponseHeaders,
+ });
+}
+
+export async function handleDashboardTilesRoute(
+ request: Request,
+ options: LoadDashboardResponseOptions = {},
+): Promise {
+ const tiles = await parseDashboardTilesBatchRequest(request);
+ if (!tiles) {
+ return Response.json(
+ { ok: false, message: "Invalid dashboard tiles request" },
+ { status: 400 },
+ );
+ }
+
+ return Response.json(await loadDashboardTilesResponse(tiles, options), {
+ headers: dashboardTileResponseHeaders,
+ });
+}
+
+export async function handleDashboardEventsRoute(
+ options: DashboardEventsRouteOptions = {},
+): Promise {
+ const stream = new ReadableStream({
+ async start(controller) {
+ const encoder = new TextEncoder();
+ const dashboard = await loadDashboardResponse({
+ ...options,
+ hydrateLiveDatasources: false,
+ });
+
+ if (dashboard.state !== "ready" || options.signal?.aborted) {
+ controller.close();
+ return;
+ }
+
+ try {
+ for (const tile of dashboardEventTiles(dashboard.document)) {
+ if (options.signal?.aborted) break;
+ const resolution = await loadDashboardTileResponse(tile, options);
+ if (resolution.state === "ready") {
+ controller.enqueue(
+ encoder.encode(dashboardTileEventChunk(resolution)),
+ );
+ }
+ }
+ } finally {
+ controller.close();
+ }
+ },
+ });
+
+ return new Response(stream, {
+ headers: {
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "Content-Type": "text/event-stream; charset=utf-8",
+ "X-Accel-Buffering": "no",
+ },
+ });
+}
+
+export function dashboardTileCacheKey(tile: DashboardTileReference): string {
+ return JSON.stringify(tile);
+}
+
+function logDashboardTileResolution(
+ options: LoadDashboardResponseOptions,
+ event: DashboardTileResolutionLogEvent,
+): void {
+ if (options.logTileResolution) {
+ options.logTileResolution(event);
+ return;
+ }
+
+ console.info("dashboard.tile", event);
+}
+
+function elapsedDashboardTileMs(
+ startedAt: number,
+ options: LoadDashboardResponseOptions,
+): number {
+ const now = options.now?.() ?? Date.now();
+ return Math.max(0, now - startedAt);
+}
+
+function dashboardTileErrorCategory(error: unknown): string {
+ if (
+ typeof error === "object" &&
+ error !== null &&
+ "name" in error &&
+ typeof error.name === "string"
+ ) {
+ return error.name;
+ }
+
+ return "unknown";
+}
+
+async function resolveDashboardTilesBatch(
+ tiles: DashboardTileReference[],
+ options: LoadDashboardResponseOptions,
+): Promise {
+ const results: DashboardTileResolution[] = new Array(tiles.length);
+ let nextIndex = 0;
+
+ async function worker() {
+ while (nextIndex < tiles.length) {
+ const index = nextIndex;
+ nextIndex += 1;
+ results[index] = await loadDashboardTileResponse(tiles[index], options);
+ }
+ }
+
+ await Promise.all(
+ Array.from(
+ { length: Math.min(dashboardBatchTileConcurrency, tiles.length) },
+ () => worker(),
+ ),
+ );
+
+ return results;
+}
+
+async function parseDashboardTilesBatchRequest(
+ request: Request,
+): Promise {
+ try {
+ const body = await request.json() as unknown;
+ if (
+ typeof body !== "object" ||
+ body === null ||
+ !("tiles" in body) ||
+ !Array.isArray(body.tiles)
+ ) {
+ return null;
+ }
+
+ const tiles = body.tiles.map(parseDashboardTileReference);
+ return tiles.every((tile): tile is DashboardTileReference => tile !== null)
+ ? tiles
+ : null;
+ } catch {
+ return null;
+ }
+}
+
+function parseDashboardTileReference(value: unknown): DashboardTileReference | null {
+ if (typeof value !== "object" || value === null || !("kind" in value)) {
+ return null;
+ }
+
+ if (
+ (value.kind === "telemetry" || value.kind === "module") &&
+ "id" in value &&
+ typeof value.id === "string"
+ ) {
+ return { kind: value.kind, id: value.id };
+ }
+
+ if (
+ value.kind === "service" &&
+ "groupId" in value &&
+ typeof value.groupId === "string" &&
+ "id" in value &&
+ typeof value.id === "string"
+ ) {
+ return {
+ kind: "service",
+ groupId: value.groupId,
+ id: value.id,
+ };
+ }
+
+ if (
+ value.kind === "status" &&
+ "stripId" in value &&
+ typeof value.stripId === "string" &&
+ "id" in value &&
+ typeof value.id === "string"
+ ) {
+ return {
+ kind: "status",
+ stripId: value.stripId,
+ id: value.id,
+ };
+ }
+
+ return null;
+}
+
+function dashboardEventTiles(document: DashboardDocument): DashboardTileReference[] {
+ const status = document.statusStrips.flatMap((strip) =>
+ strip.items.map((item): DashboardTileReference => ({
+ kind: "status",
+ stripId: strip.id,
+ id: item.id,
+ })),
+ );
+ const telemetry = document.telemetry
+ .filter((card) => card.datasource?.type === "external")
+ .map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
+ const modules = (document.modules || [])
+ .filter((module) =>
+ module.datasource?.type === "external" ||
+ module.id === "runtime-health-summary"
+ )
+ .map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
+ const services = document.serviceGroups.flatMap((group) =>
+ group.services
+ .filter((service) => service.datasource?.type === "external")
+ .map((service): DashboardTileReference => ({
+ kind: "service",
+ groupId: group.id,
+ id: service.id,
+ })),
+ );
+
+ return [...status, ...telemetry, ...modules, ...services];
+}
+
+function dashboardTileEventChunk(resolution: DashboardTileResolution): string {
+ return [
+ "event: dashboard-tile",
+ `data: ${JSON.stringify(resolution)}`,
+ "",
+ "",
+ ].join("\n");
+}
+
+class DashboardTileCacheResolutionError extends Error {
+ readonly cache: "hit" | "miss";
+ readonly coalesced: boolean;
+ override readonly cause: unknown;
+
+ constructor(
+ cause: unknown,
+ metadata: Pick,
+ ) {
+ super("Dashboard tile cache resolution failed");
+ this.name = "DashboardTileCacheResolutionError";
+ this.cause = cause;
+ this.cache = metadata.cache;
+ this.coalesced = metadata.coalesced;
+ }
+}
+
+function dashboardTileCacheResolutionError(
+ error: unknown,
+): DashboardTileCacheResolutionFailure | null {
+ if (
+ typeof error === "object" &&
+ error !== null &&
+ "cache" in error &&
+ (error.cache === "hit" || error.cache === "miss") &&
+ "coalesced" in error &&
+ typeof error.coalesced === "boolean" &&
+ "cause" in error
+ ) {
+ return {
+ cache: error.cache,
+ cause: error.cause,
+ coalesced: error.coalesced,
+ };
+ }
+
+ return null;
+}
+
+interface DashboardTileCacheResolutionFailure {
+ cache: "hit" | "miss";
+ cause: unknown;
+ coalesced: boolean;
+}
+
+function dashboardTileTtlMs(
+ document: DashboardDocument,
+ tile: DashboardTileReference,
+): number {
+ if (tile.kind === "telemetry") return 15_000;
+ if (tile.kind === "service") return 30_000;
+
+ if (tile.kind === "module") {
+ const module = document.modules?.find((item) => item.id === tile.id);
+ if (
+ module?.datasource?.type === "external" &&
+ module.datasource.adapter === "weather"
+ ) {
+ return 10 * 60_000;
+ }
+ return 30_000;
+ }
+
+ if (["system-status", "uptime", "load-avg"].includes(tile.id)) {
+ return 30_000;
+ }
+
+ return 5 * 60_000;
+}
+
+function parseDashboardTilePath(pathname: string): DashboardTileReference | null {
+ const parts = pathname.split("/").filter(Boolean);
+ const [, dashboard, tileRoot, kind, firstId, secondId] = parts;
+ if (dashboard !== "dashboard" || tileRoot !== "tile" || !kind || !firstId) {
+ return null;
+ }
+
+ const id = decodeURIComponent(firstId);
+ if (kind === "telemetry" || kind === "module") {
+ return { kind, id };
+ }
+
+ if (kind === "service" && secondId) {
+ return {
+ kind,
+ groupId: id,
+ id: decodeURIComponent(secondId),
+ };
+ }
+
+ if (kind === "status" && secondId) {
+ return {
+ kind,
+ stripId: id,
+ id: decodeURIComponent(secondId),
+ };
+ }
+
+ return null;
+}
diff --git a/apps/web/tests/e2e/dashboard.spec.ts b/apps/web/tests/e2e/dashboard.spec.ts
index 48eac8b..3eb0944 100644
--- a/apps/web/tests/e2e/dashboard.spec.ts
+++ b/apps/web/tests/e2e/dashboard.spec.ts
@@ -16,7 +16,6 @@ const linkedServiceIds = [
"open-webui",
"comfyui",
"models",
- "prompt-registry",
"adminer",
"assistant",
"suna",
@@ -104,7 +103,7 @@ test.describe("dashboard page QA gate", () => {
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
expect(metrics.telemetryCardCount).toBe(16);
- expect(metrics.serviceRowCount).toBe(28);
+ expect(metrics.serviceRowCount).toBe(27);
expect(metrics.footerCellCount).toBe(5);
expect(metrics.clippedItems).toEqual([]);
});
diff --git a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
index c548a8a..a1c0ada 100644
Binary files a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png and b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png differ
diff --git a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png
index a007213..f5d5163 100644
Binary files a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png and b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-light-desktop-chromium-desktop-linux.png differ
diff --git a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png
index 0542e73..48293a4 100644
Binary files a/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png and b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png differ
diff --git a/scripts/deploy-dimensionlab-website.sh b/scripts/deploy-dimensionlab-website.sh
new file mode 100755
index 0000000..4fac4cd
--- /dev/null
+++ b/scripts/deploy-dimensionlab-website.sh
@@ -0,0 +1,362 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+APP_NAME="${APP_NAME:-dimensionlab-website}"
+SERVICE_NAME="${SERVICE_NAME:-dimensionlab-website.service}"
+CONTAINER_NAME="${CONTAINER_NAME:-dimensionlab-website}"
+IMAGE_REPO="${IMAGE_REPO:-localhost/dimensionlab-website}"
+CONTAINERFILE="${CONTAINERFILE:-apps/web/Containerfile}"
+PUBLIC_URL="${PUBLIC_URL:-https://dimensionlab.net/}"
+TILE_BATCH_URL="${TILE_BATCH_URL:-https://dimensionlab.net/api/dashboard/tiles}"
+DEPLOY_RESTART_STRATEGY="${DEPLOY_RESTART_STRATEGY:-auto}"
+DEPLOY_SMOKE_TIMEOUT_SECONDS="${DEPLOY_SMOKE_TIMEOUT_SECONDS:-120}"
+DEPLOY_CONTAINER_START_TIMEOUT_SECONDS="${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS:-90}"
+
+dry_run=false
+rollback_tag=""
+release_tag=""
+latest_tag="${IMAGE_REPO}:latest"
+container_cli=""
+deployment_started=false
+rollback_done=false
+rollback_in_progress=false
+
+usage() {
+ cat <&2
+ if [ "${deployment_started:-false}" = "true" ] && [ "${rollback_in_progress:-false}" != "true" ]; then
+ rollback || true
+ fi
+ exit 1
+}
+
+run() {
+ if "$dry_run"; then
+ printf '[deploy:%s] DRY-RUN:' "$APP_NAME"
+ printf ' %q' "$@"
+ printf '\n'
+ return 0
+ fi
+
+ "$@"
+}
+
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run)
+ dry_run=true
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ usage >&2
+ fail "unknown argument: $arg"
+ ;;
+ esac
+done
+
+deployment_ref() {
+ printf '%s' "${DEPLOY_REF:-${GITHUB_REF:-${FORGEJO_REF:-}}}"
+}
+
+deployment_event() {
+ printf '%s' "${DEPLOY_EVENT_NAME:-${GITHUB_EVENT_NAME:-${FORGEJO_EVENT_NAME:-}}}"
+}
+
+require_main_push() {
+ local event
+ local ref
+
+ event="$(deployment_event)"
+ ref="$(deployment_ref)"
+
+ if [ -n "$event" ] && [ "$event" != "push" ]; then
+ fail "refusing to deploy for event '$event'; production deploys only run for push"
+ fi
+
+ if [ -n "$ref" ]; then
+ [ "$ref" = "refs/heads/main" ] || fail "refusing to deploy ref '$ref'; expected refs/heads/main"
+ return 0
+ fi
+
+ local branch
+ branch="$(git branch --show-current 2>/dev/null || true)"
+ [ "$branch" = "main" ] || fail "refusing to deploy branch '$branch'; expected main"
+}
+
+select_container_cli() {
+ if [ -n "${DEPLOY_CONTAINER_CLI:-}" ]; then
+ command -v "$DEPLOY_CONTAINER_CLI" >/dev/null 2>&1 || fail "container CLI not found: $DEPLOY_CONTAINER_CLI"
+ container_cli="$DEPLOY_CONTAINER_CLI"
+ return 0
+ fi
+
+ if command -v podman >/dev/null 2>&1; then
+ container_cli="podman"
+ return 0
+ fi
+
+ if command -v docker >/dev/null 2>&1; then
+ container_cli="docker"
+ return 0
+ fi
+
+ fail "podman or docker is required"
+}
+
+current_sha() {
+ if [ -n "${DEPLOY_SHA:-${GITHUB_SHA:-}}" ]; then
+ printf '%s' "${DEPLOY_SHA:-${GITHUB_SHA:-}}"
+ return 0
+ fi
+
+ git rev-parse HEAD
+}
+
+tag_existing_latest_for_rollback() {
+ rollback_tag="${IMAGE_REPO}:rollback-$(date -u +%Y%m%d%H%M%S)"
+ if "$container_cli" image inspect "$latest_tag" >/dev/null 2>&1; then
+ log "tagging current latest image as $rollback_tag"
+ run "$container_cli" tag "$latest_tag" "$rollback_tag"
+ else
+ log "no existing $latest_tag image found; rollback image tag will not be created"
+ rollback_tag=""
+ fi
+}
+
+container_systemd_unit() {
+ "$container_cli" inspect "$CONTAINER_NAME" \
+ --format '{{ index .Config.Labels "PODMAN_SYSTEMD_UNIT" }}' 2>/dev/null || true
+}
+
+require_container_managed_by_service() {
+ local unit
+
+ if "$dry_run"; then
+ log "DRY-RUN: would require $CONTAINER_NAME to be managed by $SERVICE_NAME"
+ return 0
+ fi
+
+ unit="$(container_systemd_unit)"
+ [ "$unit" = "$SERVICE_NAME" ] || fail "refusing to stop $CONTAINER_NAME; expected PODMAN_SYSTEMD_UNIT=$SERVICE_NAME, got '${unit:-unset}'"
+}
+
+validate_restart_strategy() {
+ case "$DEPLOY_RESTART_STRATEGY" in
+ systemctl)
+ if ! "$dry_run" && ! systemctl --user show "$SERVICE_NAME" >/dev/null 2>&1; then
+ fail "systemctl --user cannot access $SERVICE_NAME"
+ fi
+ ;;
+ quadlet-container|kill-container)
+ require_container_managed_by_service
+ ;;
+ auto)
+ if "$dry_run"; then
+ log "DRY-RUN: would validate automatic restart strategy"
+ elif ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show "$SERVICE_NAME" >/dev/null 2>&1; then
+ require_container_managed_by_service
+ fi
+ ;;
+ *)
+ fail "unknown DEPLOY_RESTART_STRATEGY: $DEPLOY_RESTART_STRATEGY"
+ ;;
+ esac
+}
+
+initialize_submodules() {
+ log "initializing submodules"
+ run git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
+ run git submodule update --init --recursive
+}
+
+build_image() {
+ local sha
+ local short_sha
+
+ sha="$(current_sha)"
+ short_sha="${sha:0:12}"
+ release_tag="${IMAGE_REPO}:${short_sha}"
+
+ [ -f "$CONTAINERFILE" ] || fail "containerfile not found: $CONTAINERFILE"
+
+ log "building $release_tag and $latest_tag from $CONTAINERFILE"
+ run "$container_cli" build -f "$CONTAINERFILE" -t "$release_tag" -t "$latest_tag" .
+}
+
+restart_service() {
+ log "restarting $SERVICE_NAME with strategy $DEPLOY_RESTART_STRATEGY"
+
+ case "$DEPLOY_RESTART_STRATEGY" in
+ systemctl)
+ run systemctl --user restart "$SERVICE_NAME"
+ ;;
+ quadlet-container|kill-container)
+ require_container_managed_by_service
+ run "$container_cli" stop "$CONTAINER_NAME"
+ ;;
+ auto)
+ if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active "$SERVICE_NAME" >/dev/null 2>&1; then
+ run systemctl --user restart "$SERVICE_NAME"
+ else
+ require_container_managed_by_service
+ run "$container_cli" stop "$CONTAINER_NAME"
+ fi
+ ;;
+ *)
+ fail "unknown DEPLOY_RESTART_STRATEGY: $DEPLOY_RESTART_STRATEGY"
+ ;;
+ esac
+}
+
+latest_image_id() {
+ "$container_cli" image inspect "$latest_tag" --format '{{.Id}}' 2>/dev/null || true
+}
+
+container_image_id() {
+ "$container_cli" inspect "$CONTAINER_NAME" --format '{{.Image}}' 2>/dev/null || true
+}
+
+container_running() {
+ local running
+ running="$("$container_cli" inspect "$CONTAINER_NAME" --format '{{.State.Running}}' 2>/dev/null || true)"
+ [ "$running" = "true" ]
+}
+
+wait_for_container_restart() {
+ local expected_image
+
+ if "$dry_run"; then
+ log "DRY-RUN: would wait for $CONTAINER_NAME to run $latest_tag"
+ return 0
+ fi
+
+ expected_image="$(latest_image_id)"
+ [ -n "$expected_image" ] || fail "could not resolve image id for $latest_tag"
+
+ wait_for_container_image "$expected_image" "new image" || fail "$CONTAINER_NAME did not restart on $latest_tag within ${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS}s"
+}
+
+wait_for_container_image() {
+ local expected_image="$1"
+ local label="$2"
+ local deadline
+
+ deadline=$((SECONDS + DEPLOY_CONTAINER_START_TIMEOUT_SECONDS))
+
+ while [ "$SECONDS" -lt "$deadline" ]; do
+ if container_running && [ "$(container_image_id)" = "$expected_image" ]; then
+ log "$CONTAINER_NAME is running the $label"
+ return 0
+ fi
+
+ sleep 2
+ done
+
+ return 1
+}
+
+smoke_get() {
+ local url="$1"
+ curl -fsS --max-time 10 -o /dev/null "$url"
+}
+
+smoke_tiles() {
+ local response
+
+ response="$(
+ curl -fsS --max-time 20 \
+ -H "content-type: application/json" \
+ --data '{"tiles":[{"kind":"status","stripId":"footer-status","id":"system-status"}]}' \
+ "$TILE_BATCH_URL"
+ )"
+
+ [[ "$response" == *'"state":"ready"'* ]]
+}
+
+wait_for_smoke() {
+ local deadline
+
+ if "$dry_run"; then
+ log "DRY-RUN: would smoke check $PUBLIC_URL and $TILE_BATCH_URL"
+ return 0
+ fi
+
+ deadline=$((SECONDS + DEPLOY_SMOKE_TIMEOUT_SECONDS))
+ until smoke_get "$PUBLIC_URL" && smoke_tiles; do
+ if [ "$SECONDS" -ge "$deadline" ]; then
+ fail "smoke checks failed for $PUBLIC_URL and $TILE_BATCH_URL"
+ fi
+
+ sleep 3
+ done
+
+ log "smoke checks passed"
+}
+
+rollback() {
+ local rollback_image
+
+ if [ "$deployment_started" != "true" ] || [ -z "$rollback_tag" ] || [ "$rollback_done" = "true" ]; then
+ return 0
+ fi
+
+ rollback_done=true
+ rollback_in_progress=true
+ printf '[deploy:%s] rolling back to %s\n' "$APP_NAME" "$rollback_tag" >&2
+ rollback_image="$("$container_cli" image inspect "$rollback_tag" --format '{{.Id}}' 2>/dev/null || true)"
+ "$container_cli" tag "$rollback_tag" "$latest_tag" || true
+ if container_running; then
+ restart_service || true
+ else
+ printf '[deploy:%s] waiting for %s to recover with rollback image\n' "$APP_NAME" "$SERVICE_NAME" >&2
+ fi
+ if [ -n "$rollback_image" ] && wait_for_container_image "$rollback_image" "rollback image"; then
+ printf '[deploy:%s] rollback image is running\n' "$APP_NAME" >&2
+ else
+ printf '[deploy:%s] ERROR: rollback image did not become healthy\n' "$APP_NAME" >&2
+ fi
+ rollback_in_progress=false
+}
+
+on_error() {
+ local status=$?
+ rollback
+ exit "$status"
+}
+
+trap on_error ERR
+
+require_main_push
+select_container_cli
+validate_restart_strategy
+
+log "using container CLI: $container_cli"
+log "target image: $latest_tag"
+log "target service: $SERVICE_NAME"
+
+initialize_submodules
+tag_existing_latest_for_rollback
+build_image
+deployment_started=true
+restart_service
+wait_for_container_restart
+wait_for_smoke
+
+log "deployment finished"