Compare commits

..

22 commits

Author SHA1 Message Date
a2c998b894 Merge pull request 'fix(web): remove prompt registry shortcut' (#57)
All checks were successful
Dimension Lab website / ci (push) Successful in 20s
Dimension Lab website / deploy (push) Successful in 33s
fix(web): remove prompt registry shortcut
2026-06-22 02:58:09 +02:00
vince
799299efca test(web): update dashboard e2e snapshots
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-22 02:57:13 +02:00
vince
6405254e42 fix(web): remove prompt registry shortcut
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-22 02:49:30 +02:00
8b0ef39cdf Merge pull request 'ci: use dedicated host deploy runner' (#56) from codex/use-host-deploy-runner into main
All checks were successful
Dimension Lab website / ci (push) Successful in 19s
Dimension Lab website / deploy (push) Successful in 33s
2026-06-20 17:41:46 +02:00
vince
d00a7b3b80 ci: use dedicated host deploy runner
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 18s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:37:38 +02:00
da9a5a942d Merge pull request 'docs(ci): document Forgejo deploy runner option' (#55)
Some checks failed
Dimension Lab website / ci (push) Successful in 18s
Dimension Lab website / deploy (push) Failing after 4s
2026-06-20 17:24:41 +02:00
vince
16c69a1065 docs(ci): document Forgejo deploy runner option
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:22:16 +02:00
b087e65978 Merge pull request 'ci: deploy website from Forgejo Actions' (#54)
Some checks failed
Dimension Lab website / ci (push) Successful in 20s
Dimension Lab website / deploy (push) Failing after 4s
2026-06-20 17:18:41 +02:00
vince
35cb1574b4 fix(ci): allow deploy job Podman socket access
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:12:29 +02:00
vince
d61d296ea1 fix(ci): verify deployment socket and rollback image
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 19s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:10:08 +02:00
vince
006c7e041f fix(ci): guard deploy auto fallback
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:59:36 +02:00
vince
a010520a94 fix(ci): run web unit tests with Bun
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:58:13 +02:00
vince
7e4d13a85c fix(ci): harden website deploy rollback
Some checks failed
Dimension Lab website / ci (pull_request) Failing after 15s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:54:18 +02:00
vince
96fe4d26c7 ci: deploy website from Forgejo Actions
Some checks failed
Dimension Lab website / ci (pull_request) Failing after 35s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:45:40 +02:00
29077923e8 Merge pull request 'fix(web): ignore stale aggregate health snapshots' (#53) 2026-06-20 15:54:33 +02:00
vince
3188d8b905 fix(web): ignore stale aggregate health snapshots 2026-06-20 15:49:22 +02:00
216545b838 Merge pull request 'perf(web): stream dashboard tile events' (#52)
Merge PR #52 from codex/dashboard-sse-events
2026-06-20 15:26:49 +02:00
vince
41fb964ebb perf(web): stream dashboard tile events 2026-06-20 15:23:14 +02:00
a66f1fdd57 Merge pull request 'perf(web): batch dashboard tile hydration' (#51)
Merge PR #51 from codex/dashboard-batch-tiles
2026-06-20 15:18:26 +02:00
vince
a7d0a4c3b2 fix(web): use structured dashboard tile match keys 2026-06-20 15:16:51 +02:00
vince
de87464ef1 perf(web): batch dashboard tile hydration 2026-06-20 15:12:18 +02:00
63b008095f Merge pull request 'perf(web): add dashboard hydration observability' (#50)
Merge PR #50 from codex/dashboard-observability
2026-06-20 15:06:55 +02:00
20 changed files with 1783 additions and 67 deletions

View file

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

View file

@ -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.

View file

@ -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"

View file

@ -4,6 +4,7 @@ import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import {
AppStateView,
dashboardTileMatchKey,
dashboardHydrationTiles,
restoreDashboardTileSnapshots,
} from "./App";
@ -159,4 +160,79 @@ describe("React app dashboard state view", () => {
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" }),
);
});
});

View file

@ -15,8 +15,10 @@ import {
createDashboardTileSnapshotStore,
createDashboardTileBackoff,
createDashboardRequestAborter,
isPersistableDashboardTileSnapshot,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
subscribeToDashboardTileEvents,
waitForDashboardHydrationIdle,
type DashboardIntersectionObserverFactory,
type DashboardTileReference,
@ -52,6 +54,11 @@ type DashboardTileResponse =
message: string;
};
type DashboardTilesBatchResponse = {
state: "ready";
tiles: DashboardTileResponse[];
};
type DashboardTileHydrationResult = "aborted" | "failed" | "ready";
const dashboardTileHydrationConcurrency = 6;
@ -172,6 +179,7 @@ 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();
@ -208,6 +216,8 @@ export default function App() {
function pauseRefreshes() {
clearRefreshTimer();
unsubscribeTileEvents?.();
unsubscribeTileEvents = undefined;
requestAborter.abortActiveRequests();
setHydratingItemIds(new Set());
}
@ -261,6 +271,14 @@ export default function App() {
schemaVersion: restored.dashboard.schemaVersion,
},
);
subscribeDashboardTileEvents(
currentRun,
tileSignal,
{
currentRevisionId: restored.dashboard.currentRevisionId,
schemaVersion: restored.dashboard.schemaVersion,
},
);
} else {
setHydratingItemIds(new Set());
}
@ -292,6 +310,7 @@ export default function App() {
const modelIds = tiles.map(dashboardTileModelId);
void runViewportAwareDashboardHydrationQueue({
batchSize: dashboardTileHydrationConcurrency,
collectVisibleModelIds: async () => {
await waitForDashboardRenderFrame(signal);
return collectVisibleDashboardModelIds({
@ -321,6 +340,23 @@ export default function App() {
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(),
@ -335,6 +371,93 @@ export default function App() {
});
}
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<Array<{ result: DashboardTileHydrationResult; tile: DashboardTileReference }>> {
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,
@ -347,11 +470,48 @@ export default function App() {
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 ||
!response.ok ||
!tileResponse ||
dashboardTileMatchKey(tileResponse.tile) !== dashboardTileMatchKey(tile) ||
tileResponse.state !== "ready"
) {
return signal.aborted || cancelled || run !== hydrationRun
@ -374,13 +534,13 @@ export default function App() {
});
performanceMarks.markFirstTileReady();
return "ready";
} catch (error) {
if (!isAbortError(error) && !cancelled) {
console.error("Dashboard tile hydration failed", error);
return "failed";
}
return "aborted";
} finally {
function finishDashboardTileHydration(
key: string,
run: number,
signal: AbortSignal,
) {
if (!cancelled && !signal.aborted && run === hydrationRun) {
setHydratingItemIds((current) => {
const next = new Set(current);
@ -389,7 +549,6 @@ export default function App() {
});
}
}
}
const detachRefreshLifecycle = attachDashboardRefreshLifecycle({
documentTarget: document,
@ -404,6 +563,7 @@ export default function App() {
return () => {
cancelled = true;
clearRefreshTimer();
unsubscribeTileEvents?.();
requestAborter.abortActiveRequests();
detachRefreshLifecycle();
};
@ -503,6 +663,20 @@ function isAbortError(error: unknown): boolean {
);
}
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[],
@ -517,7 +691,9 @@ export function restoreDashboardTileSnapshots(
};
}
return snapshots.reduce(
return snapshots.filter((snapshot) =>
isPersistableDashboardTileSnapshot(snapshot.response.tile)
).reduce(
(current, snapshot) => ({
dashboard: {
...current.dashboard,
@ -627,6 +803,10 @@ function dashboardTileKey(tile: DashboardTileReference): string {
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;

View file

@ -9,9 +9,11 @@ import {
createDashboardTileBackoff,
createDashboardRequestAborter,
runDashboardHydrationQueue,
runDashboardHydrationBatchQueue,
runViewportAwareDashboardHydrationQueue,
shouldPauseDashboardRefresh,
splitDashboardHydrationItemsByVisibility,
subscribeToDashboardTileEvents,
waitForDashboardHydrationIdle,
type DashboardIntersectionEntry,
} from "./dashboard-refresh";
@ -121,6 +123,25 @@ describe("dashboard refresh lifecycle", () => {
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> = [];
@ -282,6 +303,69 @@ describe("dashboard refresh lifecycle", () => {
]);
});
test("subscribes to dashboard tile events", () => {
const received: unknown[] = [];
let listener: ((event: MessageEvent<string>) => 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<string>);
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();
@ -434,6 +518,77 @@ describe("dashboard refresh lifecycle", () => {
},
]);
});
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) {

View file

@ -152,10 +152,12 @@ export async function runDashboardHydrationQueue<TItem>(
}
export interface DashboardViewportHydrationQueueOptions<TItem> {
batchSize?: number;
collectVisibleModelIds: () => Promise<ReadonlySet<string>>;
concurrency: number;
getModelId: (item: TItem) => string;
hydrate: (item: TItem) => Promise<void> | void;
hydrateBatch?: (items: TItem[]) => Promise<void> | void;
items: TItem[];
onAllItemsSettled?: () => void;
onVisibleItemsSettled?: () => void;
@ -175,12 +177,7 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
visibleModelIds,
});
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: visible,
signal: options.signal,
});
await runDashboardHydrationItems(options, visible);
if (options.signal.aborted) return;
options.onVisibleItemsSettled?.();
@ -192,13 +189,44 @@ export async function runViewportAwareDashboardHydrationQueue<TItem>(
await options.waitForIdle();
if (options.signal.aborted) return;
await runDashboardHydrationItems(options, deferred);
options.onAllItemsSettled?.();
}
async function runDashboardHydrationItems<TItem>(
options: DashboardViewportHydrationQueueOptions<TItem>,
items: TItem[],
): Promise<void> {
if (!options.hydrateBatch) {
await runDashboardHydrationQueue({
concurrency: options.concurrency,
hydrate: options.hydrate,
items: deferred,
items,
signal: options.signal,
});
options.onAllItemsSettled?.();
return;
}
await runDashboardHydrationBatchQueue({
batchSize: options.batchSize ?? options.concurrency,
hydrateBatch: options.hydrateBatch,
items,
signal: options.signal,
});
}
export async function runDashboardHydrationBatchQueue<TItem>(options: {
batchSize: number;
hydrateBatch: (items: TItem[]) => Promise<void> | void;
items: TItem[];
signal: AbortSignal;
}): Promise<void> {
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<TItem>(options: {
@ -461,6 +489,51 @@ export function createDashboardPerformanceMarks(
};
}
export interface DashboardTileEventSource {
addEventListener(
type: "dashboard-tile",
listener: (event: MessageEvent<string>) => 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() {
@ -544,7 +617,9 @@ export function createDashboardTileSnapshotStore(
return {
currentRevisionId: payload.currentRevisionId,
schemaVersion: payload.schemaVersion,
tiles: payload.tiles.filter(isDashboardTileSnapshotRecord),
tiles: payload.tiles
.filter(isDashboardTileSnapshotRecord)
.filter((record) => isPersistableDashboardTileSnapshot(record.tile)),
version: 1,
};
} catch {
@ -587,6 +662,8 @@ export function createDashboardTileSnapshotStore(
response: Extract<DashboardTileSnapshotResponse, { state: "ready" }>;
},
): void {
if (!isPersistableDashboardTileSnapshot(input.response.tile)) return;
const payload = matchingPayload(input);
const key = dashboardTileSnapshotKey(input.response.tile);
const nextRecord: DashboardTileSnapshotRecord = {
@ -647,6 +724,14 @@ 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 {

View file

@ -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",

View file

@ -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({

View file

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

View file

@ -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"]),
@ -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<string, string>,
env: Record<string, string>,
): { 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
`;
}

View file

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

View file

@ -1,6 +1,11 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-dashboard";
import { handleDashboardRoute, handleDashboardTileRoute } 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,16 @@ export async function handleRequest(request: Request): Promise<Response> {
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);

View file

@ -3,8 +3,11 @@ import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import {
createDashboardTileCache,
dashboardTileCacheKey,
handleDashboardEventsRoute,
handleDashboardTilesRoute,
handleDashboardTileRoute,
loadDashboardResponse,
loadDashboardTilesResponse,
loadDashboardTileResponse,
type DashboardTileResolutionLogEvent,
} from "./dashboard";
@ -563,6 +566,154 @@ describe("dashboard API route", () => {
);
});
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<string, () => 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<void>((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";
@ -598,6 +749,15 @@ function jsonResponse(payload: unknown): Response {
});
}
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);

View file

@ -25,6 +25,10 @@ export interface LoadDashboardResponseOptions
tileCache?: DashboardTileCache;
}
export interface DashboardEventsRouteOptions extends LoadDashboardResponseOptions {
signal?: AbortSignal;
}
interface DashboardTileCacheEntry {
expiresAt: number;
response: DashboardTileResolution;
@ -43,6 +47,8 @@ const dashboardTileResponseHeaders = {
"Cache-Control": "private, max-age=5, stale-while-revalidate=30",
};
const dashboardBatchTileConcurrency = 6;
const defaultDashboardTileCache = createDashboardTileCache();
interface DashboardTileCacheResult {
@ -60,6 +66,11 @@ export interface DashboardTileResolutionLogEvent {
tileKey: string;
}
export interface DashboardTilesBatchResponse {
state: "ready";
tiles: DashboardTileResolution[];
}
export function createDashboardTileCache(): DashboardTileCache {
const entries = new Map<string, DashboardTileCacheEntry>();
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
@ -236,6 +247,16 @@ export async function loadDashboardTileResponse(
}
}
export async function loadDashboardTilesResponse(
tiles: DashboardTileReference[],
options: LoadDashboardResponseOptions = {},
): Promise<DashboardTilesBatchResponse> {
return {
state: "ready",
tiles: await resolveDashboardTilesBatch(tiles, options),
};
}
export async function handleDashboardRoute(): Promise<Response> {
return Response.json(await loadDashboardResponse());
}
@ -259,6 +280,65 @@ export async function handleDashboardTileRoute(
});
}
export async function handleDashboardTilesRoute(
request: Request,
options: LoadDashboardResponseOptions = {},
): Promise<Response> {
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<Response> {
const stream = new ReadableStream<Uint8Array>({
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);
}
@ -296,6 +376,137 @@ function dashboardTileErrorCategory(error: unknown): string {
return "unknown";
}
async function resolveDashboardTilesBatch(
tiles: DashboardTileReference[],
options: LoadDashboardResponseOptions,
): Promise<DashboardTileResolution[]> {
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<DashboardTileReference[] | null> {
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;

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 KiB

After

Width:  |  Height:  |  Size: 451 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Before After
Before After

View file

@ -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 <<USAGE
Usage: $0 [--dry-run]
Build and deploy ${APP_NAME} for the production ${SERVICE_NAME} unit.
Options:
--dry-run Print the deployment actions without changing the host.
USAGE
}
log() {
printf '[deploy:%s] %s\n' "$APP_NAME" "$*"
}
fail() {
printf '[deploy:%s] ERROR: %s\n' "$APP_NAME" "$*" >&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"