diff --git a/.containerignore b/.containerignore
index 70b617e..1812091 100644
--- a/.containerignore
+++ b/.containerignore
@@ -5,8 +5,18 @@ coverage
data
dist
node_modules
+out
playwright-report
storybook-static
test-results
.env
.env.*
+apps/*/.turbo
+apps/*/build
+apps/*/data
+apps/*/dist
+apps/*/playwright-report
+apps/*/test-results
+packages/*/.turbo
+packages/*/dist
+packages/*/storybook-static
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/.gitignore b/.gitignore
index 7d8d6c4..eed0de1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,12 @@
node_modules/
+out/
.svelte-kit/
build/
dist/
.vite/
+.turbo/
+apps/*/.turbo/
+packages/*/.turbo/
.env
.env.*
@@ -10,6 +14,8 @@ dist/
data/*.sqlite
data/*.sqlite-*
+apps/*/data/*.sqlite
+apps/*/data/*.sqlite-*
coverage/
playwright-report/
test-results/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..32d1c14
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "packages/ui"]
+ path = packages/ui
+ url = ssh://git@git.dimensionlab.net/vince/dimensionlab-ui.git
+ branch = main
diff --git a/.storybook/main.ts b/.storybook/main.ts
deleted file mode 100644
index cfd3a61..0000000
--- a/.storybook/main.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { StorybookConfig } from "@storybook/react-vite";
-
-const config: StorybookConfig = {
- stories: ["../src/**/*.stories.@(js|ts|tsx)"],
- staticDirs: ["../static"],
- addons: [
- "@storybook/addon-a11y",
- "@storybook/addon-vitest",
- ],
- framework: {
- name: "@storybook/react-vite",
- options: {},
- },
- docs: {
- autodocs: "tag",
- },
-};
-
-export default config;
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
deleted file mode 100644
index 59d663d..0000000
--- a/.storybook/preview.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import "../src/app.css";
-import type { Preview } from "@storybook/react-vite";
-import { setupWorker } from "msw/browser";
-import { externalApiHandlers } from "../src/lib/testing/external-api-mocks";
-
-if (typeof window !== "undefined") {
- const worker = setupWorker(...externalApiHandlers);
- void worker.start({
- onUnhandledRequest: "bypass",
- serviceWorker: {
- url: "/mockServiceWorker.js",
- },
- });
-}
-
-const preview: Preview = {
- parameters: {
- backgrounds: {
- default: "canvas",
- values: [
- { name: "canvas", value: "#020302" },
- { name: "raised", value: "#0b0d0c" },
- ],
- },
- controls: {
- matchers: {
- color: /(background|color)$/i,
- date: /Date$/i,
- },
- },
- layout: "fullscreen",
- },
-};
-
-export default preview;
diff --git a/Containerfile b/Containerfile
deleted file mode 100644
index d19c2d0..0000000
--- a/Containerfile
+++ /dev/null
@@ -1,31 +0,0 @@
-FROM docker.io/oven/bun:1.3.14 AS deps
-
-WORKDIR /app
-COPY package.json bun.lock ./
-RUN bun install --frozen-lockfile
-
-FROM deps AS build
-
-COPY . .
-RUN bun run build
-
-FROM docker.io/oven/bun:1.3.14 AS runtime
-
-WORKDIR /app
-ENV NODE_ENV=production
-ENV HOST=0.0.0.0
-ENV PORT=3000
-ENV DATABASE_URL=file:/data/dimensionlab.sqlite
-ENV DASHBOARD_MIGRATIONS_DIR=/app/drizzle
-
-COPY package.json bun.lock ./
-RUN bun install --frozen-lockfile --production
-COPY --from=build /app/build ./build
-COPY --from=build /app/dist ./dist
-COPY --from=build /app/drizzle ./drizzle
-
-RUN mkdir -p /data
-VOLUME ["/data"]
-EXPOSE 3000
-
-CMD ["bun", "build/index.js"]
diff --git a/README.md b/README.md
index f39dcb5..85177c5 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,30 @@
# Dimension Lab Website
-Standalone React runtime for the Dimension Lab system overview dashboard.
+Turbo/Bun workspace for the Dimension Lab system overview dashboard and its
+reusable React component library.
This project is not a Homepage customization and does not depend on Homepage
runtime, frontend code, or configuration. The dashboard will be model-driven:
-the reusable renderer stays content-free, while environment-specific data lives
-in validated dashboard model state.
+the reusable UI package stays content-free, the reusable dashboard model
+package owns schema and validation, and environment-specific data lives in
+validated dashboard model state inside the web app.
+
+## Workspace Layout
+
+- `apps/web`: Vite React website, Bun API server, model fixtures, Drizzle
+ persistence, Playwright e2e checks, and container build.
+- `packages/dashboard-model`: reusable dashboard schema, validation, and
+ generic model fixtures shared by apps and tooling.
+- `packages/ui`: Git submodule for the reusable dashboard React components,
+ design tokens, shadcn/radix primitives, generic fixtures, and Storybook.
+ Component source is grouped under `foundation`, `frames`, `operations`, and
+ `telemetry` domains.
+- `docs/superpowers`: migration specs and execution plans used for this repo.
## Development
```sh
+git submodule update --init --recursive
bun install
bun run dev
```
@@ -18,18 +33,19 @@ This MVP uses Drizzle with Bun SQLite for local file-backed persistence.
## Scripts
-- `bun run dev`: start the local Vite development server with a Bun API proxy.
-- `bun run check`: run TypeScript checks.
-- `bun run test`: run Vitest.
+- `bun run dev`: start the web app dev runtime through Turbo.
+- `bun run check`: run TypeScript checks in all workspaces.
+- `bun run test`: run the unit test stage in all workspaces.
- `bun run test:unit`: run Vitest explicitly as the unit test stage.
- `bun run test:e2e`: build and run Playwright browser smoke and QA checks.
-- `bun run test:qa`: run the MVP release gate.
-- `bun run build`: build the production app.
-- `bun run preview`: preview the production build.
-- `bun run storybook`: start the component explorer on port 6006.
-- `bun run build-storybook`: build the static Storybook review artifact.
-- `bun run db:generate`: generate Drizzle migrations from the server schema.
-- `bun run db:check`: validate migration consistency.
+- `bun run test:qa`: run the release gate through Turbo across check, unit,
+ build, Storybook, and e2e tasks.
+- `bun run build`: build the UI package, production website, and Bun server.
+- `bun run preview`: preview the production web build.
+- `bun run storybook`: start the UI package component explorer on port 6006.
+- `bun run build-storybook`: build the UI package static Storybook artifact.
+- `bun run db:generate`: generate web app Drizzle migrations.
+- `bun run db:check`: validate web app migration consistency.
## Persistence
@@ -40,14 +56,14 @@ URL is:
DATABASE_URL=file:./data/dimensionlab.sqlite
```
-SQLite files under `data/` are ignored. Drizzle schema lives in
-`src/lib/server/db/schema.ts`; tracked migrations live in `drizzle/`. Runtime
-startup applies the checked-in dashboard migrations before reads or writes. If
-the app is launched from outside the repo tree, set `DASHBOARD_MIGRATIONS_DIR`
-to the tracked migrations directory. The current driver is `bun:sqlite`, which
-keeps this repo installable in the Bun workflow. The store boundary is isolated
-so a later Postgres driver can replace the SQLite connection without changing
-the dashboard model or renderer.
+SQLite files under `data/` and `apps/*/data/` are ignored. Drizzle schema lives
+in `apps/web/src/lib/server/db/schema.ts`; tracked migrations live in
+`apps/web/drizzle/`. Runtime startup applies the checked-in dashboard migrations
+before reads or writes. If the app is launched from outside the web app tree,
+set `DASHBOARD_MIGRATIONS_DIR` to the tracked migrations directory. The current
+driver is `bun:sqlite`, which keeps this repo installable in the Bun workflow.
+The store boundary is isolated so a later Postgres driver can replace the
+SQLite connection without changing the dashboard model or renderer.
Stored dashboard documents pass through a version migration boundary before
reads or writes; the MVP supports `dashboard.v1` and fails unsupported versions
with an explicit migration error.
@@ -55,25 +71,26 @@ with an explicit migration error.
## Seed Data
The initial Dimension Lab dashboard lives in
-`src/lib/model/fixtures/dimensionlab.ts` as validated model data. It includes
-the first-screen telemetry, service groups, status strip, weather module,
-Iconify icon identifiers, links, and datasource references. Values that are not
-live yet are labeled as fallback values in the data so later datasource adapters
-can replace them without changing presentation components.
+`apps/web/src/lib/dashboard-seed/dimensionlab.ts` as validated model data. It
+includes the first-screen telemetry, service groups, status strip, weather
+module, Iconify icon identifiers, links, and datasource references. Values that
+are not live yet are labeled as fallback values in the data so later datasource
+adapters can replace them without changing presentation components.
## Runtime Shape
-The browser app is built with Vite and React. Local development starts Vite for
-HMR and a loopback Bun API server for `/api/*` routes. Production uses a small
-Bun HTTP server at `build/index.js` to serve the Vite `dist/` assets and JSON
-API routes. The current persistence runtime is Bun because the MVP SQLite
-driver is `bun:sqlite`.
+The browser app in `apps/web` is built with Vite and React. Local development
+starts Vite for HMR and a loopback Bun API server for `/api/*` routes.
+Production uses a small Bun HTTP server at `apps/web/build/index.js` to serve
+the Vite `apps/web/dist/` assets and JSON API routes. The current persistence
+runtime is Bun because the MVP SQLite driver is `bun:sqlite`.
## Storybook
-Storybook covers the reusable UI components with generic fixtures only. Stories
-must not import environment-specific dashboard content; the presentation layer
-accepts labels, values, icons, status, and links through typed props.
+Storybook lives with `packages/ui` and covers the reusable UI components with
+generic fixtures only. Stories must not import environment-specific dashboard
+content; the presentation layer accepts labels, values, icons, status, and links
+through typed props.
## MVP QA Gate
@@ -92,7 +109,9 @@ bun run test:qa
The gate runs TypeScript checks, Vitest coverage for model,
persistence, renderer, datasource mocks, and presentation boundaries, the
production build, the static Storybook build, and Playwright desktop/mobile
-smoke checks against the built adapter output. Playwright also performs
+smoke checks against the built adapter output. Turbo owns the release task
+graph; app package scripts stay as leaf commands and do not re-run the QA
+pipeline internally. Playwright also performs
baseline screenshot checks, keyboard navigation checks, reduced-motion checks,
landmark checks, and axe accessibility checks against the real model-driven
route.
@@ -106,27 +125,31 @@ belong in validated model data, not reusable components.
## Deployment Notes
-The production build emits Vite client assets under `dist/` and a Bun server
-entry at `build/index.js`. A minimal deployment flow is:
+The production build emits Vite client assets under `apps/web/dist/` and a Bun
+server entry at `apps/web/build/index.js`. A minimal deployment flow is:
```sh
+git submodule update --init --recursive
bun install --frozen-lockfile
bun run build
+cd apps/web
DATABASE_URL=file:/data/dimensionlab.sqlite HOST=0.0.0.0 PORT=3000 bun build/index.js
```
Mount `/data` or set `DATABASE_URL` to another persistent SQLite path. If the
process starts outside the repository root, set `DASHBOARD_MIGRATIONS_DIR` to
-the checked-in `drizzle/` directory so startup migrations can run.
+the checked-in `apps/web/drizzle/` directory so startup migrations can run.
### Internal Container
-The checked-in `Containerfile` builds the React client and Bun server into a
-runtime image. For the Dimension Lab internal host, run it behind Caddy on a
-loopback port and mount persistent state at `/data`:
+The checked-in `apps/web/Containerfile` runs
+`turbo prune @dimensionlab/web --docker`, installs the pruned manifest set, and
+builds the React client plus Bun server from the pruned workspace source. For the
+Dimension Lab internal host, run it behind Caddy on a loopback port and mount
+persistent state at `/data`:
```sh
-podman build -t localhost/dimensionlab-website:latest .
+podman build -f apps/web/Containerfile -t localhost/dimensionlab-website:latest .
podman run --rm \
--publish 127.0.0.1:25341:3000 \
--volume "$HOME/containers/dimensionlab-website/data:/data:Z" \
@@ -137,4 +160,28 @@ podman run --rm \
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=/app/drizzle`.
+`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
new file mode 100644
index 0000000..32387a4
--- /dev/null
+++ b/apps/web/Containerfile
@@ -0,0 +1,39 @@
+FROM docker.io/oven/bun:1.3.14 AS base
+
+WORKDIR /repo
+ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/dashboard-model/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH
+
+FROM base AS pruner
+
+COPY . .
+RUN bunx turbo prune @dimensionlab/web --docker
+
+FROM base AS deps
+
+COPY --from=pruner /repo/out/json/ ./
+RUN bun install --frozen-lockfile --ignore-scripts
+
+FROM deps AS build
+
+COPY --from=pruner /repo/out/full/ ./
+COPY --from=pruner /repo/tsconfig.base.json /repo/tsconfig.json ./
+RUN bun run build
+
+FROM base AS runtime
+
+WORKDIR /repo/apps/web
+ENV NODE_ENV=production
+ENV HOST=0.0.0.0
+ENV PORT=3000
+ENV DATABASE_URL=file:/data/dimensionlab.sqlite
+ENV DASHBOARD_MIGRATIONS_DIR=/repo/apps/web/drizzle
+
+COPY --from=build /repo/apps/web/build ./build
+COPY --from=build /repo/apps/web/dist ./dist
+COPY --from=build /repo/apps/web/drizzle ./drizzle
+
+RUN mkdir -p /data
+VOLUME ["/data"]
+EXPOSE 3000
+
+CMD ["bun", "build/index.js"]
diff --git a/drizzle.config.ts b/apps/web/drizzle.config.ts
similarity index 100%
rename from drizzle.config.ts
rename to apps/web/drizzle.config.ts
diff --git a/drizzle/0000_dashboard_persistence.sql b/apps/web/drizzle/0000_dashboard_persistence.sql
similarity index 100%
rename from drizzle/0000_dashboard_persistence.sql
rename to apps/web/drizzle/0000_dashboard_persistence.sql
diff --git a/drizzle/meta/0000_snapshot.json b/apps/web/drizzle/meta/0000_snapshot.json
similarity index 100%
rename from drizzle/meta/0000_snapshot.json
rename to apps/web/drizzle/meta/0000_snapshot.json
diff --git a/drizzle/meta/_journal.json b/apps/web/drizzle/meta/_journal.json
similarity index 100%
rename from drizzle/meta/_journal.json
rename to apps/web/drizzle/meta/_journal.json
diff --git a/index.html b/apps/web/index.html
similarity index 56%
rename from index.html
rename to apps/web/index.html
index 5e47684..75dae71 100644
--- a/index.html
+++ b/apps/web/index.html
@@ -5,6 +5,15 @@
Dimension Lab
+
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..015fc55
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@dimensionlab/web",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "bun src/server/dev.ts",
+ "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": "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"
+ },
+ "dependencies": {
+ "@dimensionlab/dashboard-model": "workspace:*",
+ "@dimensionlab/ui": "workspace:*",
+ "@sinclair/typebox": "^0.34.49",
+ "ajv": "^8.20.0",
+ "ajv-formats": "^3.0.1",
+ "drizzle-orm": "^0.45.2",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "tw-animate-css": "^1.4.0"
+ },
+ "devDependencies": {
+ "@axe-core/playwright": "^4.11.3",
+ "@playwright/test": "^1.61.0",
+ "@tailwindcss/vite": "^4.3.1",
+ "@types/bun": "^1.3.14",
+ "@types/node": "^25.9.3",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.2",
+ "bun-types": "^1.3.14",
+ "drizzle-kit": "^0.31.10",
+ "msw": "^2.14.6",
+ "shadcn": "^4.11.0",
+ "tailwindcss": "^4.3.1",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.16",
+ "vitest": "^4.1.9"
+ },
+ "msw": {
+ "workerDirectory": [
+ "static"
+ ]
+ }
+}
diff --git a/playwright.config.ts b/apps/web/playwright.config.ts
similarity index 60%
rename from playwright.config.ts
rename to apps/web/playwright.config.ts
index 2785c33..ac0fe5e 100644
--- a/playwright.config.ts
+++ b/apps/web/playwright.config.ts
@@ -2,6 +2,8 @@ import { defineConfig, devices } from "@playwright/test";
const port = Number(process.env.PLAYWRIGHT_PORT || 4173);
const baseURL = `http://127.0.0.1:${port}`;
+const storybookPort = Number(process.env.PLAYWRIGHT_STORYBOOK_PORT || 6007);
+const storybookURL = `http://127.0.0.1:${storybookPort}`;
const databaseUrl =
process.env.PLAYWRIGHT_DATABASE_URL ||
`file:./data/playwright-${process.pid}-${Date.now()}.sqlite`;
@@ -17,12 +19,20 @@ export default defineConfig({
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
- webServer: {
- command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`,
- url: baseURL,
- reuseExistingServer: false,
- timeout: 120_000,
- },
+ webServer: [
+ {
+ command: `DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`,
+ url: baseURL,
+ reuseExistingServer: false,
+ timeout: 120_000,
+ },
+ {
+ command: `cd ../.. && STORYBOOK_STATIC_PORT=${storybookPort} bun apps/web/tests/e2e/storybook-server.ts`,
+ url: storybookURL,
+ reuseExistingServer: false,
+ timeout: 120_000,
+ },
+ ],
projects: [
{
name: "chromium-desktop",
diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx
new file mode 100644
index 0000000..c1add67
--- /dev/null
+++ b/apps/web/src/App.test.tsx
@@ -0,0 +1,238 @@
+import { renderToString } from "react-dom/server";
+import { describe, expect, test } from "vitest";
+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", () => {
+ const html = renderToString(
+ ,
+ );
+
+ expect(html).toContain("Loading Dashboard");
+ expect(html).toContain("Fetching active model");
+ });
+
+ test("renders an accessible theme toggle with the active theme", () => {
+ const html = renderToString(
+ undefined}
+ />,
+ );
+
+ expect(html).toContain('data-ui-theme-toggle="true"');
+ expect(html).toContain('data-ui-theme-current="dark"');
+ expect(html).toContain('aria-label="Light theme"');
+ expect(html).toContain('aria-pressed="false"');
+ expect(html).toContain("Theme");
+ 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
new file mode 100644
index 0000000..403bef9
--- /dev/null
+++ b/apps/web/src/App.tsx
@@ -0,0 +1,875 @@
+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,
+ SystemState,
+ ThemeToggle,
+ persistUiTheme,
+ resolveInitialUiTheme,
+ type UiTheme,
+ type UiSeverity,
+ type UiDashboardPreview,
+} from "@dimensionlab/ui";
+
+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;
+}) {
+ const themeToggle =
+ theme && onThemeChange ? (
+
+ ) : null;
+
+ if (dashboard.state === "ready") {
+ const uiDashboard = markHydratingItems(
+ dashboardDocumentToUiDashboard(dashboard.document),
+ hydratingItemIds,
+ );
+
+ return (
+
+ );
+ }
+
+ const detail = `${dashboard.subtitle}: ${dashboard.message}`;
+ const errors = dashboard.state === "invalid" ? dashboard.errors : [];
+
+ return (
+
+ {themeToggle ? (
+ {themeToggle}
+ ) : null}
+
+ {errors.length ? (
+
+ {errors.map((error) => (
+ {error}
+ ))}
+
+ ) : null}
+
+ );
+}
+
+export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
+ description: string;
+ title: string;
+} {
+ if (dashboard.state === "ready") {
+ const uiDashboard = dashboardDocumentToUiDashboard(dashboard.document);
+ return {
+ title: uiDashboard.title,
+ description:
+ uiDashboard.subtitle ||
+ dashboard.document.metadata.description ||
+ uiDashboard.title,
+ };
+ }
+
+ return {
+ title: dashboard.title,
+ description: dashboard.subtitle || dashboard.message,
+ };
+}
+
+export default function App() {
+ const [dashboard, setDashboard] =
+ useState(null);
+ const [hydratingItemIds, setHydratingItemIds] = useState>(
+ () => new Set(),
+ );
+ const [theme, setTheme] = useState(() => {
+ if (typeof window === "undefined") return "dark";
+
+ return resolveInitialUiTheme(getThemeStorage());
+ });
+
+ useEffect(() => {
+ if (!dashboard) return;
+
+ const metadata = resolveDocumentMetadata(dashboard);
+ document.title = metadata.title;
+
+ let description = document.querySelector(
+ 'meta[name="description"]',
+ );
+ if (!description) {
+ description = document.createElement("meta");
+ description.name = "description";
+ document.head.append(description);
+ }
+ description.content = metadata.description;
+ }, [dashboard]);
+
+ useEffect(() => {
+ document.documentElement.dataset.uiTheme = theme;
+ persistUiTheme(theme, getThemeStorage());
+ }, [theme]);
+
+ 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(),
+ );
+
+ function refreshPaused() {
+ return shouldPauseDashboardRefresh({
+ visibilityState: document.visibilityState,
+ online: navigator.onLine,
+ });
+ }
+
+ function clearRefreshTimer() {
+ if (refreshTimer !== undefined) {
+ window.clearTimeout(refreshTimer);
+ refreshTimer = undefined;
+ }
+ }
+
+ function scheduleNextRefresh(baseDelayMs: number) {
+ clearRefreshTimer();
+ if (cancelled || refreshPaused()) return;
+
+ 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;
+ clearRefreshTimer();
+ unsubscribeTileEvents?.();
+ requestAborter.abortActiveRequests();
+ detachRefreshLifecycle();
+ };
+ }, []);
+
+ return dashboard ? (
+
+ ) : null;
+}
+
+function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
+ if (state === "invalid") return "danger";
+ if (state === "loading") return "loading";
+ return "stale";
+}
+
+function stateIcon(state: DashboardRuntimeState["state"]): string {
+ if (state === "invalid") return "mdi:file-alert-outline";
+ if (state === "loading") return "mdi:progress-clock";
+ return "mdi:tray";
+}
+
+function getThemeStorage(): Storage | undefined {
+ try {
+ return window.localStorage;
+ } catch {
+ 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/src/app.css b/apps/web/src/app.css
similarity index 89%
rename from src/app.css
rename to apps/web/src/app.css
index 10a9ee9..eeaabcb 100644
--- a/src/app.css
+++ b/apps/web/src/app.css
@@ -1,9 +1,7 @@
@import "tailwindcss";
-@import "./lib/ui/tokens.css";
-@import "./lib/ui/components/styles.css";
+@import "@dimensionlab/ui/styles.css";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
-@import "@fontsource-variable/geist";
@custom-variant dark (&:is(.dark *));
@@ -67,8 +65,8 @@
--accent: var(--ui-color-accent);
--accent-foreground: var(--ui-color-canvas);
--destructive: var(--ui-color-danger);
- --border: rgba(244, 244, 244, 0.16);
- --input: rgba(244, 244, 244, 0.18);
+ --border: var(--ui-color-border);
+ --input: var(--ui-color-border-strong);
--ring: var(--ui-color-accent);
--chart-1: var(--ui-color-accent);
--chart-2: var(--ui-color-ok);
@@ -107,10 +105,11 @@ body {
min-height: 100vh;
margin: 0;
background:
- linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
- linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
+ radial-gradient(circle at 50% 12%, transparent 0 42%, var(--ui-color-backdrop-vignette) 100%),
+ linear-gradient(var(--ui-color-grid-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--ui-color-grid-line) 1px, transparent 1px),
var(--ui-color-canvas);
- background-size: 48px 48px, 48px 48px, auto;
+ background-size: auto, 40px 40px, 40px 40px, auto;
color: var(--ui-color-text);
font-family: var(--ui-font-mono);
text-rendering: geometricPrecision;
@@ -142,13 +141,17 @@ a {
padding: var(--ui-space-4);
}
+.state-shell__actions {
+ justify-self: center;
+}
+
.state-shell ul {
display: grid;
max-width: 56rem;
gap: var(--ui-space-2);
margin: 0;
border: var(--ui-border);
- background: rgba(6, 8, 7, 0.82);
+ background: var(--ui-color-surface-module);
color: var(--ui-color-muted);
font-size: 0.76rem;
list-style-position: inside;
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/src/lib/model/fixtures/dimensionlab.test.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
similarity index 99%
rename from src/lib/model/fixtures/dimensionlab.test.ts
rename to apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
index 38b2ac6..1a97c3b 100644
--- a/src/lib/model/fixtures/dimensionlab.test.ts
+++ b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts
@@ -7,7 +7,7 @@ import type {
DatasourceReference,
ServiceEntry,
TelemetryCard,
-} from "../schema";
+} from "@dimensionlab/dashboard-model";
describe("Dimension Lab dashboard seed", () => {
test("defines the primary first-screen sections as model data", () => {
@@ -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/src/lib/model/fixtures/dimensionlab.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.ts
similarity index 98%
rename from src/lib/model/fixtures/dimensionlab.ts
rename to apps/web/src/lib/dashboard-seed/dimensionlab.ts
index c2b4808..59162e6 100644
--- a/src/lib/model/fixtures/dimensionlab.ts
+++ b/apps/web/src/lib/dashboard-seed/dimensionlab.ts
@@ -7,7 +7,7 @@ import {
type ServiceGroup,
type Severity,
type TelemetryCard,
-} from "../schema";
+} from "@dimensionlab/dashboard-model";
type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number";
@@ -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/src/lib/presentation-boundary.test.ts b/apps/web/src/lib/presentation-boundary.test.ts
similarity index 61%
rename from src/lib/presentation-boundary.test.ts
rename to apps/web/src/lib/presentation-boundary.test.ts
index 8e29ece..8a62205 100644
--- a/src/lib/presentation-boundary.test.ts
+++ b/apps/web/src/lib/presentation-boundary.test.ts
@@ -2,9 +2,17 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
+const appRoot = process.cwd().endsWith(`${join("apps", "web")}`)
+ ? process.cwd()
+ : join(process.cwd(), "apps", "web");
+const repoRoot = existsSync(join(process.cwd(), "turbo.json"))
+ ? process.cwd()
+ : join(appRoot, "..", "..");
const presentationRoots = [
- join(process.cwd(), "src", "lib", "ui"),
- join(process.cwd(), "src", "routes"),
+ join(repoRoot, "packages", "ui", "src"),
+ join(appRoot, "src", "App.tsx"),
+ join(appRoot, "src", "app.css"),
+ join(appRoot, "src", "lib", "ui-adapter"),
];
const forbiddenTerms = [
@@ -26,7 +34,9 @@ const forbiddenTerms = [
describe("presentation content boundary", () => {
test("keeps environment-specific content out of route and UI implementation", () => {
- const source = presentationRoots.map(readPresentationSource).join("\n").toLowerCase();
+ const source = withoutInternalPackageScope(
+ presentationRoots.map(readPresentationSource).join("\n").toLowerCase(),
+ );
expect(forbiddenTerms.filter((term) => source.includes(term))).toEqual([]);
});
@@ -34,7 +44,7 @@ describe("presentation content boundary", () => {
test("does not keep legacy presentation component files in the React runtime", () => {
const legacyExtension = [".sve", "lte"].join("");
- expect(findFiles(join(process.cwd(), "src"), legacyExtension)).toEqual([]);
+ expect(findFiles(join(appRoot, "src"), legacyExtension)).toEqual([]);
});
});
@@ -44,7 +54,9 @@ function readPresentationSource(path: string): string {
const stats = statSync(path);
if (stats.isFile()) {
if (path.endsWith(".test.ts")) return "";
- if (path.includes(`${join("src", "lib", "ui", "stories")}${"/"}`)) return "";
+ if (path.includes(`${join("packages", "ui", "src", "stories")}${"/"}`)) {
+ return "";
+ }
if (!/\.(tsx|ts|js|mjs|css|json)$/.test(path)) return "";
return readFileSync(path, "utf8");
}
@@ -60,3 +72,9 @@ function findFiles(path: string, extension: string): string[] {
return readdirSync(path).flatMap((entry) => findFiles(join(path, entry), extension));
}
+
+function withoutInternalPackageScope(source: string): string {
+ return source
+ .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
+ .replaceAll("@dimensionlab/ui", "@internal/ui");
+}
diff --git a/src/lib/server/agent-config/agent-config.test.ts b/apps/web/src/lib/server/agent-config/agent-config.test.ts
similarity index 99%
rename from src/lib/server/agent-config/agent-config.test.ts
rename to apps/web/src/lib/server/agent-config/agent-config.test.ts
index 87449e8..cdc60cf 100644
--- a/src/lib/server/agent-config/agent-config.test.ts
+++ b/apps/web/src/lib/server/agent-config/agent-config.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
-import type { DashboardDocument } from "$lib/model";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
import {
AgentConfigAuthorizationError,
diff --git a/src/lib/server/agent-config/index.ts b/apps/web/src/lib/server/agent-config/index.ts
similarity index 99%
rename from src/lib/server/agent-config/index.ts
rename to apps/web/src/lib/server/agent-config/index.ts
index ffa7dd8..ab3d0ae 100644
--- a/src/lib/server/agent-config/index.ts
+++ b/apps/web/src/lib/server/agent-config/index.ts
@@ -16,7 +16,7 @@ import {
type ServiceGroup,
type StatusItem,
type TelemetryCard,
-} from "$lib/model";
+} from "@dimensionlab/dashboard-model";
import {
createDashboardStore,
DashboardRevisionNotFoundError,
diff --git a/src/lib/server/dashboard.test.ts b/apps/web/src/lib/server/dashboard.test.ts
similarity index 89%
rename from src/lib/server/dashboard.test.ts
rename to apps/web/src/lib/server/dashboard.test.ts
index b15fa6c..5d93422 100644
--- a/src/lib/server/dashboard.test.ts
+++ b/apps/web/src/lib/server/dashboard.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
-import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { createDashboardStore, type DashboardStore } from "./db/dashboard-store";
import { loadDashboardRuntime } from "./dashboard";
@@ -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/src/lib/server/dashboard.ts b/apps/web/src/lib/server/dashboard.ts
similarity index 94%
rename from src/lib/server/dashboard.ts
rename to apps/web/src/lib/server/dashboard.ts
index 7821ee0..33afa3a 100644
--- a/src/lib/server/dashboard.ts
+++ b/apps/web/src/lib/server/dashboard.ts
@@ -1,5 +1,5 @@
-import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
-import type { DashboardDocument } from "$lib/model";
+import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import {
createDashboardStore,
DashboardPersistenceValidationError,
@@ -18,6 +18,9 @@ export interface DashboardRuntimeReady {
document: DashboardDocument;
schemaVersion: string;
currentRevisionId: string;
+ liveDatasourceHydration?: {
+ enabled: boolean;
+ };
}
export interface DashboardRuntimeEmpty {
diff --git a/src/lib/server/datasources/dashboard-datasources.test.ts b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
similarity index 73%
rename from src/lib/server/datasources/dashboard-datasources.test.ts
rename to apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
index 19082a2..d24b48b 100644
--- a/src/lib/server/datasources/dashboard-datasources.test.ts
+++ b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts
@@ -2,8 +2,8 @@ import { describe, expect, test, vi } from "vitest";
import {
DASHBOARD_SCHEMA_VERSION,
type DashboardDocument,
-} from "$lib/model";
-import { resolveDashboardDatasources } from ".";
+} from "@dimensionlab/dashboard-model";
+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/src/lib/server/datasources/index.ts b/apps/web/src/lib/server/datasources/index.ts
similarity index 73%
rename from src/lib/server/datasources/index.ts
rename to apps/web/src/lib/server/datasources/index.ts
index 804fbb5..62df13a 100644
--- a/src/lib/server/datasources/index.ts
+++ b/apps/web/src/lib/server/datasources/index.ts
@@ -8,10 +8,40 @@ import type {
StatusItem,
StatusStrip,
TelemetryCard,
-} from "$lib/model";
+} 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/src/lib/server/db/connection.ts b/apps/web/src/lib/server/db/connection.ts
similarity index 100%
rename from src/lib/server/db/connection.ts
rename to apps/web/src/lib/server/db/connection.ts
diff --git a/src/lib/server/db/dashboard-store.test.ts b/apps/web/src/lib/server/db/dashboard-store.test.ts
similarity index 97%
rename from src/lib/server/db/dashboard-store.test.ts
rename to apps/web/src/lib/server/db/dashboard-store.test.ts
index ecb80f3..c2717f5 100644
--- a/src/lib/server/db/dashboard-store.test.ts
+++ b/apps/web/src/lib/server/db/dashboard-store.test.ts
@@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
-import type { DashboardDocument } from "$lib/model";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import {
DashboardPersistenceValidationError,
createDashboardStore,
diff --git a/src/lib/server/db/dashboard-store.ts b/apps/web/src/lib/server/db/dashboard-store.ts
similarity index 99%
rename from src/lib/server/db/dashboard-store.ts
rename to apps/web/src/lib/server/db/dashboard-store.ts
index 92c7a73..3928206 100644
--- a/src/lib/server/db/dashboard-store.ts
+++ b/apps/web/src/lib/server/db/dashboard-store.ts
@@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm";
import {
type DashboardDocument,
type DashboardValidationFailure,
-} from "$lib/model";
+} from "@dimensionlab/dashboard-model";
import {
type DashboardDatabaseConnection,
openDashboardDatabase,
diff --git a/src/lib/server/db/migrations.ts b/apps/web/src/lib/server/db/migrations.ts
similarity index 100%
rename from src/lib/server/db/migrations.ts
rename to apps/web/src/lib/server/db/migrations.ts
diff --git a/src/lib/server/db/model-migrations.test.ts b/apps/web/src/lib/server/db/model-migrations.test.ts
similarity index 87%
rename from src/lib/server/db/model-migrations.test.ts
rename to apps/web/src/lib/server/db/model-migrations.test.ts
index eb16b8f..5c863a0 100644
--- a/src/lib/server/db/model-migrations.test.ts
+++ b/apps/web/src/lib/server/db/model-migrations.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
-import { DASHBOARD_SCHEMA_VERSION } from "$lib/model";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import { DASHBOARD_SCHEMA_VERSION } from "@dimensionlab/dashboard-model";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import {
UnsupportedDashboardModelVersionError,
migrateDashboardDocumentForPersistence,
diff --git a/src/lib/server/db/model-migrations.ts b/apps/web/src/lib/server/db/model-migrations.ts
similarity index 97%
rename from src/lib/server/db/model-migrations.ts
rename to apps/web/src/lib/server/db/model-migrations.ts
index 92550e6..72e9c47 100644
--- a/src/lib/server/db/model-migrations.ts
+++ b/apps/web/src/lib/server/db/model-migrations.ts
@@ -3,7 +3,7 @@ import {
validateDashboardDocument,
type DashboardDocument,
type DashboardValidationFailure,
-} from "$lib/model";
+} from "@dimensionlab/dashboard-model";
export interface DashboardModelMigrationSuccess {
valid: true;
diff --git a/src/lib/server/db/schema.ts b/apps/web/src/lib/server/db/schema.ts
similarity index 94%
rename from src/lib/server/db/schema.ts
rename to apps/web/src/lib/server/db/schema.ts
index c148035..a3a7206 100644
--- a/src/lib/server/db/schema.ts
+++ b/apps/web/src/lib/server/db/schema.ts
@@ -1,4 +1,4 @@
-import type { DashboardDocument } from "$lib/model";
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const dashboardDocuments = sqliteTable("dashboard_documents", {
diff --git a/src/lib/testing/external-api-mocks.test.ts b/apps/web/src/lib/testing/external-api-mocks.test.ts
similarity index 100%
rename from src/lib/testing/external-api-mocks.test.ts
rename to apps/web/src/lib/testing/external-api-mocks.test.ts
diff --git a/src/lib/testing/external-api-mocks.ts b/apps/web/src/lib/testing/external-api-mocks.ts
similarity index 100%
rename from src/lib/testing/external-api-mocks.ts
rename to apps/web/src/lib/testing/external-api-mocks.ts
diff --git a/src/lib/ui/model-renderer.test.ts b/apps/web/src/lib/ui-adapter/model-renderer.test.ts
similarity index 82%
rename from src/lib/ui/model-renderer.test.ts
rename to apps/web/src/lib/ui-adapter/model-renderer.test.ts
index 62942ee..2f8178d 100644
--- a/src/lib/ui/model-renderer.test.ts
+++ b/apps/web/src/lib/ui-adapter/model-renderer.test.ts
@@ -1,13 +1,15 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
-import {
- type DashboardDocument,
-} from "$lib/model";
-import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import { type DashboardDocument } from "@dimensionlab/dashboard-model";
+import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { dashboardDocumentToUiDashboard } from "./model-renderer";
+const appRoot = process.cwd().endsWith(`${join("apps", "web")}`)
+ ? process.cwd()
+ : join(process.cwd(), "apps", "web");
+
describe("dashboard model renderer", () => {
test("projects the Dimension Lab model into UI component props", () => {
const dashboard = dashboardDocumentToUiDashboard(dimensionLabDashboardFixture);
@@ -86,7 +88,13 @@ describe("dashboard model renderer", () => {
});
test("does not hardcode environment-specific content in mapper source", () => {
- const source = readFileSync(join(process.cwd(), "src/lib/ui/model-renderer.ts"), "utf8").toLowerCase();
+ const source = readFileSync(
+ join(appRoot, "src/lib/ui-adapter/model-renderer.ts"),
+ "utf8",
+ )
+ .toLowerCase()
+ .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model")
+ .replaceAll("@dimensionlab/ui", "@internal/ui");
expect(source).not.toContain("dimension");
expect(source).not.toContain("vaultwarden");
diff --git a/src/lib/ui/model-renderer.ts b/apps/web/src/lib/ui-adapter/model-renderer.ts
similarity index 97%
rename from src/lib/ui/model-renderer.ts
rename to apps/web/src/lib/ui-adapter/model-renderer.ts
index d5f3647..7ac1912 100644
--- a/src/lib/ui/model-renderer.ts
+++ b/apps/web/src/lib/ui-adapter/model-renderer.ts
@@ -6,7 +6,7 @@ import type {
StatusItem,
StatusStrip,
TelemetryCard,
-} from "$lib/model";
+} from "@dimensionlab/dashboard-model";
import type {
UiDashboardPreview,
UiModuleBlock,
@@ -14,7 +14,7 @@ import type {
UiServiceRow,
UiStatusItem,
UiTelemetryCard,
-} from "./types";
+} from "@dimensionlab/ui";
export function dashboardDocumentToUiDashboard(
document: DashboardDocument,
diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts
new file mode 100644
index 0000000..58cf203
--- /dev/null
+++ b/apps/web/src/lib/workspace-boundary.test.ts
@@ -0,0 +1,578 @@
+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";
+
+const root = existsSync(join(process.cwd(), "turbo.json"))
+ ? process.cwd()
+ : join(process.cwd(), "..", "..");
+
+describe("workspace boundaries", () => {
+ test("declares the root as a turbo-managed bun workspace", () => {
+ const packageJson = JSON.parse(
+ readFileSync(join(root, "package.json"), "utf8"),
+ ) as {
+ private?: boolean;
+ scripts?: Record;
+ workspaces?: string[];
+ };
+
+ expect(packageJson.private).toBe(true);
+ expect(packageJson.workspaces).toEqual(["apps/*", "packages/*"]);
+ expect(packageJson.scripts?.build).toBe("turbo run build");
+ expect(existsSync(join(root, "turbo.json"))).toBe(true);
+ });
+
+ test("keeps release orchestration in the root turbo task graph", () => {
+ const rootPackage = JSON.parse(
+ readFileSync(join(root, "package.json"), "utf8"),
+ ) as {
+ scripts?: Record;
+ };
+ const webPackage = JSON.parse(
+ readFileSync(join(root, "apps/web/package.json"), "utf8"),
+ ) as { scripts?: Record };
+ const turboConfig = JSON.parse(
+ readFileSync(join(root, "turbo.json"), "utf8"),
+ ) as {
+ globalDependencies?: string[];
+ tasks?: Record;
+ };
+
+ expect(rootPackage.scripts?.["test:qa"]).toBe(
+ "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"]),
+ );
+ expect(turboConfig.tasks?.["test:e2e"]?.env).toEqual(
+ expect.arrayContaining([
+ "CI",
+ "PLAYWRIGHT_DATABASE_URL",
+ "PLAYWRIGHT_PORT",
+ "PLAYWRIGHT_STORYBOOK_PORT",
+ ]),
+ );
+ });
+
+ test("lets turbo build app and Storybook artifacts before e2e serves them", () => {
+ const playwrightConfig = readFileSync(
+ join(root, "apps/web/playwright.config.ts"),
+ "utf8",
+ );
+ const turboConfig = JSON.parse(
+ readFileSync(join(root, "turbo.json"), "utf8"),
+ ) as {
+ tasks?: Record;
+ };
+
+ expect(playwrightConfig).not.toContain("bun run build &&");
+ expect(playwrightConfig).not.toContain("bun run build-storybook");
+ expect(turboConfig.tasks?.["test:e2e"]?.dependsOn).toEqual(
+ expect.arrayContaining(["@dimensionlab/ui#build-storybook"]),
+ );
+ });
+
+ test("accounts for local env files in cacheable Vite and Storybook task hashes", () => {
+ const turboConfig = JSON.parse(
+ readFileSync(join(root, "turbo.json"), "utf8"),
+ ) as {
+ tasks?: Record;
+ };
+
+ for (const taskName of ["build", "build-storybook"]) {
+ expect(turboConfig.tasks?.[taskName]?.inputs).toEqual([
+ "$TURBO_DEFAULT$",
+ ".env*",
+ ]);
+ }
+ });
+
+ test("keeps the website app and reusable UI library as separate packages", () => {
+ const webPackage = JSON.parse(
+ readFileSync(join(root, "apps/web/package.json"), "utf8"),
+ ) as { dependencies?: Record; name?: string };
+ const uiPackage = JSON.parse(
+ readFileSync(join(root, "packages/ui/package.json"), "utf8"),
+ ) as { exports?: Record; name?: string };
+
+ expect(webPackage.name).toBe("@dimensionlab/web");
+ expect(webPackage.dependencies?.["@dimensionlab/ui"]).toBe("workspace:*");
+ expect(uiPackage.name).toBe("@dimensionlab/ui");
+ expect(uiPackage.exports).toHaveProperty(".");
+ expect(uiPackage.exports).toHaveProperty("./styles.css");
+ });
+
+ test("keeps the dashboard model in a reusable internal package", () => {
+ const webPackage = JSON.parse(
+ readFileSync(join(root, "apps/web/package.json"), "utf8"),
+ ) as { dependencies?: Record };
+ const modelPackage = JSON.parse(
+ readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"),
+ ) as { exports?: Record; name?: string };
+ const tsconfig = JSON.parse(
+ readFileSync(join(root, "tsconfig.json"), "utf8"),
+ ) as { references?: Array<{ path: string }> };
+
+ expect(webPackage.dependencies?.["@dimensionlab/dashboard-model"]).toBe(
+ "workspace:*",
+ );
+ expect(modelPackage.name).toBe("@dimensionlab/dashboard-model");
+ expect(modelPackage.exports).toHaveProperty(".");
+ expect(modelPackage.exports).toHaveProperty("./fixtures");
+ expect(tsconfig.references).toEqual(
+ expect.arrayContaining([{ path: "./packages/dashboard-model" }]),
+ );
+ expect(existsSync(join(root, "apps/web/src/lib/model/index.ts"))).toBe(
+ false,
+ );
+ });
+
+ test("consumes workspace packages through package exports instead of source aliases", () => {
+ const webTsconfig = JSON.parse(
+ readFileSync(join(root, "apps/web/tsconfig.json"), "utf8"),
+ ) as { compilerOptions?: { paths?: Record } };
+ const modelPackage = JSON.parse(
+ readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"),
+ ) as { exports?: Record };
+ const uiPackage = JSON.parse(
+ readFileSync(join(root, "packages/ui/package.json"), "utf8"),
+ ) as { exports?: Record };
+ const viteConfig = readFileSync(join(root, "apps/web/vite.config.ts"), "utf8");
+ const turboConfig = JSON.parse(
+ readFileSync(join(root, "turbo.json"), "utf8"),
+ ) as { tasks?: Record };
+
+ expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty(
+ "@dimensionlab/dashboard-model",
+ );
+ expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty(
+ "@dimensionlab/dashboard-model/fixtures",
+ );
+ expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty("@dimensionlab/ui");
+ expect(webTsconfig.compilerOptions?.paths).not.toHaveProperty(
+ "@dimensionlab/ui/styles.css",
+ );
+ expect(webTsconfig.compilerOptions?.paths).toEqual({
+ "$lib/*": ["src/lib/*"],
+ });
+ expect(viteConfig).not.toContain("../../packages/dashboard-model/src");
+ expect(viteConfig).not.toContain("../../packages/ui/src");
+ expect(turboConfig.tasks?.dev?.dependsOn).toEqual(["^build"]);
+ expectPackageExport(modelPackage.exports?.["."], {
+ types: "./dist/index.d.ts",
+ development: "./src/index.ts",
+ default: "./dist/index.js",
+ });
+ expectPackageExport(modelPackage.exports?.["./fixtures"], {
+ types: "./dist/fixtures/index.d.ts",
+ development: "./src/fixtures/index.ts",
+ default: "./dist/fixtures/index.js",
+ });
+ expectPackageExport(uiPackage.exports?.["."], {
+ types: "./dist/index.d.ts",
+ development: "./src/index.ts",
+ default: "./dist/index.js",
+ });
+ expectPackageExport(uiPackage.exports?.["./styles.css"], {
+ development: "./src/styles.css",
+ default: "./dist/styles.css",
+ });
+ expectPackageExport(uiPackage.exports?.["./tokens.css"], {
+ development: "./src/tokens.css",
+ default: "./dist/tokens.css",
+ });
+ });
+
+ test("builds the internal container from a turbo-pruned web workspace", () => {
+ const containerfile = readFileSync(
+ join(root, "apps/web/Containerfile"),
+ "utf8",
+ );
+ const pruneCommand = "turbo prune @dimensionlab/web --docker";
+ const jsonCopy = "COPY --from=pruner /repo/out/json/ ./";
+ 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 --ignore-scripts";
+
+ expect(containerfile).toContain("AS pruner");
+ expect(containerfile).toContain(pruneCommand);
+ expect(containerfile).toContain(jsonCopy);
+ expect(containerfile).toContain(sourceCopy);
+ expect(containerfile).toContain(tsconfigCopy);
+ expect(containerfile).not.toContain(
+ "COPY packages/dashboard-model/package.json",
+ );
+ expect(containerfile).not.toContain("COPY packages/ui/package.json");
+
+ const jsonCopyIndex = containerfile.indexOf(jsonCopy);
+ const installIndex = containerfile.indexOf(installCommand);
+ const sourceCopyIndex = containerfile.indexOf(sourceCopy);
+ const tsconfigCopyIndex = containerfile.indexOf(tsconfigCopy);
+
+ expect(jsonCopyIndex).toBeGreaterThan(-1);
+ expect(installIndex).toBeGreaterThan(jsonCopyIndex);
+ expect(sourceCopyIndex).toBeGreaterThan(installIndex);
+ expect(tsconfigCopyIndex).toBeGreaterThan(sourceCopyIndex);
+ expect(containerfile.indexOf("RUN bun run build")).toBeGreaterThan(
+ tsconfigCopyIndex,
+ );
+ });
+
+ test("keeps local turbo prune output out of git and container contexts", () => {
+ const webPackage = JSON.parse(
+ readFileSync(join(root, "apps/web/package.json"), "utf8"),
+ ) as { scripts?: Record };
+ const gitignore = readFileSync(join(root, ".gitignore"), "utf8");
+ const containerignore = readFileSync(join(root, ".containerignore"), "utf8");
+ const containerIgnoreRules = new Set(
+ containerignore
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean),
+ );
+
+ expect(gitignore).toContain("out/");
+ expect(containerignore).toContain("out");
+ expect([...containerIgnoreRules]).toEqual(
+ expect.arrayContaining([
+ "apps/*/.turbo",
+ "apps/*/build",
+ "apps/*/data",
+ "apps/*/dist",
+ "apps/*/playwright-report",
+ "apps/*/test-results",
+ "packages/*/.turbo",
+ "packages/*/dist",
+ "packages/*/storybook-static",
+ ]),
+ );
+ expect(webPackage.scripts?.build).toBe(
+ "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 =
+ | string
+ | {
+ types?: string;
+ development?: string;
+ default?: string;
+ };
+
+function expectPackageExport(
+ actual: WorkspacePackageExport | undefined,
+ expected: Exclude,
+): 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/src/main.tsx b/apps/web/src/main.tsx
similarity index 100%
rename from src/main.tsx
rename to apps/web/src/main.tsx
diff --git a/src/page.test.tsx b/apps/web/src/page.test.tsx
similarity index 97%
rename from src/page.test.tsx
rename to apps/web/src/page.test.tsx
index fc68147..2ec5869 100644
--- a/src/page.test.tsx
+++ b/apps/web/src/page.test.tsx
@@ -1,6 +1,6 @@
import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
-import { genericDashboardFixture } from "$lib/model/fixtures/generic";
+import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { AppStateView, resolveDocumentMetadata } from "./App";
describe("home page model renderer", () => {
diff --git a/src/server/dev.test.ts b/apps/web/src/server/dev.test.ts
similarity index 78%
rename from src/server/dev.test.ts
rename to apps/web/src/server/dev.test.ts
index f43f953..60031db 100644
--- a/src/server/dev.test.ts
+++ b/apps/web/src/server/dev.test.ts
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { createDevServerConfig } from "../../vite.config";
+import { apiServerArgs } from "./dev";
describe("local development runtime", () => {
test("starts the Bun API server together with the Vite dev server", () => {
@@ -22,4 +23,11 @@ describe("local development runtime", () => {
changeOrigin: true,
});
});
+
+ test("uses development package export conditions for the Bun API server", () => {
+ expect(apiServerArgs).toEqual([
+ "--conditions=development",
+ "src/server/index.ts",
+ ]);
+ });
});
diff --git a/src/server/dev.ts b/apps/web/src/server/dev.ts
similarity index 90%
rename from src/server/dev.ts
rename to apps/web/src/server/dev.ts
index c515240..fa4dc2f 100644
--- a/src/server/dev.ts
+++ b/apps/web/src/server/dev.ts
@@ -3,6 +3,10 @@ const webPort = process.env.PORT || "5173";
const apiHost = process.env.DASHBOARD_DEV_API_HOST || "127.0.0.1";
const apiPort = process.env.DASHBOARD_DEV_API_PORT || "5174";
const apiTarget = `http://${apiHost}:${apiPort}`;
+export const apiServerArgs = [
+ "--conditions=development",
+ "src/server/index.ts",
+] as const;
if (import.meta.main) {
runDevServers();
@@ -51,7 +55,7 @@ export function runDevServers(): void {
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));
- spawn("api server", [process.execPath, "src/server/index.ts"], {
+ spawn("api server", [process.execPath, ...apiServerArgs], {
HOST: apiHost,
PORT: apiPort,
});
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/src/server/index.ts b/apps/web/src/server/index.ts
similarity index 77%
rename from src/server/index.ts
rename to apps/web/src/server/index.ts
index 860d042..9cbcbd9 100644
--- a/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/src/server/routes/agent-dashboard.test.ts b/apps/web/src/server/routes/agent-dashboard.test.ts
similarity index 100%
rename from src/server/routes/agent-dashboard.test.ts
rename to apps/web/src/server/routes/agent-dashboard.test.ts
diff --git a/src/server/routes/agent-dashboard.ts b/apps/web/src/server/routes/agent-dashboard.ts
similarity index 100%
rename from src/server/routes/agent-dashboard.ts
rename to apps/web/src/server/routes/agent-dashboard.ts
diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts
new file mode 100644
index 0000000..a4cf7ef
--- /dev/null
+++ b/apps/web/src/server/routes/dashboard.test.ts
@@ -0,0 +1,799 @@
+import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
+import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
+import {
+ createDashboardTileCache,
+ dashboardTileCacheKey,
+ handleDashboardEventsRoute,
+ handleDashboardTilesRoute,
+ handleDashboardTileRoute,
+ loadDashboardResponse,
+ loadDashboardTilesResponse,
+ loadDashboardTileResponse,
+ type DashboardTileResolutionLogEvent,
+} from "./dashboard";
+
+describe("dashboard API route", () => {
+ 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({
+ refreshSeedDocument: true,
+ seedIfEmpty: true,
+ });
+
+ expect(response.state).toBe("ready");
+ if (response.state !== "ready") throw new Error("expected ready dashboard");
+ 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
new file mode 100644
index 0000000..c42227c
--- /dev/null
+++ b/apps/web/src/server/routes/dashboard.ts
@@ -0,0 +1,609 @@
+import type { DashboardDocument } from "@dimensionlab/dashboard-model";
+import {
+ loadDashboardRuntime,
+ type DashboardRuntimeOptions,
+ type DashboardRuntimeState,
+} from "$lib/server/dashboard";
+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,
+ seedDocument: options.seedDocument,
+ });
+
+ if (dashboard.state !== "ready") {
+ 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, 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/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts
similarity index 100%
rename from src/vite-env.d.ts
rename to apps/web/src/vite-env.d.ts
diff --git a/static/mockServiceWorker.js b/apps/web/static/mockServiceWorker.js
similarity index 100%
rename from static/mockServiceWorker.js
rename to apps/web/static/mockServiceWorker.js
diff --git a/tests/e2e/dashboard.spec.ts b/apps/web/tests/e2e/dashboard.spec.ts
similarity index 70%
rename from tests/e2e/dashboard.spec.ts
rename to apps/web/tests/e2e/dashboard.spec.ts
index b432d14..3eb0944 100644
--- a/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,11 +103,66 @@ 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([]);
});
+ test("keeps intermediate viewports scrollable without horizontal clipping", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-desktop");
+
+ for (const viewport of [
+ { width: 900, height: 956 },
+ { width: 1024, height: 768 },
+ ]) {
+ await page.setViewportSize(viewport);
+ await page.goto("/");
+ await waitForDashboardReady(page);
+
+ const metrics = await page.evaluate(() => {
+ const footer = document.querySelector("[data-model-id='footer-status']");
+ const trackedElements = [
+ document.querySelector(".dashboard-frame__header"),
+ ...document.querySelectorAll(".telemetry-card"),
+ ...document.querySelectorAll(".service-panel"),
+ document.querySelector("[data-model-id='runtime-health']"),
+ footer,
+ ].filter((element): element is Element => Boolean(element));
+
+ const footerRect = footer?.getBoundingClientRect();
+ const clippedRight = trackedElements
+ .map((element) => {
+ const rect = element.getBoundingClientRect();
+ return {
+ id: element.getAttribute("data-model-id") || element.className,
+ right: rect.right,
+ width: rect.width,
+ };
+ })
+ .filter((item) => item.right > window.innerWidth + 1 || item.width <= 0);
+
+ return {
+ clippedRight,
+ footerBottomInDocument: (footerRect?.bottom ?? 0) + window.scrollY,
+ frameOverflow: window.getComputedStyle(
+ document.querySelector(".dashboard-frame") as Element,
+ ).overflow,
+ scrollHeight: document.documentElement.scrollHeight,
+ scrollWidth: document.documentElement.scrollWidth,
+ };
+ });
+
+ expect(metrics.scrollWidth).toBeLessThanOrEqual(viewport.width);
+ expect(metrics.scrollHeight).toBeGreaterThanOrEqual(
+ Math.ceil(metrics.footerBottomInDocument),
+ );
+ expect(metrics.frameOverflow).not.toBe("hidden");
+ expect(metrics.clippedRight).toEqual([]);
+ }
+ });
+
test("renders bookmark rows as a compact divider list", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop");
@@ -209,6 +263,15 @@ test.describe("dashboard page QA gate", () => {
await waitForDashboardReady(page);
await expect(page.getByRole("main")).toHaveCount(1);
+ await page.keyboard.press("Tab");
+ const themeToggle = page.getByRole("button", { name: "Light theme" });
+ await expect(themeToggle).toBeFocused();
+ await expect(themeToggle).toHaveAttribute("aria-pressed", "false");
+ const themeFocusBoxShadow = await themeToggle.evaluate((element) => {
+ return window.getComputedStyle(element).boxShadow;
+ });
+ expect(themeFocusBoxShadow).not.toBe("none");
+
for (const serviceId of linkedServiceIds) {
await page.keyboard.press("Tab");
@@ -230,6 +293,28 @@ test.describe("dashboard page QA gate", () => {
expect(results.violations).toEqual([]);
});
+ test("passes automated accessibility checks in light mode", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-desktop");
+
+ await page.goto("/");
+ await waitForDashboardReady(page);
+ await page.getByRole("button", { name: "Light theme" }).click();
+ await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
+ await expect(page.getByRole("button", { name: "Light theme" })).toHaveAttribute(
+ "aria-pressed",
+ "true",
+ );
+
+ const results = await new AxeBuilder({ page }).analyze();
+ expect(results.violations).toEqual([]);
+
+ await expect(page).toHaveScreenshot("dashboard-light-desktop.png", {
+ fullPage: true,
+ });
+ });
+
test("honors reduced-motion preferences", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
@@ -255,6 +340,31 @@ test.describe("dashboard page QA gate", () => {
0.01,
);
});
+
+ test("toggles the dashboard between dark and light themes", async ({ page }) => {
+ await page.goto("/");
+ await waitForDashboardReady(page);
+
+ await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "dark");
+
+ const switchToLight = page.getByRole("button", {
+ name: "Light theme",
+ });
+ await expect(switchToLight).toBeVisible();
+ await expect(switchToLight).toHaveAttribute("aria-pressed", "false");
+ await switchToLight.click();
+
+ await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
+ await expect(page.getByRole("button", { name: "Light theme" })).toHaveAttribute(
+ "aria-pressed",
+ "true",
+ );
+
+ await page.reload();
+ await waitForDashboardReady(page);
+
+ await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
+ });
});
async function waitForDashboardReady(page: Page): Promise {
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
new file mode 100644
index 0000000..a1c0ada
Binary files /dev/null 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
new file mode 100644
index 0000000..f5d5163
Binary files /dev/null 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
new file mode 100644
index 0000000..48293a4
Binary files /dev/null and b/apps/web/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png differ
diff --git a/apps/web/tests/e2e/storybook-server.ts b/apps/web/tests/e2e/storybook-server.ts
new file mode 100644
index 0000000..7544654
--- /dev/null
+++ b/apps/web/tests/e2e/storybook-server.ts
@@ -0,0 +1,35 @@
+import { existsSync } from "node:fs";
+import { resolve, sep } from "node:path";
+
+const root = resolve(process.cwd(), "packages/ui/storybook-static");
+const port = Number(process.env.STORYBOOK_STATIC_PORT || 6007);
+
+if (!existsSync(resolve(root, "iframe.html"))) {
+ throw new Error(
+ "packages/ui/storybook-static is missing. Run bun run build-storybook first.",
+ );
+}
+
+Bun.serve({
+ hostname: "127.0.0.1",
+ port,
+ async fetch(request) {
+ const url = new URL(request.url);
+ const pathname = decodeURIComponent(url.pathname);
+ const relativePath = pathname === "/" ? "/index.html" : pathname;
+ const filePath = resolve(root, `.${relativePath}`);
+
+ if (!filePath.startsWith(`${root}${sep}`)) {
+ return new Response("Forbidden", { status: 403 });
+ }
+
+ const file = Bun.file(filePath);
+ if (!(await file.exists())) {
+ return new Response("Not found", { status: 404 });
+ }
+
+ return new Response(file);
+ },
+});
+
+console.log(`Storybook static listening on http://127.0.0.1:${port}`);
diff --git a/apps/web/tests/e2e/storybook.spec.ts b/apps/web/tests/e2e/storybook.spec.ts
new file mode 100644
index 0000000..47a58ba
--- /dev/null
+++ b/apps/web/tests/e2e/storybook.spec.ts
@@ -0,0 +1,40 @@
+import { expect, test } from "@playwright/test";
+
+const storybookPort = Number(process.env.PLAYWRIGHT_STORYBOOK_PORT || 6007);
+const storybookURL = `http://127.0.0.1:${storybookPort}`;
+
+test.describe("Storybook theme QA", () => {
+ test("renders the ThemeToggle light story on the light canvas", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-desktop");
+
+ const consoleMessages: string[] = [];
+ page.on("console", (message) => {
+ if (["error", "warning"].includes(message.type())) {
+ consoleMessages.push(message.text());
+ }
+ });
+
+ const url = `${storybookURL}/iframe.html?id=ui-themetoggle--light&viewMode=story`;
+ await page.goto(url, { waitUntil: "networkidle" });
+
+ await expect(page.locator("html")).toHaveAttribute("data-ui-theme", "light");
+ await expect(
+ page.getByRole("button", { name: "Light theme" }),
+ ).toBeVisible();
+ await expect(page.getByRole("button", { name: "Light theme" })).toHaveAttribute(
+ "aria-pressed",
+ "true",
+ );
+ await expect(page.locator("body")).toHaveCSS(
+ "background-color",
+ "rgb(238, 242, 231)",
+ );
+ expect(
+ consoleMessages.filter((message) =>
+ message.includes("Global args/argTypes can only be set globally"),
+ ),
+ ).toEqual([]);
+ });
+});
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..120931b
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "$lib/*": ["src/lib/*"]
+ },
+ "types": ["node", "bun-types", "react", "react-dom", "vite/client"]
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "tests/**/*.ts",
+ "vite.config.ts",
+ "playwright.config.ts",
+ "drizzle.config.ts"
+ ],
+ "exclude": ["build", "dist", "node_modules"]
+}
diff --git a/vite.config.ts b/apps/web/vite.config.ts
similarity index 100%
rename from vite.config.ts
rename to apps/web/vite.config.ts
diff --git a/bun.lock b/bun.lock
index 36ab7d6..d6915d0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -4,45 +4,92 @@
"workspaces": {
"": {
"name": "dimensionlab-website",
+ "devDependencies": {
+ "turbo": "^2.5.0",
+ },
+ },
+ "apps/web": {
+ "name": "@dimensionlab/web",
+ "version": "0.0.1",
"dependencies": {
- "@fontsource-variable/geist": "^5.2.9",
- "@iconify/react": "^6.0.2",
+ "@dimensionlab/dashboard-model": "workspace:*",
+ "@dimensionlab/ui": "workspace:*",
"@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
"drizzle-orm": "^0.45.2",
- "lucide-react": "^1.21.0",
- "radix-ui": "^1.6.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
- "tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
- "uplot": "^1.6.32",
},
"devDependencies": {
"@axe-core/playwright": "^4.11.3",
"@playwright/test": "^1.61.0",
- "@storybook/addon-a11y": "^10.4.6",
- "@storybook/addon-vitest": "^10.4.6",
- "@storybook/react-vite": "^10.4.6",
"@tailwindcss/vite": "^4.3.1",
"@types/bun": "^1.3.14",
"@types/node": "^25.9.3",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
+ "bun-types": "^1.3.14",
"drizzle-kit": "^0.31.10",
"msw": "^2.14.6",
"shadcn": "^4.11.0",
- "storybook": "^10.4.6",
"tailwindcss": "^4.3.1",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.9",
},
},
+ "packages/dashboard-model": {
+ "name": "@dimensionlab/dashboard-model",
+ "version": "0.0.1",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.49",
+ "ajv": "^8.20.0",
+ "ajv-formats": "^3.0.1",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.14",
+ "@types/node": "^25.9.3",
+ "bun-types": "^1.3.14",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9",
+ },
+ },
+ "packages/ui": {
+ "name": "@dimensionlab/ui",
+ "version": "0.0.1",
+ "dependencies": {
+ "@fontsource-variable/geist": "^5.2.9",
+ "@iconify/react": "^6.0.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.21.0",
+ "radix-ui": "^1.6.0",
+ "tailwind-merge": "^3.6.0",
+ "tw-animate-css": "^1.4.0",
+ "uplot": "^1.6.32",
+ },
+ "devDependencies": {
+ "@storybook/addon-a11y": "^10.4.6",
+ "@storybook/addon-vitest": "^10.4.6",
+ "@storybook/react-vite": "^10.4.6",
+ "@types/bun": "^1.3.14",
+ "@types/node": "^25.9.3",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "bun-types": "^1.3.14",
+ "storybook": "^10.4.6",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.16",
+ "vitest": "^4.1.9",
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ },
+ },
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
@@ -107,6 +154,12 @@
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+ "@dimensionlab/dashboard-model": ["@dimensionlab/dashboard-model@workspace:packages/dashboard-model"],
+
+ "@dimensionlab/ui": ["@dimensionlab/ui@workspace:packages/ui"],
+
+ "@dimensionlab/web": ["@dimensionlab/web@workspace:apps/web"],
+
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.74.3", "", { "dependencies": { "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-+VjTIiOCApjB0K4a41+SkN18gTetmhU9UN2JD8LHeNUqo/38FmtgvdtWsW/WWN1dyYRGS7XKeAAb/ruy7x1tRw=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
@@ -123,57 +176,57 @@
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
- "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
- "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
- "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
- "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
- "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
- "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
- "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
- "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
- "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
- "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
- "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
- "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
- "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
- "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
- "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
- "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
- "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
- "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
- "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
- "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
- "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
- "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
- "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
- "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
- "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
- "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
@@ -587,6 +640,18 @@
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
+ "@turbo/darwin-64": ["@turbo/darwin-64@2.9.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw=="],
+
+ "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ=="],
+
+ "@turbo/linux-64": ["@turbo/linux-64@2.9.18", "", { "os": "linux", "cpu": "x64" }, "sha512-zCdIDtz69AnbYh913elJRRoF3QY5aa2HNnf+4rAkc7bQ+tWujiDkCNV7stazOUPggaDvhKIf2Z87qHftTeXSkw=="],
+
+ "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-Va1kXI04naMgYwqv/5Dfa36dTDx8015U7oaQAjrXa45ua9OoFjSV4OmvkML4EmXvUclQHCiBRbY8bvd0jV7eAg=="],
+
+ "@turbo/windows-64": ["@turbo/windows-64@2.9.18", "", { "os": "win32", "cpu": "x64" }, "sha512-m0kDhZANxSNz9ck1ybogFscHabriAsp4eDFNrN/1H5WrgTF7b3VlcPZnhuO3v2+E2KnCbeAc+UUT10BZZHdDKw=="],
+
+ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-nUdR8WqoomUys9iIQmG45TMiizJ+5BV8egSeLLZba/AWblyp3fVBcIH1kSE58OtK4g2YzbMJEth6Ttv9w5rqMA=="],
+
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
@@ -625,7 +690,7 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
- "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
+ "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="],
"@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="],
@@ -635,7 +700,7 @@
"@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="],
- "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+ "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
"@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="],
@@ -655,7 +720,7 @@
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
- "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
@@ -669,7 +734,7 @@
"atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="],
- "axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="],
+ "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
@@ -699,7 +764,7 @@
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
- "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@@ -819,7 +884,7 @@
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
- "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+ "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -827,7 +892,7 @@
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
- "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
@@ -945,7 +1010,7 @@
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
- "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
@@ -975,7 +1040,7 @@
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
- "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+ "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
@@ -1101,7 +1166,7 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
- "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
+ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
@@ -1331,6 +1396,8 @@
"tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="],
+ "turbo": ["turbo@2.9.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.18", "@turbo/darwin-arm64": "2.9.18", "@turbo/linux-64": "2.9.18", "@turbo/linux-arm64": "2.9.18", "@turbo/windows-64": "2.9.18", "@turbo/windows-arm64": "2.9.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg=="],
+
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="],
@@ -1387,7 +1454,7 @@
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
- "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
+ "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -1405,6 +1472,8 @@
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
+ "@axe-core/playwright/axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="],
+
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -1429,7 +1498,7 @@
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
- "@storybook/addon-a11y/axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
+ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="],
@@ -1447,14 +1516,6 @@
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
- "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
-
- "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
-
- "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
-
- "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
-
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -1467,12 +1528,12 @@
"dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
+ "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
- "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
-
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
@@ -1489,8 +1550,6 @@
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
@@ -1501,24 +1560,20 @@
"router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
- "storybook/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
+ "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
- "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
+ "storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
+
+ "storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
-
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
- "vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="],
-
- "vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="],
+ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
-
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@@ -1533,6 +1588,10 @@
"@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
+ "@dotenvx/dotenvx/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
+
+ "@dotenvx/dotenvx/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
@@ -1579,128 +1638,78 @@
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
- "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
-
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+ "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+
+ "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+
+ "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+
+ "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+
+ "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+
+ "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+
+ "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+
+ "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+
+ "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+
+ "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+
+ "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+
+ "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+
+ "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+
+ "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+
+ "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+
+ "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+ "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
- "storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
+ "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
- "storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
+ "storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
- "storybook/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
+ "storybook/@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
- "storybook/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
-
- "storybook/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
-
- "storybook/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
-
- "storybook/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
-
- "storybook/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
-
- "storybook/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
-
- "storybook/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
-
- "storybook/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
-
- "storybook/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
-
- "storybook/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
-
- "storybook/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
-
- "storybook/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
-
- "storybook/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
-
- "storybook/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
-
- "storybook/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
-
- "storybook/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
-
- "storybook/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
-
- "storybook/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
-
- "storybook/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
-
- "storybook/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
-
- "storybook/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
-
- "storybook/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
-
- "storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
-
- "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
+ "storybook/@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
-
- "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
-
- "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
-
- "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
-
- "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
-
- "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
-
- "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
-
- "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
-
- "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
-
- "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
-
- "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
-
- "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
-
- "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
-
- "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
-
- "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
-
- "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
-
- "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
-
- "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
-
- "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
-
- "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
-
- "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
-
- "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
-
- "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
-
- "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
-
- "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
-
- "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
-
- "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
-
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "storybook/open/wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
+ "storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
}
}
diff --git a/components.json b/components.json
deleted file mode 100644
index f8d8d59..0000000
--- a/components.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "radix-nova",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "src/app.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "iconLibrary": "lucide",
- "rtl": false,
- "aliases": {
- "components": "$lib/components",
- "utils": "$lib/utils",
- "ui": "$lib/components/ui",
- "lib": "$lib/.",
- "hooks": "$lib/hooks"
- },
- "menuColor": "default",
- "menuAccent": "subtle",
- "registries": {}
-}
diff --git a/docs/superpowers/plans/2026-06-20-turbo-component-library.md b/docs/superpowers/plans/2026-06-20-turbo-component-library.md
new file mode 100644
index 0000000..22d2ad8
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-20-turbo-component-library.md
@@ -0,0 +1,436 @@
+# Turbo Component Library Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Convert the single-package Dimension Lab website into a Turborepo workspace where `apps/web` consumes a compiled reusable React component library from `packages/ui`.
+
+**Architecture:** The root becomes a private Bun workspace with Turbo orchestration. `packages/ui` owns generic reusable UI components, CSS, theme helpers, Storybook, and package-level tests. `apps/web` owns the website runtime, model/server/database logic, the model-to-UI adapter, e2e tests, and deployment container.
+
+**Tech Stack:** Bun workspaces, Turborepo, Vite, React 19, TypeScript, Storybook React Vite, Tailwind CSS v4, shadcn CSS, Vitest, Playwright, Drizzle ORM, Bun SQLite.
+
+---
+
+## File Structure
+
+- Create root `turbo.json`: cacheable task graph for `build`, `check`, `test:unit`, `build-storybook`, `test:e2e`, and `test:qa`.
+- Create root `tsconfig.base.json`: shared strict TypeScript defaults.
+- Modify root `package.json`: private workspace root with `apps/*` and `packages/*`, Turbo scripts, and `turbo` dev dependency only.
+- Create `packages/ui/package.json`: compiled `@dimensionlab/ui` package with code and CSS exports.
+- Create `packages/ui/tsconfig.json` and `packages/ui/tsconfig.build.json`: package typecheck and declaration/JS build config.
+- Create `packages/ui/src/styles.css`: library style entry importing font, uPlot CSS, tokens, and component CSS.
+- Move `src/lib/ui/components/**` to `packages/ui/src/components/**`.
+- Move `src/lib/ui/stories/**` to `packages/ui/src/stories/**`.
+- Move `src/lib/ui/tokens.css` to `packages/ui/src/tokens.css`.
+- Move `src/lib/ui/theme.ts`, `types.ts`, `format.ts`, `fixtures.ts`, and `index.ts` to `packages/ui/src/**`.
+- Move `src/lib/ui/components/styles.css` to `packages/ui/src/components/styles.css`.
+- Move `.storybook/**` to `packages/ui/.storybook/**`.
+- Move unused shadcn primitives from `src/lib/components/ui/**` to `packages/ui/src/primitives/**` and update their `cn` import to package-local `src/utils.ts`.
+- Move `src/lib/utils.ts` to `packages/ui/src/utils.ts`.
+- Move app runtime files into `apps/web`: `src/App.tsx`, `src/main.tsx`, `src/app.css`, `src/server/**`, `src/lib/model/**`, `src/lib/server/**`, `src/lib/testing/**`, `src/vite-env.d.ts`, `src/page.test.tsx`, `src/server/dev.ts`, `tests/**`, `drizzle/**`, `Containerfile`, `index.html`, `playwright.config.ts`, `vite.config.ts`, `drizzle.config.ts`, and app-specific README/deployment files.
+- Move `src/lib/ui/model-renderer.ts` and `model-renderer.test.ts` into `apps/web/src/lib/ui-adapter/**`.
+- Create `apps/web/package.json`, `apps/web/tsconfig.json`, `apps/web/vite.config.ts`, and `apps/web/playwright.config.ts`.
+- Update `apps/web/src/App.tsx` to import components/types from `@dimensionlab/ui` and import the adapter from `$lib/ui-adapter/model-renderer`.
+- Update `apps/web/src/app.css` to import `@dimensionlab/ui/styles.css` instead of local UI CSS files.
+- Update tests that read paths so package boundary tests inspect `packages/ui` and app tests inspect `apps/web`.
+- Update `README.md` to describe the workspace commands, package boundaries, Storybook location, and deployment path.
+
+## Task 1: Workspace And Boundary Tests
+
+**Files:**
+- Modify: `package.json`
+- Create: `turbo.json`
+- Create: `tsconfig.base.json`
+- Create: `packages/ui/package.json`
+- Create: `packages/ui/tsconfig.json`
+- Create: `packages/ui/tsconfig.build.json`
+- Create: `apps/web/package.json`
+- Create: `apps/web/tsconfig.json`
+- Create: `apps/web/src/lib/workspace-boundary.test.ts`
+
+- [ ] **Step 1: Write the failing workspace boundary test**
+
+Create `apps/web/src/lib/workspace-boundary.test.ts`:
+
+```ts
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, test } from "vitest";
+
+const root = join(import.meta.dir, "../../../..");
+
+describe("workspace boundaries", () => {
+ test("declares the root as a turbo-managed bun workspace", () => {
+ const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
+ private?: boolean;
+ scripts?: Record;
+ workspaces?: string[];
+ };
+
+ expect(packageJson.private).toBe(true);
+ expect(packageJson.workspaces).toEqual(["apps/*", "packages/*"]);
+ expect(packageJson.scripts?.build).toBe("turbo build");
+ expect(existsSync(join(root, "turbo.json"))).toBe(true);
+ });
+
+ test("keeps the website app and reusable UI library as separate packages", () => {
+ const webPackage = JSON.parse(
+ readFileSync(join(root, "apps/web/package.json"), "utf8"),
+ ) as { dependencies?: Record; name?: string };
+ const uiPackage = JSON.parse(
+ readFileSync(join(root, "packages/ui/package.json"), "utf8"),
+ ) as { exports?: Record; name?: string };
+
+ expect(webPackage.name).toBe("@dimensionlab/web");
+ expect(webPackage.dependencies?.["@dimensionlab/ui"]).toBe("workspace:*");
+ expect(uiPackage.name).toBe("@dimensionlab/ui");
+ expect(uiPackage.exports).toHaveProperty(".");
+ expect(uiPackage.exports).toHaveProperty("./styles.css");
+ });
+});
+```
+
+- [ ] **Step 2: Run the test to verify it fails**
+
+Run:
+
+```sh
+bunx vitest run apps/web/src/lib/workspace-boundary.test.ts
+```
+
+Expected: FAIL because `apps/web`, `packages/ui`, and `turbo.json` do not exist.
+
+- [ ] **Step 3: Add minimal workspace manifests**
+
+Create root `package.json` as the workspace orchestrator:
+
+```json
+{
+ "name": "dimensionlab",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "packageManager": "bun@1.3.14",
+ "workspaces": ["apps/*", "packages/*"],
+ "scripts": {
+ "dev": "turbo dev --filter=@dimensionlab/web",
+ "build": "turbo build",
+ "preview": "bun run --cwd apps/web preview",
+ "storybook": "turbo storybook --filter=@dimensionlab/ui",
+ "build-storybook": "turbo build-storybook --filter=@dimensionlab/ui",
+ "check": "turbo check",
+ "test": "turbo test:unit",
+ "test:unit": "turbo test:unit",
+ "test:e2e": "turbo test:e2e --filter=@dimensionlab/web",
+ "test:qa": "turbo test:qa",
+ "db:generate": "bun run --cwd apps/web db:generate",
+ "db:check": "bun run --cwd apps/web db:check"
+ },
+ "devDependencies": {
+ "turbo": "^2.5.0"
+ }
+}
+```
+
+Create `turbo.json`:
+
+```json
+{
+ "$schema": "https://turbo.build/schema.json",
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**", "build/**"]
+ },
+ "check": {
+ "dependsOn": ["^build"],
+ "outputs": []
+ },
+ "test:unit": {
+ "dependsOn": ["^build"],
+ "outputs": []
+ },
+ "build-storybook": {
+ "dependsOn": ["^build"],
+ "outputs": ["storybook-static/**"]
+ },
+ "test:e2e": {
+ "dependsOn": ["build", "^build"],
+ "outputs": ["test-results/**", "playwright-report/**"]
+ },
+ "test:qa": {
+ "dependsOn": ["check", "test:unit", "build", "build-storybook", "test:e2e"],
+ "outputs": []
+ },
+ "dev": {
+ "cache": false,
+ "persistent": true
+ },
+ "storybook": {
+ "cache": false,
+ "persistent": true
+ }
+ }
+}
+```
+
+Create minimal `packages/ui/package.json` with `@dimensionlab/ui` exports and
+minimal `apps/web/package.json` with `@dimensionlab/ui` as a workspace
+dependency. Move the full dependency lists in Task 2 and Task 3.
+
+- [ ] **Step 4: Run the boundary test**
+
+Run:
+
+```sh
+bunx vitest run apps/web/src/lib/workspace-boundary.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```sh
+git add package.json turbo.json tsconfig.base.json apps/web/package.json apps/web/tsconfig.json apps/web/src/lib/workspace-boundary.test.ts packages/ui/package.json packages/ui/tsconfig.json packages/ui/tsconfig.build.json
+git commit -m "build: add turbo workspace manifests"
+```
+
+## Task 2: Extract The UI Package
+
+**Files:**
+- Move: `src/lib/ui/components/**` to `packages/ui/src/components/**`
+- Move: `src/lib/ui/stories/**` to `packages/ui/src/stories/**`
+- Move: `src/lib/ui/{index.ts,types.ts,theme.ts,format.ts,fixtures.ts,tokens.css}` to `packages/ui/src/**`
+- Move: `.storybook/**` to `packages/ui/.storybook/**`
+- Move: `src/lib/components/ui/**` to `packages/ui/src/primitives/**`
+- Move: `src/lib/utils.ts` to `packages/ui/src/utils.ts`
+- Create: `packages/ui/src/styles.css`
+- Modify: `packages/ui/src/index.ts`
+- Modify: `packages/ui/src/storybook.test.ts`
+- Modify: `packages/ui/src/content-boundary.test.ts`
+
+- [ ] **Step 1: Write the failing UI isolation assertion**
+
+Update the package boundary tests to read from `packages/ui/src` and assert no
+imports from `apps/web`, `$lib/server`, or `$lib/model` exist:
+
+```ts
+expect(source).not.toMatch(/from ["'](?:apps\/web|\$lib\/server|\$lib\/model)/);
+```
+
+Run:
+
+```sh
+bunx vitest run packages/ui/src/content-boundary.test.ts
+```
+
+Expected: FAIL until the files move and the app-specific adapter is removed.
+
+- [ ] **Step 2: Move generic UI files**
+
+Run mechanical moves with `git mv`. Move `model-renderer.ts` out of the UI
+package in Task 3 rather than into `packages/ui`.
+
+- [ ] **Step 3: Add the UI style entry**
+
+Create `packages/ui/src/styles.css`:
+
+```css
+@import "@fontsource-variable/geist";
+@import "uplot/dist/uPlot.min.css";
+@import "./tokens.css";
+@import "./components/styles.css";
+```
+
+- [ ] **Step 4: Make package imports relative or package-local**
+
+Update moved primitive files to import `cn` from `../utils`.
+Remove `dashboardDocumentToUiDashboard` from `packages/ui/src/index.ts`.
+
+- [ ] **Step 5: Build the package**
+
+Run:
+
+```sh
+bun run --cwd packages/ui build
+bun run --cwd packages/ui check
+bun run --cwd packages/ui test:unit
+```
+
+Expected: PASS and `packages/ui/dist` contains `index.js`, `index.d.ts`, and
+CSS files.
+
+- [ ] **Step 6: Commit**
+
+```sh
+git add packages/ui src/lib/ui src/lib/components src/lib/utils.ts
+git commit -m "refactor(ui): extract reusable component package"
+```
+
+## Task 3: Move The Web App Workspace
+
+**Files:**
+- Move: `src/**` app files that are not reusable UI to `apps/web/src/**`
+- Move: `tests/**` to `apps/web/tests/**`
+- Move: `drizzle/**` to `apps/web/drizzle/**`
+- Move: `Containerfile` to `apps/web/Containerfile`
+- Move: `index.html`, `vite.config.ts`, `playwright.config.ts`, `drizzle.config.ts` to `apps/web/**`
+- Create: `apps/web/src/lib/ui-adapter/model-renderer.ts`
+- Modify: `apps/web/src/App.tsx`
+- Modify: `apps/web/src/app.css`
+
+- [ ] **Step 1: Move the model adapter out of UI**
+
+Move `src/lib/ui/model-renderer.ts` to
+`apps/web/src/lib/ui-adapter/model-renderer.ts` and update imports from
+`$lib/model` plus UI types from `@dimensionlab/ui`.
+
+- [ ] **Step 2: Move app runtime and tests**
+
+Use `git mv` for app/server/model/test/deployment files into `apps/web`.
+
+- [ ] **Step 3: Update app imports**
+
+In `apps/web/src/App.tsx`, import UI components and types from
+`@dimensionlab/ui`, and import `dashboardDocumentToUiDashboard` from
+`$lib/ui-adapter/model-renderer`.
+
+In `apps/web/src/app.css`, replace local UI imports with:
+
+```css
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+@import "@dimensionlab/ui/styles.css";
+```
+
+- [ ] **Step 4: Run app typecheck and unit tests**
+
+Run:
+
+```sh
+bun run --cwd apps/web check
+bun run --cwd apps/web test:unit
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```sh
+git add apps/web src tests drizzle Containerfile index.html vite.config.ts playwright.config.ts drizzle.config.ts
+git commit -m "refactor(web): move website into app workspace"
+```
+
+## Task 4: Wire Turbo, Storybook, Playwright, And Container
+
+**Files:**
+- Modify: `apps/web/playwright.config.ts`
+- Modify: `apps/web/Containerfile`
+- Modify: `apps/web/vite.config.ts`
+- Modify: `packages/ui/.storybook/main.ts`
+- Modify: `packages/ui/.storybook/preview.ts`
+- Modify: `tests/e2e/storybook-server.ts` after move to `apps/web/tests/e2e/storybook-server.ts`
+- Modify: `README.md`
+
+- [ ] **Step 1: Update Playwright commands for workspaces**
+
+In `apps/web/playwright.config.ts`, make the web server command build from the
+workspace root or app directory consistently:
+
+```ts
+command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`
+```
+
+This command runs inside `apps/web` when invoked through the app package script.
+
+- [ ] **Step 2: Update Storybook package paths**
+
+`packages/ui/.storybook/main.ts` should use `../src/**/*.stories.@(js|ts|tsx)`.
+`packages/ui/.storybook/preview.ts` should import `../src/styles.css` and use
+generic MSW handlers only if they are moved into the UI package.
+
+- [ ] **Step 3: Update the container build**
+
+`apps/web/Containerfile` should build from the repository root context or copy
+only the workspace files it needs. Preserve the runtime command:
+
+```dockerfile
+CMD ["bun", "build/index.js"]
+```
+
+- [ ] **Step 4: Run full root checks**
+
+Run:
+
+```sh
+bun install
+bun run check
+bun run test:unit
+bun run build
+bun run build-storybook
+bun run test:e2e
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```sh
+git add README.md apps/web packages/ui package.json turbo.json bun.lock
+git commit -m "build: wire turbo workspace qa"
+```
+
+## Task 5: Final QA, PR, Review, Merge, Deploy
+
+**Files:**
+- No planned source edits unless verification finds blockers.
+
+- [ ] **Step 1: Run release gate**
+
+Run:
+
+```sh
+bun run test:qa
+```
+
+Expected: PASS.
+
+- [ ] **Step 2: Push and open PR**
+
+Run:
+
+```sh
+git push -u origin codex/turbo-component-library
+```
+
+Open a ready PR against `main` titled:
+
+```text
+refactor: migrate dashboard to turbo component library
+```
+
+- [ ] **Step 3: Independent review**
+
+Send an independent reviewer to inspect the PR diff against the objective:
+Turbo repo, `apps/web`, compiled `packages/ui`, reusable components removed
+from app, Storybook with UI package, root QA passing, no server behavior change.
+
+- [ ] **Step 4: Resolve blockers**
+
+For each blocking review finding, write or update a failing test first, verify
+the failure, implement the fix, run targeted checks, commit, push, and re-review.
+
+- [ ] **Step 5: Merge and deploy**
+
+When review and checks are clean, merge the PR into `main`, sync the production
+checkout, rebuild the Podman image, restart `dimensionlab-website.service`, and
+verify `https://dimensionlab.net/` with a browser smoke check.
+
+## Self-Review
+
+- Spec coverage: every completion criterion in the design maps to Task 1 through
+ Task 5.
+- Placeholder scan: no task says TBD, TODO, or "add tests" without commands.
+- Type consistency: package names are consistently `@dimensionlab/ui` and
+ `@dimensionlab/web`; the app adapter path is consistently
+ `$lib/ui-adapter/model-renderer`.
diff --git a/docs/superpowers/specs/2026-06-20-turbo-component-library-design.md b/docs/superpowers/specs/2026-06-20-turbo-component-library-design.md
new file mode 100644
index 0000000..ba0fefc
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-20-turbo-component-library-design.md
@@ -0,0 +1,186 @@
+# Turbo Component Library Migration Design
+
+## Context
+
+The current Dimension Lab website is a single Bun/Vite React package. It owns
+the browser app, Bun production server, dashboard model, persistence, datasource
+adapters, reusable UI components, Storybook, Playwright checks, and container
+deployment from one `package.json`.
+
+The UI components are already mostly generic and content-free under
+`src/lib/ui`, but they are not reusable by another project because they live
+inside the app package, depend on app path aliases, and share the app build,
+test, and Storybook configuration. One file in that area,
+`src/lib/ui/model-renderer.ts`, imports the Dimension Lab dashboard model and
+therefore is an app adapter, not reusable component-library code.
+
+The migration goal is to turn the repository into a Turborepo workspace where
+the Dimension Lab website consumes a separate reusable React component package.
+
+## Target Repository Shape
+
+Use one application workspace and one component-library workspace:
+
+```text
+.
+├── apps/
+│ └── web/
+│ ├── src/
+│ ├── tests/
+│ ├── drizzle/
+│ ├── Containerfile
+│ └── package.json
+├── packages/
+│ └── ui/
+│ ├── src/
+│ ├── .storybook/
+│ ├── package.json
+│ └── tsconfig.json
+├── package.json
+├── turbo.json
+├── tsconfig.base.json
+└── bun.lock
+```
+
+The root package is private and contains only workspace orchestration:
+workspaces, Turbo scripts, shared dev dependencies where useful, and the lock
+file. Runtime dependencies belong to the workspace that imports them.
+
+## Package Ownership
+
+`packages/ui` is a compiled React library named `@dimensionlab/ui`.
+
+It owns:
+
+- Reusable React components currently under `src/lib/ui/components`.
+- UI CSS tokens and component styles.
+- Theme helpers currently under `src/lib/ui/theme.ts`.
+- Generic UI prop/data types currently under `src/lib/ui/types.ts`.
+- Generic formatting helpers currently under `src/lib/ui/format.ts`.
+- Generic Storybook stories and story fixtures.
+- UI render tests, boundary tests, and Storybook inventory tests.
+
+It must not import from the website app, the dashboard model, server modules,
+database modules, datasource modules, or deployment files.
+
+The library publishes explicit package exports:
+
+- `@dimensionlab/ui` for component and type exports.
+- `@dimensionlab/ui/styles.css` for the combined token/component CSS entry.
+- Optional explicit subpath exports for future direct imports where useful.
+
+The compiled output goes to `packages/ui/dist` and includes JavaScript,
+declaration files, and copied CSS. The package stays private for now but is
+structured so it can later be published or moved into another Dimension Lab repo
+without taking the website runtime with it.
+
+`apps/web` owns:
+
+- The Vite React browser app.
+- The Bun production server and API routes.
+- Dashboard model, fixtures, schema, validation, and model migrations.
+- Drizzle/SQLite persistence and checked-in SQL migrations.
+- Datasource adapters and runtime dashboard loading.
+- Agent dashboard configuration endpoint.
+- The `dashboardDocumentToUiDashboard` adapter that maps the app model to UI
+ package props.
+- Playwright e2e tests and deployment container.
+
+## Why A Compiled Package
+
+The component package should be compiled rather than a just-in-time source
+package. This is slightly more setup, but it better fits reuse outside the
+current app because consumers can import stable JavaScript and declarations
+instead of relying on their bundler to transpile this repo's TypeScript source.
+It also gives Turbo a cacheable `@dimensionlab/ui#build` task.
+
+## Build And Task Graph
+
+Root scripts delegate through Turbo:
+
+- `bun run dev` runs the web dev server and any required dependency tasks.
+- `bun run build` runs package builds in dependency order.
+- `bun run check` runs TypeScript checks for all workspaces.
+- `bun run test:unit` runs Vitest unit tests for all workspaces.
+- `bun run build-storybook` builds Storybook from `packages/ui`.
+- `bun run test:e2e` runs the web app Playwright suite.
+- `bun run test:qa` is the full release gate.
+
+`turbo.json` defines `build`, `check`, `test:unit`, `build-storybook`,
+`test:e2e`, and `test:qa` tasks. Build outputs include `dist/**`,
+`storybook-static/**`, and `build/**` as appropriate.
+
+The web app depends on `@dimensionlab/ui` using Bun workspace syntax. The web
+Vite config aliases `$lib` to `apps/web/src/lib`; the UI package should not use
+that app alias.
+
+## Storybook
+
+Storybook moves with the component package. It should load
+`@dimensionlab/ui/styles.css`, use React Vite Storybook, and keep the existing
+generic story inventory. Environment-specific Dimension Lab labels, hostnames,
+links, fallback values, and datasource names remain forbidden in package UI
+source and stories.
+
+The repository should no longer have a root Storybook tied to the web app.
+
+## Deployment
+
+The deployed website remains the same service from the outside:
+
+- Production command remains `bun build/index.js` inside the runtime image.
+- The container still exposes port `3000` and mounts `/data`.
+- Runtime environment variables and database behavior remain unchanged.
+
+The `Containerfile` moves to `apps/web/Containerfile` or remains root with
+updated workspace-aware copy/build steps. The chosen layout must preserve the
+existing Podman service contract used by `dimensionlab-website.service`.
+
+## Testing Strategy
+
+The migration must add or update tests that prove the new boundaries:
+
+- The root package is a Bun workspace with `apps/*` and `packages/*`.
+- The web app imports UI from `@dimensionlab/ui`, not from local copied
+ component files.
+- `packages/ui` does not import from `apps/web`, `$lib/server`, `$lib/model`,
+ or any website runtime module.
+- `dashboardDocumentToUiDashboard` lives in `apps/web` and is tested there.
+- Storybook inventory is evaluated against `packages/ui`.
+- The full QA gate still covers typecheck, unit tests, app build, Storybook
+ build, and Playwright desktop/mobile checks.
+
+## Completion Criteria
+
+The migration is complete only when current evidence proves all of these:
+
+- Root `package.json` is a private workspace root with Turbo scripts.
+- `turbo.json` exists and models the workspace task graph.
+- The web application lives under `apps/web`.
+- The reusable React component library lives under `packages/ui`.
+- `packages/ui/package.json` is named `@dimensionlab/ui` and has compiled
+ exports for code, types, and CSS.
+- The web app depends on `@dimensionlab/ui` through the workspace.
+- Reusable components and Storybook have been removed from the web app package.
+- App-specific model/server/database/datasource code has not moved into
+ `packages/ui`.
+- `dashboardDocumentToUiDashboard` is outside the UI package.
+- `bun run check` passes from the root.
+- `bun run test:unit` passes from the root.
+- `bun run build` passes from the root.
+- `bun run build-storybook` passes from the root.
+- `bun run test:e2e` passes from the root.
+- The production container can still be built and run with the same service
+ contract.
+
+## Migration Approach
+
+Implement this on branch `codex/turbo-component-library` in focused commits:
+
+1. Add the workspace/Turbo scaffolding and boundary tests.
+2. Move UI code and Storybook to `packages/ui`.
+3. Move the app runtime into `apps/web` and wire it to `@dimensionlab/ui`.
+4. Move the model-to-UI adapter into the web app.
+5. Update build, test, Playwright, Storybook, README, and container paths.
+6. Run the full QA gate, push a ready PR, perform independent review, resolve
+ blockers, merge to `main`, and deploy only when checks and review are clean.
diff --git a/package.json b/package.json
index f1cf4ab..e33d3aa 100644
--- a/package.json
+++ b/package.json
@@ -1,63 +1,28 @@
{
- "name": "dimensionlab-website",
+ "name": "dimensionlab",
"version": "0.0.1",
"private": true,
"type": "module",
+ "packageManager": "bun@1.3.14",
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ],
"scripts": {
- "dev": "bun src/server/dev.ts",
- "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",
- "storybook": "storybook dev -p 6006 --host 0.0.0.0",
- "build-storybook": "storybook build",
- "check": "tsc --noEmit",
- "test": "vitest run",
- "test:unit": "vitest run",
- "test:e2e": "env -u NO_COLOR playwright test",
- "test:qa": "bun run check && bun run test:unit && bun run build && bun run build-storybook && bun run test:e2e",
- "db:generate": "drizzle-kit generate",
- "db:check": "drizzle-kit check"
- },
- "dependencies": {
- "@fontsource-variable/geist": "^5.2.9",
- "@iconify/react": "^6.0.2",
- "@sinclair/typebox": "^0.34.49",
- "ajv": "^8.20.0",
- "ajv-formats": "^3.0.1",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "drizzle-orm": "^0.45.2",
- "lucide-react": "^1.21.0",
- "radix-ui": "^1.6.0",
- "react": "^19.2.7",
- "react-dom": "^19.2.7",
- "tailwind-merge": "^3.6.0",
- "tw-animate-css": "^1.4.0",
- "uplot": "^1.6.32"
+ "dev": "turbo run dev --filter=@dimensionlab/web",
+ "build": "turbo run build",
+ "preview": "turbo run preview --filter=@dimensionlab/web",
+ "storybook": "turbo run storybook --filter=@dimensionlab/ui",
+ "build-storybook": "turbo run build-storybook --filter=@dimensionlab/ui",
+ "check": "turbo run check",
+ "test": "turbo run test:unit",
+ "test:unit": "turbo run test:unit",
+ "test:e2e": "turbo run test:e2e --filter=@dimensionlab/web",
+ "test:qa": "turbo run check test:unit build @dimensionlab/ui#build-storybook @dimensionlab/web#test:e2e",
+ "db:generate": "turbo run db:generate --filter=@dimensionlab/web",
+ "db:check": "turbo run db:check --filter=@dimensionlab/web"
},
"devDependencies": {
- "@axe-core/playwright": "^4.11.3",
- "@playwright/test": "^1.61.0",
- "@storybook/addon-a11y": "^10.4.6",
- "@storybook/addon-vitest": "^10.4.6",
- "@storybook/react-vite": "^10.4.6",
- "@tailwindcss/vite": "^4.3.1",
- "@types/bun": "^1.3.14",
- "@types/node": "^25.9.3",
- "@types/react": "^19.2.17",
- "@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.2",
- "drizzle-kit": "^0.31.10",
- "msw": "^2.14.6",
- "shadcn": "^4.11.0",
- "storybook": "^10.4.6",
- "tailwindcss": "^4.3.1",
- "typescript": "^6.0.3",
- "vite": "^8.0.16",
- "vitest": "^4.1.9"
- },
- "msw": {
- "workerDirectory": [
- "static"
- ]
+ "turbo": "^2.5.0"
}
}
diff --git a/packages/dashboard-model/package.json b/packages/dashboard-model/package.json
new file mode 100644
index 0000000..ae9a49b
--- /dev/null
+++ b/packages/dashboard-model/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@dimensionlab/dashboard-model",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "development": "./src/index.ts",
+ "default": "./dist/index.js"
+ },
+ "./fixtures": {
+ "types": "./dist/fixtures/index.d.ts",
+ "development": "./src/fixtures/index.ts",
+ "default": "./dist/fixtures/index.js"
+ }
+ },
+ "scripts": {
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
+ "check": "tsc --noEmit",
+ "test": "vitest run",
+ "test:unit": "vitest run"
+ },
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.49",
+ "ajv": "^8.20.0",
+ "ajv-formats": "^3.0.1"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.14",
+ "@types/node": "^25.9.3",
+ "bun-types": "^1.3.14",
+ "typescript": "^6.0.3",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/src/lib/model/fixtures/generic.ts b/packages/dashboard-model/src/fixtures/generic.ts
similarity index 100%
rename from src/lib/model/fixtures/generic.ts
rename to packages/dashboard-model/src/fixtures/generic.ts
diff --git a/packages/dashboard-model/src/fixtures/index.ts b/packages/dashboard-model/src/fixtures/index.ts
new file mode 100644
index 0000000..f62d646
--- /dev/null
+++ b/packages/dashboard-model/src/fixtures/index.ts
@@ -0,0 +1 @@
+export { genericDashboardFixture } from "./generic";
diff --git a/src/lib/model/index.ts b/packages/dashboard-model/src/index.ts
similarity index 100%
rename from src/lib/model/index.ts
rename to packages/dashboard-model/src/index.ts
diff --git a/src/lib/model/schema.test.ts b/packages/dashboard-model/src/schema.test.ts
similarity index 96%
rename from src/lib/model/schema.test.ts
rename to packages/dashboard-model/src/schema.test.ts
index 7eeed74..9975f87 100644
--- a/src/lib/model/schema.test.ts
+++ b/packages/dashboard-model/src/schema.test.ts
@@ -5,7 +5,6 @@ import {
validateDashboardDocument,
} from ".";
import {
- dimensionLabDashboardFixture,
genericDashboardFixture,
} from "./fixtures";
@@ -19,12 +18,6 @@ describe("dashboard model validation", () => {
}
});
- it("accepts the Dimension Lab dashboard fixture", () => {
- const result = validateDashboardDocument(dimensionLabDashboardFixture);
-
- expect(result.valid).toBe(true);
- });
-
it("rejects documents with an unsupported schema version", () => {
const invalid = {
...genericDashboardFixture,
diff --git a/src/lib/model/schema.ts b/packages/dashboard-model/src/schema.ts
similarity index 100%
rename from src/lib/model/schema.ts
rename to packages/dashboard-model/src/schema.ts
diff --git a/src/lib/model/validation.ts b/packages/dashboard-model/src/validation.ts
similarity index 100%
rename from src/lib/model/validation.ts
rename to packages/dashboard-model/src/validation.ts
diff --git a/packages/dashboard-model/tsconfig.build.json b/packages/dashboard-model/tsconfig.build.json
new file mode 100644
index 0000000..5d3e7ce
--- /dev/null
+++ b/packages/dashboard-model/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "emitDeclarationOnly": false,
+ "noEmit": false,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["dist", "node_modules", "src/**/*.test.ts"]
+}
diff --git a/packages/dashboard-model/tsconfig.json b/packages/dashboard-model/tsconfig.json
new file mode 100644
index 0000000..26cb1c4
--- /dev/null
+++ b/packages/dashboard-model/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "types": ["node", "bun-types"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/ui b/packages/ui
new file mode 160000
index 0000000..a7a4720
--- /dev/null
+++ b/packages/ui
@@ -0,0 +1 @@
+Subproject commit a7a472083555152b8e1a2dc018d3be5b3b10d60b
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"
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index c394ee9..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { renderToString } from "react-dom/server";
-import { describe, expect, test } from "vitest";
-import { AppStateView } from "./App";
-
-describe("React app dashboard state view", () => {
- test("renders loading dashboard state", () => {
- const html = renderToString(
- ,
- );
-
- expect(html).toContain("Loading Dashboard");
- expect(html).toContain("Fetching active model");
- });
-});
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 81ec0c3..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useEffect, useState } from "react";
-import type { DashboardRuntimeState } from "$lib/server/dashboard";
-import {
- DashboardFrame,
- SystemState,
- dashboardDocumentToUiDashboard,
- type UiSeverity,
-} from "$lib/ui";
-
-const loadingDashboardState: DashboardRuntimeState = {
- state: "loading",
- title: "Loading Dashboard",
- subtitle: "Fetching active model",
- message: "Waiting for the active dashboard document.",
-};
-
-export function AppStateView({
- dashboard,
-}: {
- dashboard: DashboardRuntimeState;
-}) {
- if (dashboard.state === "ready") {
- return (
-
- );
- }
-
- const detail = `${dashboard.subtitle}: ${dashboard.message}`;
- const errors = dashboard.state === "invalid" ? dashboard.errors : [];
-
- return (
-
-
- {errors.length ? (
-
- {errors.map((error) => (
- {error}
- ))}
-
- ) : null}
-
- );
-}
-
-export function resolveDocumentMetadata(dashboard: DashboardRuntimeState): {
- description: string;
- title: string;
-} {
- if (dashboard.state === "ready") {
- const uiDashboard = dashboardDocumentToUiDashboard(dashboard.document);
- return {
- title: uiDashboard.title,
- description:
- uiDashboard.subtitle ||
- dashboard.document.metadata.description ||
- uiDashboard.title,
- };
- }
-
- return {
- title: dashboard.title,
- description: dashboard.subtitle || dashboard.message,
- };
-}
-
-export default function App() {
- const [dashboard, setDashboard] =
- useState(loadingDashboardState);
-
- useEffect(() => {
- const metadata = resolveDocumentMetadata(dashboard);
- document.title = metadata.title;
-
- let description = document.querySelector(
- 'meta[name="description"]',
- );
- if (!description) {
- description = document.createElement("meta");
- description.name = "description";
- document.head.append(description);
- }
- description.content = metadata.description;
- }, [dashboard]);
-
- useEffect(() => {
- let cancelled = false;
- let refreshTimer: number | undefined;
-
- async function loadDashboard() {
- const response = await fetch("/api/dashboard");
- const nextDashboard = (await response.json()) as DashboardRuntimeState;
-
- if (cancelled) return;
- setDashboard(nextDashboard);
-
- if (refreshTimer) {
- window.clearInterval(refreshTimer);
- refreshTimer = undefined;
- }
-
- const refreshIntervalSeconds =
- nextDashboard.state === "ready"
- ? nextDashboard.document.metadata.refreshIntervalSeconds
- : undefined;
-
- if (refreshIntervalSeconds) {
- refreshTimer = window.setInterval(
- () => void loadDashboard(),
- refreshIntervalSeconds * 1000,
- );
- }
- }
-
- void loadDashboard();
-
- return () => {
- cancelled = true;
- if (refreshTimer) window.clearInterval(refreshTimer);
- };
- }, []);
-
- return ;
-}
-
-function stateSeverity(state: DashboardRuntimeState["state"]): UiSeverity {
- if (state === "invalid") return "danger";
- if (state === "loading") return "loading";
- return "stale";
-}
-
-function stateIcon(state: DashboardRuntimeState["state"]): string {
- if (state === "invalid") return "mdi:file-alert-outline";
- if (state === "loading") return "mdi:progress-clock";
- return "mdi:tray";
-}
diff --git a/src/lib/components/ui/alert.tsx b/src/lib/components/ui/alert.tsx
deleted file mode 100644
index fc34841..0000000
--- a/src/lib/components/ui/alert.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "$lib/utils"
-
-const alertVariants = cva(
- "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-card text-card-foreground",
- destructive:
- "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Alert({
- className,
- variant,
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- )
-}
-
-function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
- svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
- className
- )}
- {...props}
- />
- )
-}
-
-function AlertDescription({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export { Alert, AlertTitle, AlertDescription, AlertAction }
diff --git a/src/lib/components/ui/badge.tsx b/src/lib/components/ui/badge.tsx
deleted file mode 100644
index a3d91d7..0000000
--- a/src/lib/components/ui/badge.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
-
-import { cn } from "$lib/utils"
-
-const badgeVariants = cva(
- "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
- secondary:
- "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
- destructive:
- "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
- outline:
- "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
- ghost:
- "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-function Badge({
- className,
- variant = "default",
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps
& { asChild?: boolean }) {
- const Comp = asChild ? Slot.Root : "span"
-
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/src/lib/components/ui/button.tsx b/src/lib/components/ui/button.tsx
deleted file mode 100644
index 8dda5f2..0000000
--- a/src/lib/components/ui/button.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { Slot } from "radix-ui"
-
-import { cn } from "$lib/utils"
-
-const buttonVariants = cva(
- "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/80",
- outline:
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
- ghost:
- "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
- destructive:
- "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default:
- "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
- sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
- lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
- icon: "size-8",
- "icon-xs":
- "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
- "icon-sm":
- "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
- "icon-lg": "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-function Button({
- className,
- variant = "default",
- size = "default",
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps & {
- asChild?: boolean
- }) {
- const Comp = asChild ? Slot.Root : "button"
-
- return (
-
- )
-}
-
-export { Button, buttonVariants }
diff --git a/src/lib/components/ui/card.tsx b/src/lib/components/ui/card.tsx
deleted file mode 100644
index bcb9e07..0000000
--- a/src/lib/components/ui/card.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import * as React from "react"
-
-import { cn } from "$lib/utils"
-
-function Card({
- className,
- size = "default",
- ...props
-}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
- return (
- img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
- className
- )}
- {...props}
- />
- )
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
diff --git a/src/lib/components/ui/progress.tsx b/src/lib/components/ui/progress.tsx
deleted file mode 100644
index 65d0a6b..0000000
--- a/src/lib/components/ui/progress.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as React from "react"
-import { Progress as ProgressPrimitive } from "radix-ui"
-
-import { cn } from "$lib/utils"
-
-function Progress({
- className,
- value,
- ...props
-}: React.ComponentProps
) {
- return (
-
-
-
- )
-}
-
-export { Progress }
diff --git a/src/lib/components/ui/separator.tsx b/src/lib/components/ui/separator.tsx
deleted file mode 100644
index 84e3c64..0000000
--- a/src/lib/components/ui/separator.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Separator as SeparatorPrimitive } from "radix-ui"
-
-import { cn } from "$lib/utils"
-
-function Separator({
- className,
- orientation = "horizontal",
- decorative = true,
- ...props
-}: React.ComponentProps) {
- return (
-
- )
-}
-
-export { Separator }
diff --git a/src/lib/components/ui/skeleton.tsx b/src/lib/components/ui/skeleton.tsx
deleted file mode 100644
index 61466a8..0000000
--- a/src/lib/components/ui/skeleton.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { cn } from "$lib/utils"
-
-function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export { Skeleton }
diff --git a/src/lib/model/fixtures/index.ts b/src/lib/model/fixtures/index.ts
deleted file mode 100644
index a04278d..0000000
--- a/src/lib/model/fixtures/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { dimensionLabDashboardFixture } from "./dimensionlab";
-export { genericDashboardFixture } from "./generic";
diff --git a/src/lib/ui/components/Badge.tsx b/src/lib/ui/components/Badge.tsx
deleted file mode 100644
index a21ade8..0000000
--- a/src/lib/ui/components/Badge.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { UiSeverity } from "../types";
-import { StatusBadge } from "./StatusBadge";
-
-export interface BadgeProps {
- label: string;
- severity?: UiSeverity;
-}
-
-export function Badge({ label, severity = "neutral" }: BadgeProps) {
- return ;
-}
diff --git a/src/lib/ui/components/Button.tsx b/src/lib/ui/components/Button.tsx
deleted file mode 100644
index 8ae3b4e..0000000
--- a/src/lib/ui/components/Button.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { ButtonHTMLAttributes } from "react";
-import { IconGlyph } from "./IconGlyph";
-
-export interface ButtonProps
- extends Omit, "type"> {
- label: string;
- variant?: "primary" | "secondary" | "danger" | "ghost";
- size?: "default" | "compact";
- icon?: string;
- loading?: boolean;
- type?: "button" | "submit" | "reset";
-}
-
-export function Button({
- label,
- variant = "primary",
- size = "default",
- icon,
- disabled = false,
- loading = false,
- type = "button",
- className = "",
- ...buttonProps
-}: ButtonProps) {
- return (
-
- {icon ? : null}
- {loading ? "Loading" : label}
-
- );
-}
diff --git a/src/lib/ui/components/CornerBracketFrame.tsx b/src/lib/ui/components/CornerBracketFrame.tsx
deleted file mode 100644
index 1dfd4f7..0000000
--- a/src/lib/ui/components/CornerBracketFrame.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-export interface CornerBracketFrameProps {
- density?: "regular" | "tight";
- size?: "sm" | "md" | "lg";
- tone?: "neutral" | "accent" | "danger";
-}
-
-export function CornerBracketFrame({
- density = "regular",
- size = "md",
- tone = "neutral",
-}: CornerBracketFrameProps) {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/lib/ui/components/DashboardFrame.tsx b/src/lib/ui/components/DashboardFrame.tsx
deleted file mode 100644
index e8b8fe2..0000000
--- a/src/lib/ui/components/DashboardFrame.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useId } from "react";
-import type { UiDashboardPreview } from "../types";
-import { ModuleCard } from "./ModuleCard";
-import { ServicePanel } from "./ServicePanel";
-import { StatusStrip } from "./StatusStrip";
-import { TelemetryGrid } from "./TelemetryGrid";
-
-export interface DashboardFrameProps {
- dashboard: UiDashboardPreview;
- titleId?: string;
-}
-
-export function DashboardFrame({ dashboard, titleId }: DashboardFrameProps) {
- const generatedTitleId = useId();
- const resolvedTitleId = titleId || `${generatedTitleId}-title`;
-
- return (
-
-
-
-
-
-
- {dashboard.serviceGroups.map((group) => (
-
- ))}
-
-
-
-
- );
-}
diff --git a/src/lib/ui/components/DashboardHeader.tsx b/src/lib/ui/components/DashboardHeader.tsx
deleted file mode 100644
index 68b4e11..0000000
--- a/src/lib/ui/components/DashboardHeader.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { useId } from "react";
-import type { UiModuleBlock } from "../types";
-import { ModuleCard } from "./ModuleCard";
-
-export interface DashboardHeaderProps {
- title: string;
- subtitle?: string;
- eyebrow?: string;
- module?: UiModuleBlock;
-}
-
-export function DashboardHeader({
- title,
- subtitle,
- eyebrow,
- module,
-}: DashboardHeaderProps) {
- const titleId = useId();
-
- return (
-
-
- {eyebrow ?
{eyebrow}
: null}
-
{title}
- {subtitle ?
{subtitle} : null}
-
- {module ? : null}
-
- );
-}
diff --git a/src/lib/ui/components/DiagonalStripeField.tsx b/src/lib/ui/components/DiagonalStripeField.tsx
deleted file mode 100644
index b4c60f8..0000000
--- a/src/lib/ui/components/DiagonalStripeField.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-export interface DiagonalStripeFieldProps {
- density?: "open" | "regular" | "tight";
- direction?: "forward" | "backward";
- size?: "sm" | "md" | "lg";
- tone?: "neutral" | "accent" | "warning" | "danger";
-}
-
-export function DiagonalStripeField({
- density = "regular",
- direction = "forward",
- size = "md",
- tone = "accent",
-}: DiagonalStripeFieldProps) {
- return (
-
- );
-}
diff --git a/src/lib/ui/components/FooterCell.tsx b/src/lib/ui/components/FooterCell.tsx
deleted file mode 100644
index 760e853..0000000
--- a/src/lib/ui/components/FooterCell.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { UiStatusItem } from "../types";
-
-export interface FooterCellProps {
- item: UiStatusItem;
-}
-
-export function FooterCell({ item }: FooterCellProps) {
- const target = item.link?.external ? "_blank" : undefined;
- const rel = item.link?.external ? "noreferrer" : undefined;
- const content = (
- <>
- {item.label}
- {item.value}
- >
- );
-
- if (item.link) {
- return (
-
- {content}
-
- );
- }
-
- return (
-
- {content}
-
- );
-}
diff --git a/src/lib/ui/components/FooterStatusCell.tsx b/src/lib/ui/components/FooterStatusCell.tsx
deleted file mode 100644
index dcfb8b4..0000000
--- a/src/lib/ui/components/FooterStatusCell.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { UiStatusItem } from "../types";
-import { FooterCell } from "./FooterCell";
-
-export function FooterStatusCell({ item }: { item: UiStatusItem }) {
- return ;
-}
diff --git a/src/lib/ui/components/GridFrame.tsx b/src/lib/ui/components/GridFrame.tsx
deleted file mode 100644
index 0d9c91f..0000000
--- a/src/lib/ui/components/GridFrame.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { PropsWithChildren } from "react";
-
-export interface GridFrameProps extends PropsWithChildren {
- density?: "compact" | "dense";
-}
-
-export function GridFrame({ children, density = "dense" }: GridFrameProps) {
- return (
-
- );
-}
diff --git a/src/lib/ui/components/IconButton.tsx b/src/lib/ui/components/IconButton.tsx
deleted file mode 100644
index 92fcb46..0000000
--- a/src/lib/ui/components/IconButton.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { ButtonHTMLAttributes } from "react";
-import { IconGlyph } from "./IconGlyph";
-
-export interface IconButtonProps
- extends Omit, "type"> {
- icon: string;
- label: string;
- active?: boolean;
- type?: "button" | "submit" | "reset";
-}
-
-export function IconButton({
- icon,
- label,
- active = false,
- disabled = false,
- type = "button",
- className = "",
- ...buttonProps
-}: IconButtonProps) {
- return (
-
-
-
- );
-}
diff --git a/src/lib/ui/components/IconGlyph.tsx b/src/lib/ui/components/IconGlyph.tsx
deleted file mode 100644
index d21feb1..0000000
--- a/src/lib/ui/components/IconGlyph.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Icon } from "@iconify/react";
-
-export interface IconGlyphProps {
- name?: string;
- label?: string;
- size?: "sm" | "md" | "lg";
-}
-
-export function IconGlyph({ name, label, size = "md" }: IconGlyphProps) {
- return (
-
- {name ? : null}
-
- );
-}
diff --git a/src/lib/ui/components/LineChart.tsx b/src/lib/ui/components/LineChart.tsx
deleted file mode 100644
index 386b515..0000000
--- a/src/lib/ui/components/LineChart.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import "uplot/dist/uPlot.min.css";
-import { useEffect, useRef } from "react";
-import type { UiSeverity } from "../types";
-
-export interface LineChartProps {
- values?: number[];
- severity?: UiSeverity;
- label?: string;
-}
-
-export function LineChart({
- values = [],
- severity = "neutral",
- label = "Telemetry trend",
-}: LineChartProps) {
- const chartElementRef = useRef(null);
- const valuesKey = values.join(",");
-
- useEffect(() => {
- const chartElement = chartElementRef.current;
- if (!chartElement) return;
-
- let chart: ChartInstance | null = null;
- let resizeObserver: ResizeObserver | null = null;
- let disposed = false;
-
- void import("uplot").then((module) => {
- if (disposed || !chartElementRef.current) return;
-
- const element = chartElementRef.current;
- chart = new module.default(chartOptions(element, severity), chartData(values), element);
-
- if (typeof ResizeObserver !== "undefined") {
- resizeObserver = new ResizeObserver(() => {
- chart?.setSize(chartSize(element));
- });
- resizeObserver.observe(element);
- }
- });
-
- return () => {
- disposed = true;
- resizeObserver?.disconnect();
- chart?.destroy();
- };
- }, [severity, valuesKey, values]);
-
- return (
-
- );
-}
-
-type ChartInstance = {
- destroy(): void;
- setSize(size: { width: number; height: number }): void;
-};
-
-function chartData(values: number[]): import("uplot").AlignedData {
- const normalized = values.length ? values : [0, 0];
- return [
- normalized.map((_, index) => index),
- normalized.map((value) => Math.max(0, Number(value) || 0)),
- ];
-}
-
-function chartOptions(
- element: HTMLElement,
- severity: UiSeverity,
-): import("uplot").Options {
- return {
- ...chartSize(element),
- cursor: { show: false },
- legend: { show: false },
- padding: [2, 0, 2, 0],
- scales: {
- x: { time: false },
- y: { auto: true },
- },
- axes: [{ show: false }, { show: false }],
- series: [
- {},
- {
- stroke: chartStroke(element, severity),
- width: 2,
- points: { show: false },
- },
- ],
- };
-}
-
-function chartSize(element?: HTMLElement): { width: number; height: number } {
- return {
- width: Math.max(80, Math.round(element?.clientWidth || 120)),
- height: Math.max(20, Math.round(element?.clientHeight || 24)),
- };
-}
-
-function chartStroke(element: HTMLElement, severity: UiSeverity): string {
- const styles = window.getComputedStyle(element.closest(".line-chart") || element);
- const currentColor = styles.color;
- if (currentColor) return currentColor;
- if (severity === "danger") return "#ff1744";
- if (severity === "warning") return "#ffb020";
- return "#d7ff00";
-}
diff --git a/src/lib/ui/components/ModuleCard.tsx b/src/lib/ui/components/ModuleCard.tsx
deleted file mode 100644
index d05bee0..0000000
--- a/src/lib/ui/components/ModuleCard.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useId } from "react";
-import type { UiModuleBlock } from "../types";
-import { IconGlyph } from "./IconGlyph";
-
-export interface ModuleCardProps {
- module: UiModuleBlock;
-}
-
-export function ModuleCard({ module }: ModuleCardProps) {
- const generatedId = useId();
- const titleId = `${generatedId}-title`;
- const ariaLabel = module.title ? undefined : module.label || module.id;
-
- return (
-
-
- {module.title ?
{module.title} : null}
- {module.value ?
{module.value} : null}
- {module.detail || module.label ? (
-
{module.detail || module.label}
- ) : null}
-
- {module.icon ? : null}
-
- );
-}
diff --git a/src/lib/ui/components/Panel.tsx b/src/lib/ui/components/Panel.tsx
deleted file mode 100644
index 20acbd0..0000000
--- a/src/lib/ui/components/Panel.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { PropsWithChildren } from "react";
-
-export interface PanelProps extends PropsWithChildren {
- title?: string;
- density?: "compact" | "dense";
-}
-
-export function Panel({ title, density = "dense", children }: PanelProps) {
- return (
-
- {title ? (
-
- ) : null}
- {children}
-
- );
-}
diff --git a/src/lib/ui/components/ProgressMeter.tsx b/src/lib/ui/components/ProgressMeter.tsx
deleted file mode 100644
index c7278bf..0000000
--- a/src/lib/ui/components/ProgressMeter.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { CSSProperties } from "react";
-import { clampPercent } from "../format";
-import type { UiSeverity } from "../types";
-
-export interface ProgressMeterProps {
- value?: number;
- severity?: UiSeverity;
- label?: string;
-}
-
-export function ProgressMeter({
- value,
- severity = "neutral",
- label = "Progress",
-}: ProgressMeterProps) {
- const progress = value === undefined ? null : clampPercent(value);
-
- return (
-
- {progress !== null ? (
-
- ) : null}
-
- );
-}
diff --git a/src/lib/ui/components/ScanlineField.tsx b/src/lib/ui/components/ScanlineField.tsx
deleted file mode 100644
index 017fa0a..0000000
--- a/src/lib/ui/components/ScanlineField.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-export interface ScanlineFieldProps {
- intensity?: "soft" | "medium" | "hard";
- size?: "sm" | "md" | "lg";
- tone?: "neutral" | "accent" | "warning";
-}
-
-export function ScanlineField({
- intensity = "medium",
- size = "md",
- tone = "neutral",
-}: ScanlineFieldProps) {
- return (
-
- );
-}
diff --git a/src/lib/ui/components/Separator.tsx b/src/lib/ui/components/Separator.tsx
deleted file mode 100644
index ca1a955..0000000
--- a/src/lib/ui/components/Separator.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-export interface SeparatorProps {
- orientation?: "horizontal" | "vertical";
- dense?: boolean;
-}
-
-export function Separator({
- orientation = "horizontal",
- dense = false,
-}: SeparatorProps) {
- return (
-
- );
-}
diff --git a/src/lib/ui/components/ServiceGroupPanel.tsx b/src/lib/ui/components/ServiceGroupPanel.tsx
deleted file mode 100644
index 4252cc9..0000000
--- a/src/lib/ui/components/ServiceGroupPanel.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { UiServiceGroup } from "../types";
-import { ServicePanel } from "./ServicePanel";
-
-export function ServiceGroupPanel({ group }: { group: UiServiceGroup }) {
- return ;
-}
diff --git a/src/lib/ui/components/ServicePanel.tsx b/src/lib/ui/components/ServicePanel.tsx
deleted file mode 100644
index 12a2e74..0000000
--- a/src/lib/ui/components/ServicePanel.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { UiServiceGroup } from "../types";
-import { Panel } from "./Panel";
-import { ServiceRow } from "./ServiceRow";
-import { StatusStrip } from "./StatusStrip";
-
-export interface ServicePanelProps {
- group: UiServiceGroup;
-}
-
-export function ServicePanel({ group }: ServicePanelProps) {
- return (
-
-
- {group.summary?.length ? (
-
- ) : null}
- {group.services.map((service) => (
-
- ))}
-
-
- );
-}
diff --git a/src/lib/ui/components/ServiceRow.tsx b/src/lib/ui/components/ServiceRow.tsx
deleted file mode 100644
index 1030092..0000000
--- a/src/lib/ui/components/ServiceRow.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { UiServiceRow } from "../types";
-import { IconGlyph } from "./IconGlyph";
-import { StatusBadge } from "./StatusBadge";
-
-export interface ServiceRowProps {
- service: UiServiceRow;
-}
-
-export function ServiceRow({ service }: ServiceRowProps) {
- const target = service.link?.external ? "_blank" : undefined;
- const rel = service.link?.external ? "noreferrer" : undefined;
- const content = (
- <>
-
-
-
{service.label}
-
{service.description}
-
- {service.detail ? (
-
- ) : null}
- >
- );
-
- if (service.link) {
- return (
-
- {content}
-
- );
- }
-
- return (
-
- {content}
-
- );
-}
diff --git a/src/lib/ui/components/SignalTrace.tsx b/src/lib/ui/components/SignalTrace.tsx
deleted file mode 100644
index 86bc924..0000000
--- a/src/lib/ui/components/SignalTrace.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-export interface SignalTraceProps {
- density?: "regular" | "tight";
- orientation?: "horizontal" | "vertical";
- tone?: "neutral" | "accent" | "warning";
-}
-
-export function SignalTrace({
- density = "regular",
- orientation = "horizontal",
- tone = "accent",
-}: SignalTraceProps) {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/lib/ui/components/Sparkline.tsx b/src/lib/ui/components/Sparkline.tsx
deleted file mode 100644
index a79f532..0000000
--- a/src/lib/ui/components/Sparkline.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { UiSeverity } from "../types";
-
-export interface SparklineProps {
- values?: number[];
- severity?: UiSeverity;
-}
-
-export function Sparkline({
- values = [],
- severity = "neutral",
-}: SparklineProps) {
- const points = toPoints(values);
-
- return (
-
- {points ? : null}
-
- );
-}
-
-function toPoints(values: number[]): string {
- if (values.length === 0) return "";
- const max = Math.max(...values, 1);
- const step = values.length > 1 ? 100 / (values.length - 1) : 100;
- return values
- .map((value, index) => {
- const x = index * step;
- const y = 24 - (Math.max(0, value) / max) * 22;
- return `${x.toFixed(2)},${y.toFixed(2)}`;
- })
- .join(" ");
-}
diff --git a/src/lib/ui/components/StatusBadge.tsx b/src/lib/ui/components/StatusBadge.tsx
deleted file mode 100644
index fb63a04..0000000
--- a/src/lib/ui/components/StatusBadge.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { UiSeverity } from "../types";
-
-export interface StatusBadgeProps {
- label: string;
- severity?: UiSeverity;
-}
-
-export function StatusBadge({ label, severity = "neutral" }: StatusBadgeProps) {
- return (
-
- {label}
-
- );
-}
diff --git a/src/lib/ui/components/StatusStrip.tsx b/src/lib/ui/components/StatusStrip.tsx
deleted file mode 100644
index fbbd89e..0000000
--- a/src/lib/ui/components/StatusStrip.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { UiStatusItem } from "../types";
-import { FooterCell } from "./FooterCell";
-
-export interface StatusStripProps {
- id?: string;
- items: UiStatusItem[];
- compact?: boolean;
-}
-
-export function StatusStrip({ id, items, compact = false }: StatusStripProps) {
- return (
-
- {items.map((item) => (
-
- ))}
-
- );
-}
diff --git a/src/lib/ui/components/SystemState.tsx b/src/lib/ui/components/SystemState.tsx
deleted file mode 100644
index e817e2b..0000000
--- a/src/lib/ui/components/SystemState.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { UiSeverity } from "../types";
-import { IconGlyph } from "./IconGlyph";
-
-export interface SystemStateProps {
- title: string;
- detail?: string;
- icon?: string;
- severity?: UiSeverity;
-}
-
-export function SystemState({
- title,
- detail,
- icon = "mdi:information-outline",
- severity = "neutral",
-}: SystemStateProps) {
- return (
-
-
-
-
{title}
- {detail ?
{detail}
: null}
-
-
- );
-}
diff --git a/src/lib/ui/components/TelemetryCard.tsx b/src/lib/ui/components/TelemetryCard.tsx
deleted file mode 100644
index 950a1e0..0000000
--- a/src/lib/ui/components/TelemetryCard.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { CSSProperties } from "react";
-import { clampPercent, formatMetricValue } from "../format";
-import type { UiTelemetryCard } from "../types";
-import { IconGlyph } from "./IconGlyph";
-import { LineChart } from "./LineChart";
-
-export interface TelemetryCardProps {
- card: UiTelemetryCard;
-}
-
-export function TelemetryCard({ card }: TelemetryCardProps) {
- const progress = resolveProgress(card);
- const value = formatMetricValue(card.value);
-
- return (
-
-
- {card.icon ? : null}
- {card.label}
-
- {value}
- {progress !== null ? (
-
-
-
- ) : null}
- {card.sparkline?.length ? (
-
- ) : null}
- {card.detail || card.description ? (
- {card.detail || card.description}
- ) : null}
-
- );
-}
-
-function resolveProgress(card: UiTelemetryCard): number | null {
- if (typeof card.progress === "number") return clampPercent(card.progress);
- if (card.value.kind !== "percent") return null;
-
- const progress = Number(card.value.value);
- return Number.isFinite(progress) ? clampPercent(progress) : null;
-}
diff --git a/src/lib/ui/components/TelemetryGrid.tsx b/src/lib/ui/components/TelemetryGrid.tsx
deleted file mode 100644
index 773ece5..0000000
--- a/src/lib/ui/components/TelemetryGrid.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { UiTelemetryCard } from "../types";
-import { TelemetryCard } from "./TelemetryCard";
-
-export interface TelemetryGridProps {
- cards: UiTelemetryCard[];
-}
-
-export function TelemetryGrid({ cards }: TelemetryGridProps) {
- return (
-
- {cards.map((card) => (
-
- ))}
-
- );
-}
diff --git a/src/lib/ui/components/TelemetryStrip.tsx b/src/lib/ui/components/TelemetryStrip.tsx
deleted file mode 100644
index 765c1e2..0000000
--- a/src/lib/ui/components/TelemetryStrip.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { UiTelemetryCard } from "../types";
-import { TelemetryGrid } from "./TelemetryGrid";
-
-export function TelemetryStrip({ cards }: { cards: UiTelemetryCard[] }) {
- return ;
-}
diff --git a/src/lib/ui/components/WeatherModule.tsx b/src/lib/ui/components/WeatherModule.tsx
deleted file mode 100644
index 0967cfb..0000000
--- a/src/lib/ui/components/WeatherModule.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { UiModuleBlock } from "../types";
-import { ModuleCard } from "./ModuleCard";
-
-export function WeatherModule({ module }: { module: UiModuleBlock }) {
- return ;
-}
diff --git a/src/lib/ui/components/render.test.tsx b/src/lib/ui/components/render.test.tsx
deleted file mode 100644
index 14c1ff5..0000000
--- a/src/lib/ui/components/render.test.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { renderToString } from "react-dom/server";
-import { describe, expect, test } from "vitest";
-import { Button } from "./Button";
-import { DashboardFrame } from "./DashboardFrame";
-import { FooterCell } from "./FooterCell";
-import { IconButton } from "./IconButton";
-import { ServiceRow } from "./ServiceRow";
-import { TelemetryCard } from "./TelemetryCard";
-import { dashboardPreviewFixtures } from "../fixtures";
-
-describe("dashboard UI components", () => {
- test("renders the primary generic dashboard fixture", () => {
- const body = renderToString(
- ,
- );
-
- expect(body).toContain("Operations Console");
- expect(body).toContain("Core Throughput");
- expect(body).toContain("Queue Workers");
- expect(body).toContain("data-icon-name=\"mdi:server-network\"");
- expect(body).toContain("data-severity=\"warning\"");
- expect(body).not.toContain("dashboard-title");
- });
-
- test("renders another generic dashboard fixture through the same component", () => {
- const body = renderToString(
- ,
- );
-
- expect(body).toContain("Support Desk");
- expect(body).toContain("Response Window");
- expect(body).toContain("Regional Nodes");
- expect(body).toContain("data-icon-name=\"mdi:headset\"");
- expect(body).toContain("data-severity=\"loading\"");
- });
-
- test("renders optional service and status links as focusable anchors", () => {
- const service = renderToString(
- ,
- );
- const footer = renderToString(
- ,
- );
-
- expect(service).toContain(" {
- const body = renderToString(
- ,
- );
-
- expect(body).toContain("data-model-id=\"queue-workers\"");
- expect(body).toContain("data-model-id=\"dashboard-status\"");
- });
-
- test("does not render progress bars for non-percent metrics without explicit progress", () => {
- const withoutProgress = renderToString(
- ,
- );
- const withProgress = renderToString(
- ,
- );
-
- expect(withoutProgress).not.toContain("telemetry-card__bar");
- expect(withProgress).toContain("--metric-progress:42%");
- });
-
- test("renders telemetry trends through the uPlot chart surface", () => {
- const body = renderToString(
- ,
- );
-
- expect(body).toContain('data-chart-library="uplot"');
- });
-
- test("base button controls forward native attributes", () => {
- const button = renderToString(
- ,
- );
- const iconButton = renderToString(
- ,
- );
-
- expect(button).toContain("id=\"refresh-action\"");
- expect(button).toContain("class=\"ui-button custom-action ");
- expect(button).toContain("aria-controls=\"refresh-target\"");
- expect(iconButton).toContain("id=\"refresh-icon-action\"");
- expect(iconButton).toContain("class=\"icon-button custom-icon-action ");
- expect(iconButton).toContain("aria-expanded=\"false\"");
- });
-});
diff --git a/src/lib/ui/components/styles.css b/src/lib/ui/components/styles.css
deleted file mode 100644
index d8111f4..0000000
--- a/src/lib/ui/components/styles.css
+++ /dev/null
@@ -1,655 +0,0 @@
-.dashboard-frame {
- display: grid;
- box-sizing: border-box;
- height: 100vh;
- min-height: 100vh;
- gap: 0.36rem;
- grid-template-rows: auto auto minmax(0, 1fr) auto;
- overflow: hidden;
- padding: clamp(0.42rem, 0.65vw, 0.62rem);
- background:
- linear-gradient(90deg, transparent 0 49%, rgba(255, 255, 255, 0.08) 50%, transparent 51%),
- rgba(0, 0, 0, 0.18);
-}
-
-.dashboard-frame__header,
-.dashboard-header {
- display: grid;
- gap: 0.5rem;
- align-items: start;
- border-bottom: var(--ui-border);
-}
-
-.dashboard-frame__header {
- grid-template-columns: minmax(30rem, 1fr) minmax(30rem, 0.9fr);
- min-height: 5.35rem;
- padding-bottom: 0.34rem;
-}
-
-.dashboard-header {
- grid-template-columns: minmax(0, 1fr) auto;
- padding: var(--ui-space-3);
-}
-
-.dashboard-frame__header p,
-.dashboard-frame__header span,
-.dashboard-header p,
-.dashboard-header span,
-.dashboard-frame h1,
-.dashboard-header h1 {
- margin: 0;
- text-transform: uppercase;
-}
-
-.dashboard-frame__header p,
-.dashboard-frame__header span,
-.dashboard-header p,
-.dashboard-header span {
- color: var(--ui-color-muted);
- font-size: 0.78rem;
- font-weight: 800;
-}
-
-.dashboard-frame h1,
-.dashboard-header h1 {
- max-width: 100%;
- overflow-wrap: anywhere;
- font-family: var(--ui-font-display);
- font-size: clamp(2.2rem, 3.3vw, 2.95rem);
- font-weight: 850;
- letter-spacing: 0;
- line-height: 0.8;
- white-space: nowrap;
-}
-
-.dashboard-frame__modules {
- display: grid;
- grid-template-columns: minmax(15rem, 0.66fr) minmax(17rem, 1fr);
- justify-content: end;
- gap: 0.5rem;
-}
-
-.dashboard-frame__panels {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- align-content: start;
- gap: 0.36rem;
- min-height: 0;
- overflow: hidden;
-}
-
-.dashboard-frame__panels .service-panel[data-layout="grid"] {
- grid-column: 1 / -1;
-}
-
-.dashboard-frame__panels .service-panel[data-layout="grid"] .panel__body {
- grid-template-columns: repeat(4, minmax(0, 1fr));
-}
-
-.telemetry-grid {
- display: grid;
- grid-template-columns: repeat(8, minmax(0, 1fr));
- gap: 1px;
- border: var(--ui-border);
- background: var(--ui-color-line);
-}
-
-.telemetry-card,
-.module-card,
-.panel,
-.system-state {
- border: var(--ui-border);
- background: rgba(3, 4, 3, 0.86);
- color: var(--ui-color-text);
-}
-
-.telemetry-card {
- display: grid;
- min-height: 5.15rem;
- gap: 0.25rem;
- background: linear-gradient(180deg, rgba(13, 15, 14, 0.82), rgba(2, 3, 2, 0.92));
- padding: 0.45rem 0.55rem 0.42rem;
-}
-
-.telemetry-card[data-severity="warning"] {
- border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
-}
-
-.telemetry-card[data-severity="danger"] {
- border-color: color-mix(in srgb, var(--ui-color-danger), transparent 22%);
- color: var(--ui-color-danger);
-}
-
-.telemetry-card[data-severity="stale"],
-.telemetry-card[data-severity="unavailable"] {
- color: var(--ui-color-stale);
-}
-
-.telemetry-card[data-severity="loading"] {
- color: var(--ui-color-accent);
-}
-
-.telemetry-card header {
- display: flex;
- min-width: 0;
- align-items: center;
- gap: 0.35rem;
-}
-
-.telemetry-card header .icon-glyph {
- display: none;
-}
-
-.telemetry-card h3,
-.telemetry-card p,
-.module-card h2,
-.module-card p,
-.service-row h3,
-.service-row p,
-.panel h2,
-.system-state h2,
-.system-state p {
- margin: 0;
-}
-
-.telemetry-card h3,
-.service-row h3 {
- overflow: hidden;
- font-weight: 850;
- letter-spacing: 0;
- line-height: 1.05;
- text-overflow: ellipsis;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.telemetry-card h3 {
- overflow-wrap: anywhere;
- color: inherit;
- font-size: 0.58rem;
- font-weight: 800;
- line-height: 1.02;
- white-space: normal;
-}
-
-.service-row h3 {
- font-size: 0.72rem;
-}
-
-.telemetry-card strong,
-.module-card strong {
- display: block;
- font-family: var(--ui-font-display);
- font-size: clamp(1.55rem, 2.45vw, 2.22rem);
- font-weight: 800;
- letter-spacing: 0;
- line-height: 0.76;
- white-space: nowrap;
-}
-
-.telemetry-card p,
-.module-card p,
-.service-row p {
- overflow: hidden;
- color: var(--ui-color-muted);
- line-height: 1.08;
- text-overflow: ellipsis;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.telemetry-card p {
- font-size: 0.49rem;
- line-height: 1.05;
-}
-
-.module-card p,
-.service-row p {
- font-size: 0.56rem;
-}
-
-.telemetry-card__bar,
-.progress-meter {
- height: 0.25rem;
- border: var(--ui-border);
- background: #030403;
-}
-
-.telemetry-card__bar span,
-.progress-meter span {
- display: block;
- width: var(--metric-progress, var(--meter-progress));
- height: 100%;
- background: currentColor;
-}
-
-.telemetry-card[data-severity="loading"] .telemetry-card__bar span {
- min-width: 1.25rem;
-}
-
-.line-chart {
- position: relative;
- min-width: 0;
- height: 1rem;
- color: var(--ui-color-accent);
-}
-
-.line-chart[data-severity="warning"] {
- color: var(--ui-color-warning);
-}
-
-.line-chart[data-severity="danger"] {
- color: var(--ui-color-danger);
-}
-
-.line-chart[data-severity="stale"],
-.line-chart[data-severity="unavailable"] {
- color: var(--ui-color-stale);
-}
-
-.line-chart__canvas {
- width: 100%;
- height: 100%;
-}
-
-.line-chart__canvas .uplot {
- width: 100% !important;
- height: 100% !important;
- background: transparent;
- font-family: var(--ui-font-mono);
-}
-
-.line-chart__canvas .u-over,
-.line-chart__canvas .u-under {
- overflow: visible;
-}
-
-.module-card {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 0.5rem;
- align-items: start;
- min-width: 0;
- min-height: 4.15rem;
- background: rgba(6, 8, 7, 0.82);
- padding: 0.5rem;
-}
-
-.module-card h2 {
- color: var(--ui-color-muted);
- font-size: 0.56rem;
- line-height: 1;
- text-transform: uppercase;
-}
-
-.module-card strong {
- margin-top: 0.15rem;
- font-size: clamp(1.25rem, 1.75vw, 1.72rem);
- line-height: 0.82;
- overflow-wrap: anywhere;
-}
-
-.module-card p {
- margin-top: 0.2rem;
- font-size: 0.49rem;
- line-height: 1.05;
-}
-
-.module-card[data-severity="ok"] .icon-glyph {
- background: var(--ui-color-accent);
- color: #050605;
-}
-
-.module-card[data-severity="warning"] {
- border-color: color-mix(in srgb, var(--ui-color-warning), transparent 42%);
- color: var(--ui-color-warning);
-}
-
-.module-card[data-severity="danger"] {
- border-color: color-mix(in srgb, var(--ui-color-danger), transparent 32%);
- color: var(--ui-color-danger);
-}
-
-.module-card[data-severity="stale"],
-.module-card[data-severity="unavailable"] {
- color: var(--ui-color-stale);
-}
-
-.module-card[data-severity="loading"] {
- color: var(--ui-color-accent);
-}
-
-.service-row {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- gap: 0.24rem;
- align-items: center;
- min-height: 2.02rem;
- border: 0;
- border-bottom: var(--ui-border);
- background: transparent;
- color: inherit;
- padding: 0.14rem 0;
- text-decoration: none;
-}
-
-.service-row[data-severity="warning"] {
- border-bottom-color: color-mix(in srgb, var(--ui-color-warning), transparent 54%);
-}
-
-.service-row[data-severity="danger"],
-.service-row[data-severity="unavailable"] {
- border-bottom-color: color-mix(in srgb, var(--ui-color-danger), transparent 50%);
-}
-
-.service-row[data-severity="loading"] {
- border-bottom-color: color-mix(in srgb, var(--ui-color-accent), transparent 56%);
-}
-
-.service-row:where(a):hover {
- border-bottom-color: var(--ui-color-accent);
- background: rgba(244, 244, 244, 0.035);
-}
-
-.service-row__main {
- min-width: 0;
-}
-
-.status-badge {
- display: inline-grid;
- min-height: 1.12rem;
- align-items: center;
- border: var(--ui-border);
- padding: 0 0.3rem;
- color: var(--ui-color-muted);
- font-size: 0.55rem;
- font-weight: 800;
- letter-spacing: 0;
- text-transform: uppercase;
- white-space: nowrap;
-}
-
-.status-badge[data-severity="ok"] {
- border-color: color-mix(in srgb, var(--ui-color-ok), transparent 42%);
- color: var(--ui-color-ok);
-}
-
-.status-badge[data-severity="warning"] {
- border-color: color-mix(in srgb, var(--ui-color-warning), transparent 35%);
- color: var(--ui-color-warning);
-}
-
-.status-badge[data-severity="danger"] {
- border-color: color-mix(in srgb, var(--ui-color-danger), transparent 30%);
- color: var(--ui-color-danger);
-}
-
-.status-badge[data-severity="stale"],
-.status-badge[data-severity="unavailable"] {
- color: var(--ui-color-stale);
-}
-
-.status-badge[data-severity="loading"] {
- color: var(--ui-color-accent);
-}
-
-.footer-cell {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- min-height: 1.9rem;
- align-items: center;
- background: rgba(2, 3, 2, 0.92);
- color: inherit;
- text-decoration: none;
-}
-
-.footer-cell span,
-.footer-cell strong {
- min-width: 0;
- padding: 0.34rem 0.52rem;
- overflow-wrap: anywhere;
- text-transform: uppercase;
-}
-
-.footer-cell span {
- height: 100%;
- border-right: var(--ui-border);
- color: var(--ui-color-muted);
- font-size: 0.52rem;
- font-weight: 800;
-}
-
-.footer-cell strong {
- color: var(--ui-color-text);
- font-size: 0.58rem;
-}
-
-.footer-cell[data-severity="ok"] strong {
- background: var(--ui-color-accent);
- color: #060706;
-}
-
-.footer-cell[data-severity="warning"] strong {
- color: var(--ui-color-warning);
-}
-
-.footer-cell[data-severity="danger"] strong {
- color: var(--ui-color-danger);
-}
-
-.footer-cell[data-severity="stale"] strong,
-.footer-cell[data-severity="unavailable"] strong {
- color: var(--ui-color-stale);
-}
-
-.footer-cell[data-severity="loading"] strong {
- color: var(--ui-color-accent);
-}
-
-.footer-cell:where(a):hover strong {
- color: var(--ui-color-accent);
-}
-
-.status-strip {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
- gap: 1px;
- border: var(--ui-border);
- background: var(--ui-color-line);
-}
-
-.status-strip[data-compact="true"] {
- grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
-}
-
-.panel {
- position: relative;
- min-width: 0;
- box-shadow: var(--ui-shadow-hard);
-}
-
-.panel::before,
-.panel::after {
- position: absolute;
- width: 0.55rem;
- height: 0.55rem;
- border-color: var(--ui-color-line-strong);
- content: "";
-}
-
-.panel::before {
- top: 0.2rem;
- right: 0.2rem;
- border-top: 1px solid;
- border-right: 1px solid;
-}
-
-.panel::after {
- right: 0.2rem;
- bottom: 0.2rem;
- border-right: 1px solid;
- border-bottom: 1px solid;
-}
-
-.panel__header {
- padding: 0.42rem 0.55rem 0;
-}
-
-.panel h2 {
- font-family: var(--ui-font-display);
- font-size: clamp(1.25rem, 1.8vw, 1.75rem);
- font-weight: 800;
- line-height: 0.92;
- text-transform: uppercase;
-}
-
-.panel__body {
- display: grid;
- gap: 0.25rem;
- padding: 0.42rem 0.55rem 0.55rem;
-}
-
-.service-panel .panel__body {
- gap: 0;
- padding-block: 0.24rem 0.42rem;
-}
-
-.ui-button,
-.icon-button {
- display: inline-grid;
- align-items: center;
- justify-content: center;
- border: var(--ui-border);
- cursor: pointer;
-}
-
-.ui-button {
- grid-auto-flow: column;
- gap: var(--ui-space-2);
- min-height: 2.5rem;
- background: var(--ui-color-accent);
- color: var(--ui-color-canvas);
- font-size: 0.76rem;
- font-weight: 850;
- padding: 0 var(--ui-space-4);
- text-transform: uppercase;
-}
-
-.icon-button {
- width: 2.5rem;
- height: 2.5rem;
- place-items: center;
- background: rgba(2, 3, 2, 0.92);
- color: var(--ui-color-muted);
- padding: 0;
-}
-
-.icon-glyph {
- display: inline-grid;
- width: 1.55rem;
- height: 1.55rem;
- place-items: center;
- border: var(--ui-border);
- background: #050605;
- color: currentColor;
- line-height: 1;
-}
-
-.icon-glyph svg {
- width: 0.95rem;
- height: 0.95rem;
-}
-
-.icon-glyph[data-size="sm"] {
- width: 1.1rem;
- height: 1.1rem;
-}
-
-.icon-glyph[data-size="lg"] {
- width: 2.35rem;
- height: 2.35rem;
-}
-
-.system-state {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- gap: var(--ui-space-3);
- align-items: center;
- padding: var(--ui-space-4);
-}
-
-.grid-frame {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
- gap: var(--ui-space-3);
- padding: var(--ui-space-3);
-}
-
-.separator {
- background: var(--ui-color-line);
-}
-
-.separator[data-orientation="horizontal"] {
- width: 100%;
- height: 1px;
- margin-block: var(--ui-space-3);
-}
-
-.corner-bracket-frame,
-.diagonal-stripe-field,
-.scanline-field,
-.signal-trace {
- display: block;
- min-height: 3rem;
-}
-
-@media (max-width: 1100px) {
- .telemetry-grid {
- grid-template-columns: repeat(4, minmax(0, 1fr));
- }
-}
-
-@media (max-width: 780px) {
- .dashboard-frame {
- height: auto;
- min-height: 100vh;
- overflow: visible;
- }
-
- .dashboard-frame__header,
- .dashboard-header,
- .dashboard-frame__modules,
- .dashboard-frame__panels {
- grid-template-columns: 1fr;
- }
-
- .dashboard-frame h1,
- .dashboard-header h1 {
- white-space: normal;
- }
-
- .dashboard-frame__panels {
- overflow: visible;
- }
-
- .dashboard-frame__panels .service-panel[data-layout="grid"] .panel__body {
- grid-template-columns: 1fr;
- }
-}
-
-@media (max-width: 700px) {
- .telemetry-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-}
-
-@media (max-width: 580px) {
- .service-row {
- grid-template-columns: auto minmax(0, 1fr);
- }
-
- .service-row .status-badge {
- grid-column: 2;
- justify-self: start;
- }
-}
diff --git a/src/lib/ui/content-boundary.test.ts b/src/lib/ui/content-boundary.test.ts
deleted file mode 100644
index 6613644..0000000
--- a/src/lib/ui/content-boundary.test.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { readdirSync, readFileSync, statSync } from "node:fs";
-import { join } from "node:path";
-import { describe, expect, test } from "vitest";
-
-const forbiddenTerms = [
- "dimensionlab",
- "dimension lab",
- "vaultwarden",
- "forgejo",
- "grafana",
- "uptime kuma",
- "prometheus",
- "backrest",
- "open webui",
- "comfyui",
- "adminer",
- "cockpit",
- "ollama",
-];
-
-describe("UI content boundary", () => {
- test("keeps environment-specific content out of reusable UI source", () => {
- const source = readUiSource(join(process.cwd(), "src", "lib", "ui"));
- const normalized = source.toLowerCase();
-
- expect(
- forbiddenTerms.filter((term) => normalized.includes(term)),
- ).toEqual([]);
- });
-
- test("keeps icon rendering driven by icon identifiers", () => {
- const source = readUiSource(join(process.cwd(), "src", "lib", "ui"));
-
- expect(source).not.toContain("@iconify-json/");
- expect(source).not.toContain("/icons/");
- });
-});
-
-function readUiSource(path: string): string {
- const stats = statSync(path);
- if (stats.isFile()) {
- if (path.endsWith(".test.ts")) return "";
- return readFileSync(path, "utf8");
- }
-
- return readdirSync(path)
- .map((entry) => readUiSource(join(path, entry)))
- .join("\n");
-}
diff --git a/src/lib/ui/fixtures.ts b/src/lib/ui/fixtures.ts
deleted file mode 100644
index b8008c1..0000000
--- a/src/lib/ui/fixtures.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import type { UiDashboardPreview } from "./types";
-
-export const dashboardPreviewFixtures: Record = {
- primary: {
- eyebrow: "Command Surface",
- title: "Operations Console",
- subtitle: "Capacity, latency, and queue health",
- telemetry: [
- metric("core-throughput", "Core Throughput", "mdi:server-network", 82, "ok"),
- metric("edge-cache", "Edge Cache", "mdi:database-clock", 67, "neutral"),
- metric("queue-depth", "Queue Depth", "mdi:tray-full", 74, "warning"),
- metric("error-budget", "Error Budget", "mdi:chart-timeline-variant", 18, "danger"),
- metric("build-latency", "Build Latency", "mdi:timer-sand", 23, "stale"),
- metric("worker-load", "Worker Load", "mdi:cpu-64-bit", 55, "ok"),
- ],
- modules: [
- {
- id: "ambient-conditions",
- title: "Local Node",
- value: "21.4 C",
- detail: "clear window",
- icon: "mdi:weather-partly-cloudy",
- severity: "ok",
- },
- ],
- serviceGroups: [
- {
- id: "queue-workers",
- title: "Queue Workers",
- services: [
- service("ingest", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms"),
- service("scheduler", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"),
- service("archive", "Archive", "Cold storage transfer", "mdi:archive-arrow-down", "ok", "1.301 ms"),
- ],
- },
- {
- id: "regional-nodes",
- title: "Regional Nodes",
- services: [
- service("north", "North", "Primary traffic cell", "mdi:access-point", "ok", "899 ms"),
- service("west", "West", "Replica traffic cell", "mdi:access-point-network", "neutral", "1.118 ms"),
- service("south", "South", "Maintenance window", "mdi:wrench-clock", "stale", "paused"),
- ],
- },
- {
- id: "control-plane",
- title: "Control Plane",
- services: [
- service("identity", "Identity", "Access token exchange", "mdi:account-key", "ok", "721 ms"),
- service("policy", "Policy", "Rules evaluation", "mdi:shield-check", "ok", "812 ms"),
- service("audit", "Audit", "Event ledger writer", "mdi:text-box-search", "warning", "1.442 ms"),
- ],
- },
- ],
- statusItems: [
- { id: "system", label: "System", value: "Nominal", severity: "ok" },
- { id: "sync", label: "Last Sync", value: "2 minutes ago", severity: "stale" },
- { id: "uptime", label: "Uptime", value: "14d 08h", severity: "neutral" },
- { id: "refresh", label: "Refresh", value: "15s", severity: "neutral" },
- ],
- statusStripId: "dashboard-status",
- },
- secondary: {
- eyebrow: "Support Surface",
- title: "Support Desk",
- subtitle: "Response, routing, and regional health",
- telemetry: [
- metric("response-window", "Response Window", "mdi:headset", 41, "ok"),
- metric("ticket-load", "Ticket Load", "mdi:ticket-confirmation", 68, "warning"),
- metric("handoff-drift", "Handoff Drift", "mdi:swap-horizontal", 11, "neutral"),
- metric("coverage-gap", "Coverage Gap", "mdi:map-marker-alert", 7, "danger"),
- metric("routing-warmup", "Routing Warmup", "mdi:progress-clock", 0, "loading"),
- ],
- modules: [
- {
- id: "shift-state",
- title: "Shift State",
- value: "covered",
- detail: "static fixture",
- icon: "mdi:clipboard-check-outline",
- severity: "ok",
- },
- ],
- serviceGroups: [
- {
- id: "regional-nodes",
- title: "Regional Nodes",
- services: [
- service("desk-a", "Desk A", "Frontline queue", "mdi:monitor-dashboard", "ok", "843 ms"),
- service("desk-b", "Desk B", "Escalation queue", "mdi:monitor-star", "warning", "1.204 ms"),
- service("desk-c", "Desk C", "Overflow queue", "mdi:monitor-off", "unavailable", "offline"),
- ],
- },
- ],
- statusItems: [
- { id: "desk", label: "Desk", value: "Covered", severity: "ok" },
- { id: "handoff", label: "Handoff", value: "Pending", severity: "warning" },
- { id: "routing", label: "Routing", value: "Manual", severity: "neutral" },
- ],
- statusStripId: "support-status",
- },
-};
-
-function metric(
- id: string,
- label: string,
- icon: string,
- progress: number,
- severity: UiDashboardPreview["telemetry"][number]["severity"],
-) {
- return {
- id,
- label,
- icon,
- progress,
- severity,
- value: { kind: "percent" as const, value: progress },
- detail: "static fixture",
- sparkline: [12, 18, 15, 28, 24, progress],
- };
-}
-
-function service(
- id: string,
- label: string,
- description: string,
- icon: string,
- severity: UiDashboardPreview["serviceGroups"][number]["services"][number]["severity"],
- detail: string,
-) {
- return { id, label, description, icon, severity, detail };
-}
diff --git a/src/lib/ui/format.ts b/src/lib/ui/format.ts
deleted file mode 100644
index 9a10434..0000000
--- a/src/lib/ui/format.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import type { UiMetricValue } from "./types";
-
-const byteUnits = ["B", "KB", "MB", "GB", "TB"];
-
-export function formatMetricValue(metric: UiMetricValue): string {
- if (metric.kind === "text") return String(metric.value);
-
- const value = typeof metric.value === "number" ? metric.value : Number(metric.value);
- const precision = metric.precision ?? inferPrecision(value);
- const unit = metric.unit ?? defaultUnit(metric.kind);
-
- if (metric.kind === "bytes") return formatBytes(value, precision);
- if (metric.kind === "percent") return `${value.toFixed(precision)}%`;
- if (metric.kind === "temperature") return `${value.toFixed(precision)}°C`;
- return `${value.toFixed(precision)}${unit ? ` ${unit}` : ""}`;
-}
-
-export function clampPercent(value = 0): number {
- if (!Number.isFinite(value)) return 0;
- return Math.max(0, Math.min(100, value));
-}
-
-function defaultUnit(kind: UiMetricValue["kind"]): string {
- if (kind === "percent") return "%";
- if (kind === "temperature") return "C";
- if (kind === "latency") return "ms";
- return "";
-}
-
-function inferPrecision(value: number): number {
- return Number.isInteger(value) ? 0 : 1;
-}
-
-function formatBytes(value: number, precision: number): string {
- let size = value;
- let index = 0;
- while (size >= 1024 && index < byteUnits.length - 1) {
- size /= 1024;
- index += 1;
- }
-
- return `${size.toFixed(precision)} ${byteUnits[index]}`;
-}
diff --git a/src/lib/ui/index.ts b/src/lib/ui/index.ts
deleted file mode 100644
index 864306c..0000000
--- a/src/lib/ui/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-export { Badge } from "./components/Badge";
-export { Button } from "./components/Button";
-export { CornerBracketFrame } from "./components/CornerBracketFrame";
-export { DashboardHeader } from "./components/DashboardHeader";
-export { DashboardFrame } from "./components/DashboardFrame";
-export { DiagonalStripeField } from "./components/DiagonalStripeField";
-export { FooterCell } from "./components/FooterCell";
-export { FooterStatusCell } from "./components/FooterStatusCell";
-export { GridFrame } from "./components/GridFrame";
-export { IconGlyph } from "./components/IconGlyph";
-export { IconButton } from "./components/IconButton";
-export { LineChart } from "./components/LineChart";
-export { ModuleCard } from "./components/ModuleCard";
-export { Panel } from "./components/Panel";
-export { ProgressMeter } from "./components/ProgressMeter";
-export { Separator } from "./components/Separator";
-export { ServiceGroupPanel } from "./components/ServiceGroupPanel";
-export { ServicePanel } from "./components/ServicePanel";
-export { ServiceRow } from "./components/ServiceRow";
-export { ScanlineField } from "./components/ScanlineField";
-export { SignalTrace } from "./components/SignalTrace";
-export { Sparkline } from "./components/Sparkline";
-export { StatusBadge } from "./components/StatusBadge";
-export { StatusStrip } from "./components/StatusStrip";
-export { SystemState } from "./components/SystemState";
-export { TelemetryCard } from "./components/TelemetryCard";
-export { TelemetryGrid } from "./components/TelemetryGrid";
-export { TelemetryStrip } from "./components/TelemetryStrip";
-export { WeatherModule } from "./components/WeatherModule";
-export { dashboardPreviewFixtures } from "./fixtures";
-export { dashboardDocumentToUiDashboard } from "./model-renderer";
-export type {
- UiDashboardPreview,
- UiLink,
- UiMetricValue,
- UiModuleBlock,
- UiServiceGroup,
- UiServiceRow,
- UiSeverity,
- UiStatusItem,
- UiTelemetryCard,
-} from "./types";
diff --git a/src/lib/ui/stories/Badge.stories.tsx b/src/lib/ui/stories/Badge.stories.tsx
deleted file mode 100644
index d33394d..0000000
--- a/src/lib/ui/stories/Badge.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Badge } from "../components/Badge";
-
-const meta = {
- title: "UI/Badge",
- component: Badge,
- args: {
- label: "Ready",
- severity: "ok",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/Button.stories.tsx b/src/lib/ui/stories/Button.stories.tsx
deleted file mode 100644
index c207233..0000000
--- a/src/lib/ui/stories/Button.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Button } from "../components/Button";
-
-const meta = {
- title: "UI/Button",
- component: Button,
- args: {
- label: "Refresh",
- icon: "mdi:refresh",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Primary: Story = {};
-export const Secondary: Story = {
- args: {
- variant: "secondary",
- },
-};
diff --git a/src/lib/ui/stories/CornerBracketFrame.stories.tsx b/src/lib/ui/stories/CornerBracketFrame.stories.tsx
deleted file mode 100644
index dd7efbf..0000000
--- a/src/lib/ui/stories/CornerBracketFrame.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { CornerBracketFrame } from "../components/CornerBracketFrame";
-
-const meta = {
- title: "UI/CornerBracketFrame",
- component: CornerBracketFrame,
- args: {
- tone: "accent",
- size: "md",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/DashboardFrame.stories.tsx b/src/lib/ui/stories/DashboardFrame.stories.tsx
deleted file mode 100644
index e1963af..0000000
--- a/src/lib/ui/stories/DashboardFrame.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { DashboardFrame } from "../components/DashboardFrame";
-import { fullCompositionDashboard, secondaryDashboard } from "./story-data";
-
-const meta = {
- title: "UI/DashboardFrame",
- component: DashboardFrame,
- args: {
- dashboard: fullCompositionDashboard,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const FullComposition: Story = {};
-export const Secondary: Story = {
- args: {
- dashboard: secondaryDashboard,
- },
-};
diff --git a/src/lib/ui/stories/DashboardHeader.stories.tsx b/src/lib/ui/stories/DashboardHeader.stories.tsx
deleted file mode 100644
index 8263dd1..0000000
--- a/src/lib/ui/stories/DashboardHeader.stories.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { DashboardHeader } from "../components/DashboardHeader";
-import { moduleBlocks } from "./story-data";
-
-const meta = {
- title: "UI/DashboardHeader",
- component: DashboardHeader,
- args: {
- eyebrow: "Preview Surface",
- title: "Operations Console",
- subtitle: "Generic dashboard header",
- module: moduleBlocks.compact,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const WithModule: Story = {};
diff --git a/src/lib/ui/stories/DashboardOnePager.stories.tsx b/src/lib/ui/stories/DashboardOnePager.stories.tsx
deleted file mode 100644
index c44364b..0000000
--- a/src/lib/ui/stories/DashboardOnePager.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { DashboardFrame } from "../components/DashboardFrame";
-import { fullCompositionDashboard } from "./story-data";
-
-const meta = {
- title: "UI/DashboardOnePager",
- component: DashboardFrame,
- args: {
- dashboard: fullCompositionDashboard,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const OnePager: Story = {};
diff --git a/src/lib/ui/stories/DiagonalStripeField.stories.tsx b/src/lib/ui/stories/DiagonalStripeField.stories.tsx
deleted file mode 100644
index 4ca01f2..0000000
--- a/src/lib/ui/stories/DiagonalStripeField.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { DiagonalStripeField } from "../components/DiagonalStripeField";
-
-const meta = {
- title: "UI/DiagonalStripeField",
- component: DiagonalStripeField,
- args: {
- tone: "accent",
- density: "regular",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/FooterCell.stories.tsx b/src/lib/ui/stories/FooterCell.stories.tsx
deleted file mode 100644
index 3ba26c7..0000000
--- a/src/lib/ui/stories/FooterCell.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { FooterCell } from "../components/FooterCell";
-import { statusItems } from "./story-data";
-
-const meta = {
- title: "UI/FooterCell",
- component: FooterCell,
- args: {
- item: statusItems.ok,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Operational: Story = {};
-export const Linked: Story = {
- args: {
- item: statusItems.action,
- },
-};
diff --git a/src/lib/ui/stories/FooterStatusCell.stories.tsx b/src/lib/ui/stories/FooterStatusCell.stories.tsx
deleted file mode 100644
index 8097ba3..0000000
--- a/src/lib/ui/stories/FooterStatusCell.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { FooterStatusCell } from "../components/FooterStatusCell";
-import { statusItems } from "./story-data";
-
-const meta = {
- title: "UI/FooterStatusCell",
- component: FooterStatusCell,
- args: {
- item: statusItems.degraded,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Warning: Story = {};
diff --git a/src/lib/ui/stories/GridFrame.stories.tsx b/src/lib/ui/stories/GridFrame.stories.tsx
deleted file mode 100644
index a43ad22..0000000
--- a/src/lib/ui/stories/GridFrame.stories.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { GridFrame } from "../components/GridFrame";
-import { ModuleCard } from "../components/ModuleCard";
-import { moduleBlocks } from "./story-data";
-
-const meta = {
- title: "UI/GridFrame",
- component: GridFrame,
- render: (args) => (
-
-
-
-
-
- ),
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Dense: Story = {
- args: {
- density: "dense",
- },
-};
diff --git a/src/lib/ui/stories/IconButton.stories.tsx b/src/lib/ui/stories/IconButton.stories.tsx
deleted file mode 100644
index 35b58b9..0000000
--- a/src/lib/ui/stories/IconButton.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { IconButton } from "../components/IconButton";
-
-const meta = {
- title: "UI/IconButton",
- component: IconButton,
- args: {
- icon: "mdi:refresh",
- label: "Refresh status",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/IconGlyph.stories.tsx b/src/lib/ui/stories/IconGlyph.stories.tsx
deleted file mode 100644
index 3f1715a..0000000
--- a/src/lib/ui/stories/IconGlyph.stories.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { IconGlyph } from "../components/IconGlyph";
-
-const meta = {
- title: "UI/IconGlyph",
- component: IconGlyph,
- args: {
- name: "mdi:server-network",
- label: "Generic service",
- size: "md",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/LineChart.stories.tsx b/src/lib/ui/stories/LineChart.stories.tsx
deleted file mode 100644
index bbf8690..0000000
--- a/src/lib/ui/stories/LineChart.stories.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { LineChart } from "../components/LineChart";
-
-const meta = {
- title: "UI/LineChart",
- component: LineChart,
- args: {
- values: [12, 18, 24, 20, 36, 44],
- severity: "ok",
- label: "Generic trend",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Trend: Story = {};
diff --git a/src/lib/ui/stories/ModuleCard.stories.tsx b/src/lib/ui/stories/ModuleCard.stories.tsx
deleted file mode 100644
index 3366a55..0000000
--- a/src/lib/ui/stories/ModuleCard.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ModuleCard } from "../components/ModuleCard";
-import { moduleBlocks } from "./story-data";
-
-const meta = {
- title: "UI/ModuleCard",
- component: ModuleCard,
- args: {
- module: moduleBlocks.compact,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Compact: Story = {};
-export const Long: Story = {
- args: {
- module: moduleBlocks.long,
- },
-};
diff --git a/src/lib/ui/stories/Panel.stories.tsx b/src/lib/ui/stories/Panel.stories.tsx
deleted file mode 100644
index 730c2d9..0000000
--- a/src/lib/ui/stories/Panel.stories.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Panel } from "../components/Panel";
-import { ServiceRow } from "../components/ServiceRow";
-import { serviceRows } from "./story-data";
-
-const meta = {
- title: "UI/Panel",
- component: Panel,
- render: (args) => (
-
-
-
-
- ),
- args: {
- title: "Generic Panel",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Dense: Story = {};
diff --git a/src/lib/ui/stories/ProgressMeter.stories.tsx b/src/lib/ui/stories/ProgressMeter.stories.tsx
deleted file mode 100644
index d3bfdac..0000000
--- a/src/lib/ui/stories/ProgressMeter.stories.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ProgressMeter } from "../components/ProgressMeter";
-
-const meta = {
- title: "UI/ProgressMeter",
- component: ProgressMeter,
- args: {
- value: 72,
- severity: "ok",
- label: "Capacity",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Filled: Story = {};
diff --git a/src/lib/ui/stories/ScanlineField.stories.tsx b/src/lib/ui/stories/ScanlineField.stories.tsx
deleted file mode 100644
index 89e554e..0000000
--- a/src/lib/ui/stories/ScanlineField.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ScanlineField } from "../components/ScanlineField";
-
-const meta = {
- title: "UI/ScanlineField",
- component: ScanlineField,
- args: {
- tone: "accent",
- intensity: "medium",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/Separator.stories.tsx b/src/lib/ui/stories/Separator.stories.tsx
deleted file mode 100644
index e261f52..0000000
--- a/src/lib/ui/stories/Separator.stories.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Separator } from "../components/Separator";
-
-const meta = {
- title: "UI/Separator",
- component: Separator,
- args: {
- orientation: "horizontal",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Horizontal: Story = {};
diff --git a/src/lib/ui/stories/ServiceGroupPanel.stories.tsx b/src/lib/ui/stories/ServiceGroupPanel.stories.tsx
deleted file mode 100644
index 7183771..0000000
--- a/src/lib/ui/stories/ServiceGroupPanel.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ServiceGroupPanel } from "../components/ServiceGroupPanel";
-import { serviceGroups } from "./story-data";
-
-const meta = {
- title: "UI/ServiceGroupPanel",
- component: ServiceGroupPanel,
- args: {
- group: serviceGroups.mixed,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Mixed: Story = {};
diff --git a/src/lib/ui/stories/ServicePanel.stories.tsx b/src/lib/ui/stories/ServicePanel.stories.tsx
deleted file mode 100644
index 750c428..0000000
--- a/src/lib/ui/stories/ServicePanel.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ServicePanel } from "../components/ServicePanel";
-import { serviceGroups } from "./story-data";
-
-const meta = {
- title: "UI/ServicePanel",
- component: ServicePanel,
- args: {
- group: serviceGroups.long,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const LongList: Story = {};
diff --git a/src/lib/ui/stories/ServiceRow.stories.tsx b/src/lib/ui/stories/ServiceRow.stories.tsx
deleted file mode 100644
index 9513d2c..0000000
--- a/src/lib/ui/stories/ServiceRow.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ServiceRow } from "../components/ServiceRow";
-import { serviceRows } from "./story-data";
-
-const meta = {
- title: "UI/ServiceRow",
- component: ServiceRow,
- args: {
- service: serviceRows.normal,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Normal: Story = {};
-export const Warning: Story = {
- args: {
- service: serviceRows.degraded,
- },
-};
diff --git a/src/lib/ui/stories/SignalTrace.stories.tsx b/src/lib/ui/stories/SignalTrace.stories.tsx
deleted file mode 100644
index ae8785b..0000000
--- a/src/lib/ui/stories/SignalTrace.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { SignalTrace } from "../components/SignalTrace";
-
-const meta = {
- title: "UI/SignalTrace",
- component: SignalTrace,
- args: {
- tone: "accent",
- orientation: "horizontal",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Horizontal: Story = {};
diff --git a/src/lib/ui/stories/Sparkline.stories.tsx b/src/lib/ui/stories/Sparkline.stories.tsx
deleted file mode 100644
index efecfbd..0000000
--- a/src/lib/ui/stories/Sparkline.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { Sparkline } from "../components/Sparkline";
-
-const meta = {
- title: "UI/Sparkline",
- component: Sparkline,
- args: {
- values: [8, 14, 12, 24, 18, 30],
- severity: "ok",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/StatusBadge.stories.tsx b/src/lib/ui/stories/StatusBadge.stories.tsx
deleted file mode 100644
index 4513a08..0000000
--- a/src/lib/ui/stories/StatusBadge.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { StatusBadge } from "../components/StatusBadge";
-
-const meta = {
- title: "UI/StatusBadge",
- component: StatusBadge,
- args: {
- label: "Ready",
- severity: "ok",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Ready: Story = {};
diff --git a/src/lib/ui/stories/StatusStrip.stories.tsx b/src/lib/ui/stories/StatusStrip.stories.tsx
deleted file mode 100644
index 7b808fa..0000000
--- a/src/lib/ui/stories/StatusStrip.stories.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { StatusStrip } from "../components/StatusStrip";
-import { mixedStatusStrip } from "./story-data";
-
-const meta = {
- title: "UI/StatusStrip",
- component: StatusStrip,
- args: {
- id: "generic-status",
- items: mixedStatusStrip,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Mixed: Story = {};
diff --git a/src/lib/ui/stories/SystemState.stories.tsx b/src/lib/ui/stories/SystemState.stories.tsx
deleted file mode 100644
index 585540e..0000000
--- a/src/lib/ui/stories/SystemState.stories.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { SystemState } from "../components/SystemState";
-
-const meta = {
- title: "UI/SystemState",
- component: SystemState,
- args: {
- title: "Loading Dashboard",
- detail: "Fetching active model",
- severity: "loading",
- icon: "mdi:progress-clock",
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Loading: Story = {};
diff --git a/src/lib/ui/stories/TelemetryCard.stories.tsx b/src/lib/ui/stories/TelemetryCard.stories.tsx
deleted file mode 100644
index dbdf301..0000000
--- a/src/lib/ui/stories/TelemetryCard.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { TelemetryCard } from "../components/TelemetryCard";
-import { telemetryCards } from "./story-data";
-
-const meta = {
- title: "UI/TelemetryCard",
- component: TelemetryCard,
- args: {
- card: telemetryCards.percent,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Percent: Story = {};
-export const Danger: Story = {
- args: {
- card: telemetryCards.danger,
- },
-};
diff --git a/src/lib/ui/stories/TelemetryGrid.stories.tsx b/src/lib/ui/stories/TelemetryGrid.stories.tsx
deleted file mode 100644
index 6b0166e..0000000
--- a/src/lib/ui/stories/TelemetryGrid.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { TelemetryGrid } from "../components/TelemetryGrid";
-import { eightTelemetryCards, sixteenTelemetryCards } from "./story-data";
-
-const meta = {
- title: "UI/TelemetryGrid",
- component: TelemetryGrid,
- args: {
- cards: eightTelemetryCards,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const EightCards: Story = {};
-export const SixteenCards: Story = {
- args: {
- cards: sixteenTelemetryCards,
- },
-};
diff --git a/src/lib/ui/stories/TelemetryStrip.stories.tsx b/src/lib/ui/stories/TelemetryStrip.stories.tsx
deleted file mode 100644
index 76fc983..0000000
--- a/src/lib/ui/stories/TelemetryStrip.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { TelemetryStrip } from "../components/TelemetryStrip";
-import { eightTelemetryCards } from "./story-data";
-
-const meta = {
- title: "UI/TelemetryStrip",
- component: TelemetryStrip,
- args: {
- cards: eightTelemetryCards,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/WeatherModule.stories.tsx b/src/lib/ui/stories/WeatherModule.stories.tsx
deleted file mode 100644
index 6429de2..0000000
--- a/src/lib/ui/stories/WeatherModule.stories.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { WeatherModule } from "../components/WeatherModule";
-import { moduleBlocks } from "./story-data";
-
-const meta = {
- title: "UI/WeatherModule",
- component: WeatherModule,
- args: {
- module: moduleBlocks.compact,
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/src/lib/ui/stories/story-data.ts b/src/lib/ui/stories/story-data.ts
deleted file mode 100644
index 4085dac..0000000
--- a/src/lib/ui/stories/story-data.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-import { dashboardPreviewFixtures } from "../fixtures";
-import type {
- UiDashboardPreview,
- UiModuleBlock,
- UiServiceGroup,
- UiServiceRow,
- UiSeverity,
- UiStatusItem,
- UiTelemetryCard,
-} from "../types";
-
-export const primaryDashboard = dashboardPreviewFixtures.primary;
-export const secondaryDashboard = dashboardPreviewFixtures.secondary;
-
-export const emptyDashboard: UiDashboardPreview = {
- eyebrow: "Preview Surface",
- title: "Empty Console",
- subtitle: "No records available",
- telemetry: [],
- modules: [],
- serviceGroups: [],
- statusItems: [],
-};
-
-export const moduleBlocks: Record = {
- compact: {
- id: "module-compact",
- title: "Local Node",
- value: "21.4 C",
- detail: "static sample",
- icon: "mdi:weather-partly-cloudy",
- severity: "ok",
- },
- unavailable: {
- id: "module-unavailable",
- title: "External Signal",
- value: "n/a",
- detail: "fallback value",
- icon: "mdi:cloud-off-outline",
- severity: "unavailable",
- },
- long: {
- id: "module-long",
- title: "Regional Aggregate Signal",
- value: "manual review queued",
- detail: "long generic label wraps without layout shift",
- icon: "mdi:map-marker-radius-outline",
- severity: "warning",
- },
-};
-
-export const telemetryCards: Record = {
- percent: {
- id: "telemetry-percent",
- label: "Capacity Used",
- value: { kind: "percent", value: 82 },
- progress: 82,
- icon: "mdi:gauge",
- severity: "ok",
- detail: "static sample",
- sparkline: [24, 36, 28, 44, 52, 82],
- },
- bytes: {
- id: "telemetry-bytes",
- label: "Data Volume",
- value: { kind: "bytes", value: 982451653, precision: 1 },
- icon: "mdi:database",
- severity: "neutral",
- detail: "static sample",
- sparkline: [8, 12, 20, 18, 24, 29],
- },
- temperature: {
- id: "telemetry-temperature",
- label: "Node Temperature",
- value: { kind: "temperature", value: 21.4, precision: 1 },
- icon: "mdi:thermometer",
- severity: "neutral",
- detail: "static sample",
- sparkline: [18, 20, 19, 21, 20, 21.4],
- },
- latency: {
- id: "telemetry-latency",
- label: "Median Latency",
- value: { kind: "latency", value: 87 },
- icon: "mdi:timer-outline",
- severity: "warning",
- detail: "static sample",
- sparkline: [45, 51, 72, 63, 80, 87],
- },
- unavailable: {
- id: "telemetry-unavailable",
- label: "Remote Signal",
- value: { kind: "text", value: "n/a" },
- icon: "mdi:cloud-off-outline",
- severity: "unavailable",
- detail: "fallback value",
- },
- stale: {
- id: "telemetry-stale",
- label: "Sync Age",
- value: { kind: "text", value: "stale" },
- icon: "mdi:clock-alert-outline",
- severity: "stale",
- detail: "cached sample",
- sparkline: [22, 22, 22, 22, 22, 22],
- },
- danger: {
- id: "telemetry-danger",
- label: "Error Budget",
- value: { kind: "percent", value: 7 },
- progress: 7,
- icon: "mdi:alert-octagon-outline",
- severity: "danger",
- detail: "threshold breached",
- sparkline: [56, 41, 34, 20, 13, 7],
- },
- loading: {
- id: "telemetry-loading",
- label: "Pending Refresh",
- value: { kind: "text", value: "sync" },
- icon: "mdi:progress-clock",
- severity: "loading",
- detail: "waiting for update",
- },
- long: {
- id: "telemetry-long",
- label: "Very Long Generic Source Label That Must Wrap",
- value: { kind: "percent", value: 63 },
- progress: 63,
- icon: "mdi:chart-box-outline",
- severity: "neutral",
- detail: "long source label",
- sparkline: [14, 20, 28, 31, 49, 63],
- },
-};
-
-export const serviceRows: Record = {
- normal: row("service-normal", "Ingest", "Event intake pipeline", "mdi:arrow-collapse-down", "ok", "1.252 ms", {
- href: "#ingest",
- label: "Open ingest",
- }),
- down: row("service-down", "Replica", "Replica traffic cell", "mdi:access-point-off", "danger", "down"),
- degraded: row("service-degraded", "Scheduler", "Timed job coordinator", "mdi:calendar-clock", "warning", "1.487 ms"),
- stale: row("service-stale", "Archive", "Cold storage transfer", "mdi:archive-clock", "stale", "paused"),
- noLink: row("service-no-link", "Policy", "Rules evaluation", "mdi:shield-check", "neutral", "manual"),
- long: row(
- "service-long",
- "Long Generic Service Name That Wraps Cleanly",
- "Long generic service description with enough detail to exercise wrapping in constrained panels",
- "mdi:text-box-search-outline",
- "warning",
- "review queued",
- ),
-};
-
-export const serviceGroups: Record = {
- short: {
- id: "group-short",
- title: "Short List",
- services: [serviceRows.normal, serviceRows.degraded],
- },
- long: {
- id: "group-long",
- title: "Long List",
- services: [
- serviceRows.normal,
- serviceRows.degraded,
- serviceRows.stale,
- serviceRows.down,
- serviceRows.noLink,
- serviceRows.long,
- ],
- },
- empty: {
- id: "group-empty",
- title: "Empty Group",
- services: [],
- },
- mixed: {
- id: "group-mixed",
- title: "Mixed Statuses",
- services: [serviceRows.normal, serviceRows.down, serviceRows.degraded, serviceRows.stale],
- summary: [
- { id: "summary-ok", label: "Ok", value: "3", severity: "ok" },
- { id: "summary-warn", label: "Warn", value: "1", severity: "warning" },
- { id: "summary-down", label: "Down", value: "1", severity: "danger" },
- ],
- },
-};
-
-export const statusItems: Record = {
- ok: { id: "status-ok", label: "System", value: "Operational", severity: "ok" },
- degraded: { id: "status-degraded", label: "Routing", value: "Degraded", severity: "warning" },
- incident: { id: "status-incident", label: "Incident", value: "Active", severity: "danger" },
- syncing: { id: "status-syncing", label: "Refresh", value: "Syncing", severity: "loading" },
- stale: { id: "status-stale", label: "Last Sync", value: "18 minutes ago", severity: "stale" },
- action: {
- id: "status-action",
- label: "Action",
- value: "Inspect",
- severity: "neutral",
- link: { href: "#inspect", label: "Inspect status" },
- },
- long: {
- id: "status-long",
- label: "Long Generic Status Label",
- value: "long generic status value wraps",
- severity: "neutral",
- },
-};
-
-export const eightTelemetryCards = [
- telemetryCards.percent,
- telemetryCards.bytes,
- telemetryCards.temperature,
- telemetryCards.latency,
- telemetryCards.unavailable,
- telemetryCards.stale,
- telemetryCards.danger,
- telemetryCards.loading,
-];
-
-export const sixteenTelemetryCards = Array.from({ length: 16 }, (_, index) => {
- const card = eightTelemetryCards[index % eightTelemetryCards.length];
- return {
- ...card,
- id: `${card.id}-${index}`,
- label: `${card.label} ${index + 1}`,
- };
-});
-
-export const mixedStatusStrip = [
- statusItems.ok,
- statusItems.degraded,
- statusItems.incident,
- statusItems.syncing,
- statusItems.stale,
-];
-
-export const fullCompositionDashboard: UiDashboardPreview = {
- ...primaryDashboard,
- telemetry: eightTelemetryCards,
- modules: [moduleBlocks.compact, moduleBlocks.unavailable],
- serviceGroups: [serviceGroups.mixed, serviceGroups.short, serviceGroups.long],
- statusItems: mixedStatusStrip,
-};
-
-function row(
- id: string,
- label: string,
- description: string,
- icon: string,
- severity: UiSeverity,
- detail: string,
- link?: UiServiceRow["link"],
-): UiServiceRow {
- return { id, label, description, icon, severity, detail, link };
-}
diff --git a/src/lib/ui/storybook.test.ts b/src/lib/ui/storybook.test.ts
deleted file mode 100644
index f1303c7..0000000
--- a/src/lib/ui/storybook.test.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { existsSync, readdirSync, readFileSync } from "node:fs";
-import { join } from "node:path";
-import { describe, expect, test } from "vitest";
-
-const root = process.cwd();
-const componentsDir = join(root, "src/lib/ui/components");
-const storiesDir = join(root, "src/lib/ui/stories");
-
-const requiredStoryFiles = [
- "Badge.stories.tsx",
- "Button.stories.tsx",
- "DashboardFrame.stories.tsx",
- "DashboardHeader.stories.tsx",
- "DashboardOnePager.stories.tsx",
- "CornerBracketFrame.stories.tsx",
- "DiagonalStripeField.stories.tsx",
- "FooterCell.stories.tsx",
- "FooterStatusCell.stories.tsx",
- "GridFrame.stories.tsx",
- "IconButton.stories.tsx",
- "IconGlyph.stories.tsx",
- "LineChart.stories.tsx",
- "ModuleCard.stories.tsx",
- "Panel.stories.tsx",
- "ProgressMeter.stories.tsx",
- "Separator.stories.tsx",
- "ServiceGroupPanel.stories.tsx",
- "ServicePanel.stories.tsx",
- "ServiceRow.stories.tsx",
- "SignalTrace.stories.tsx",
- "Sparkline.stories.tsx",
- "StatusBadge.stories.tsx",
- "StatusStrip.stories.tsx",
- "SystemState.stories.tsx",
- "ScanlineField.stories.tsx",
- "TelemetryCard.stories.tsx",
- "TelemetryGrid.stories.tsx",
- "TelemetryStrip.stories.tsx",
- "WeatherModule.stories.tsx",
-] as const;
-
-const forbiddenStoryContent = [
- "dimension lab",
- "dimensionlab",
- "vince",
- "homepage",
-] as const;
-
-describe("Storybook inventory", () => {
- test("exposes scripts for local and static Storybook review", () => {
- const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
- scripts?: Record;
- };
-
- expect(packageJson.scripts?.storybook).toBeTypeOf("string");
- expect(packageJson.scripts?.storybook).toContain("storybook dev");
- expect(packageJson.scripts?.["build-storybook"]).toBe("storybook build");
- });
-
- test("has a story for every reusable dashboard UI component", () => {
- for (const filename of requiredStoryFiles) {
- expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
- }
- });
-
- test("loads dashboard component styles through the global app stylesheet", () => {
- const appStyles = readFileSync(join(root, "src/app.css"), "utf8");
- const dashboardFrame = readFileSync(
- join(componentsDir, "DashboardFrame.tsx"),
- "utf8",
- );
-
- expect(appStyles).toContain('./lib/ui/components/styles.css');
- expect(dashboardFrame).not.toContain('./styles.css');
- });
-
- test("keeps component and story files paired as the UI inventory changes", () => {
- const componentStoryFiles = readdirSync(componentsDir)
- .filter((filename) => filename.endsWith(".tsx") && !filename.endsWith(".test.tsx"))
- .map((filename) => filename.replace(".tsx", ".stories.tsx"));
-
- for (const filename of componentStoryFiles) {
- expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
- }
- });
-
- test("keeps Storybook fixtures generic and content-free", () => {
- const storyText = readdirSync(storiesDir)
- .filter((filename) => filename.endsWith(".tsx") || filename.endsWith(".ts"))
- .map((filename) => readFileSync(join(storiesDir, filename), "utf8").toLowerCase())
- .join("\n");
-
- for (const forbidden of forbiddenStoryContent) {
- expect(storyText).not.toContain(forbidden);
- }
- });
-
- test("includes style-only decorative primitives in the reusable UI inventory", () => {
- for (const component of [
- "CornerBracketFrame",
- "DiagonalStripeField",
- "ScanlineField",
- "SignalTrace",
- ]) {
- expect(existsSync(join(componentsDir, `${component}.tsx`)), `${component} is missing`).toBe(
- true,
- );
- expect(
- existsSync(join(storiesDir, `${component}.stories.tsx`)),
- `${component}.stories.tsx is missing`,
- ).toBe(true);
- }
- });
-
- test("keeps decorative primitives hidden from assistive technology", () => {
- for (const component of [
- "CornerBracketFrame",
- "DiagonalStripeField",
- "ScanlineField",
- "SignalTrace",
- ]) {
- const source = readFileSync(join(componentsDir, `${component}.tsx`), "utf8");
-
- expect(source).toContain('aria-hidden="true"');
- }
- });
-
- test("does not add deferred form/navigation primitives", () => {
- for (const component of ["Input", "ToggleGroup", "ScrollArea"]) {
- expect(existsSync(join(root, `src/lib/ui/components/${component}.tsx`))).toBe(false);
- expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false);
- }
- });
-
- test("does not keep legacy stories in the React Storybook inventory", () => {
- const legacyStoryExtension = [".stories", ".sve", "lte"].join("");
- const legacyStories = readdirSync(storiesDir).filter((filename) =>
- filename.endsWith(legacyStoryExtension),
- );
-
- expect(legacyStories).toEqual([]);
- });
-});
diff --git a/src/lib/ui/tokens.css b/src/lib/ui/tokens.css
deleted file mode 100644
index 56c2f7e..0000000
--- a/src/lib/ui/tokens.css
+++ /dev/null
@@ -1,81 +0,0 @@
-:root {
- color-scheme: dark;
- --ui-color-canvas: #020302;
- --ui-color-surface: #060706;
- --ui-color-surface-raised: #0b0d0c;
- --ui-color-line: rgba(244, 244, 244, 0.16);
- --ui-color-line-strong: rgba(244, 244, 244, 0.32);
- --ui-color-text: #f3f4ed;
- --ui-color-muted: #8d948c;
- --ui-color-dim: #555b55;
- --ui-color-accent: #d7ff00;
- --ui-color-ok: #bfff00;
- --ui-color-warning: #ffb020;
- --ui-color-danger: #ff1744;
- --ui-color-stale: #82909a;
- --ui-color-unavailable: #65707a;
- --ui-font-mono: "IBM Plex Mono", "Roboto Mono", "SFMono-Regular", Consolas, monospace;
- --ui-font-display: "Teko", "IBM Plex Mono", "Roboto Mono", monospace;
- --ui-space-1: 0.25rem;
- --ui-space-2: 0.5rem;
- --ui-space-3: 0.75rem;
- --ui-space-4: 1rem;
- --ui-space-5: 1.25rem;
- --ui-space-6: 1.5rem;
- --ui-radius-none: 0;
- --ui-border: 1px solid var(--ui-color-line);
- --ui-border-strong: 1px solid var(--ui-color-line-strong);
- --ui-shadow-hard: 0 0 0 1px rgba(215, 255, 0, 0.12) inset;
- --ui-z-base: 0;
- --ui-z-panel: 1;
- --ui-z-overlay: 10;
- --ui-density-card-min: 9.75rem;
- --ui-focus-ring: 0 0 0 2px var(--ui-color-canvas), 0 0 0 4px var(--ui-color-accent);
-}
-
-html {
- background: var(--ui-color-canvas);
-}
-
-body {
- min-width: 320px;
- min-height: 100vh;
- margin: 0;
- background:
- linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
- linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
- var(--ui-color-canvas);
- background-size: 48px 48px, 48px 48px, auto;
- color: var(--ui-color-text);
- font-family: var(--ui-font-mono);
- text-rendering: geometricPrecision;
-}
-
-button,
-input,
-textarea,
-select {
- font: inherit;
-}
-
-button:focus-visible,
-a:focus-visible,
-[tabindex]:focus-visible {
- outline: 0;
- box-shadow: var(--ui-focus-ring);
-}
-
-a {
- color: inherit;
-}
-
-@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- scroll-behavior: auto !important;
- transition-duration: 0.01ms !important;
- }
-}
diff --git a/src/lib/ui/types.ts b/src/lib/ui/types.ts
deleted file mode 100644
index 792a24b..0000000
--- a/src/lib/ui/types.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-export type UiSeverity =
- | "neutral"
- | "ok"
- | "warning"
- | "danger"
- | "stale"
- | "unavailable"
- | "loading";
-
-export type UiMetricKind =
- | "bytes"
- | "latency"
- | "number"
- | "percent"
- | "temperature"
- | "text";
-
-export interface UiMetricValue {
- kind: UiMetricKind;
- value: number | string;
- unit?: string;
- precision?: number;
-}
-
-export interface UiLink {
- href: string;
- label?: string;
- external?: boolean;
-}
-
-export interface UiTelemetryCard {
- id: string;
- label: string;
- value: UiMetricValue;
- detail?: string;
- description?: string;
- icon?: string;
- severity: UiSeverity;
- progress?: number;
- sparkline?: number[];
-}
-
-export interface UiServiceRow {
- id: string;
- label: string;
- description: string;
- icon?: string;
- severity: UiSeverity;
- detail?: string;
- link?: UiLink;
-}
-
-export interface UiServiceGroup {
- id: string;
- layout?: "grid" | "list";
- title: string;
- services: UiServiceRow[];
- summary?: UiStatusItem[];
-}
-
-export interface UiStatusItem {
- id: string;
- label: string;
- value: string;
- severity?: UiSeverity;
- link?: UiLink;
-}
-
-export interface UiModuleBlock {
- id: string;
- title?: string;
- label?: string;
- value?: string;
- detail?: string;
- icon?: string;
- severity?: UiSeverity;
-}
-
-export interface UiDashboardPreview {
- title: string;
- subtitle?: string;
- eyebrow?: string;
- telemetry: UiTelemetryCard[];
- serviceGroups: UiServiceGroup[];
- modules: UiModuleBlock[];
- statusItems: UiStatusItem[];
- statusStripId?: string;
-}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
deleted file mode 100644
index bd0c391..0000000
--- a/src/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
diff --git a/src/server/routes/dashboard.test.ts b/src/server/routes/dashboard.test.ts
deleted file mode 100644
index ebe575b..0000000
--- a/src/server/routes/dashboard.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { describe, expect, test } from "vitest";
-import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab";
-import { loadDashboardResponse } from "./dashboard";
-
-describe("dashboard API route", () => {
- test("returns ready dashboard runtime state from the existing model loader", async () => {
- const response = await loadDashboardResponse({
- disableLiveDatasources: true,
- refreshSeedDocument: true,
- seedIfEmpty: true,
- });
-
- expect(response.state).toBe("ready");
- if (response.state !== "ready") throw new Error("expected ready dashboard");
- expect(response.document.metadata.title).toBe(
- dimensionLabDashboardFixture.metadata.title,
- );
- });
-});
diff --git a/src/server/routes/dashboard.ts b/src/server/routes/dashboard.ts
deleted file mode 100644
index 21c30e4..0000000
--- a/src/server/routes/dashboard.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import {
- loadDashboardRuntime,
- type DashboardRuntimeOptions,
- type DashboardRuntimeState,
-} from "$lib/server/dashboard";
-import { resolveDashboardDatasources } from "$lib/server/datasources";
-
-export interface LoadDashboardResponseOptions
- extends Pick<
- DashboardRuntimeOptions,
- "refreshSeedDocument" | "seedIfEmpty" | "seedDocument"
- > {
- disableLiveDatasources?: boolean;
-}
-
-export async function loadDashboardResponse(
- options: LoadDashboardResponseOptions = {},
-): Promise {
- const dashboard = loadDashboardRuntime(undefined, {
- refreshSeedDocument: options.refreshSeedDocument ?? true,
- seedIfEmpty: options.seedIfEmpty ?? true,
- seedDocument: options.seedDocument,
- });
-
- if (dashboard.state !== "ready") {
- return dashboard;
- }
-
- if (options.disableLiveDatasources || process.env.DISABLE_LIVE_DATASOURCES === "1") {
- return dashboard;
- }
-
- return {
- ...dashboard,
- document: await resolveDashboardDatasources(dashboard.document),
- };
-}
-
-export async function handleDashboardRoute(): Promise {
- return Response.json(await loadDashboardResponse());
-}
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png
deleted file mode 100644
index fa3b3bf..0000000
Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-desktop-chromium-desktop-linux.png and /dev/null differ
diff --git a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png b/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png
deleted file mode 100644
index a199b15..0000000
Binary files a/tests/e2e/dashboard.spec.ts-snapshots/dashboard-mobile-chromium-mobile-linux.png and /dev/null differ
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..dcde3db
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "ignoreDeprecations": "6.0",
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "target": "ES2022",
+ "types": ["node", "bun-types", "react", "react-dom"]
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index ab92320..8fe9b00 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,35 +1,8 @@
{
- "compilerOptions": {
- "allowJs": true,
- "baseUrl": ".",
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "ignoreDeprecations": "6.0",
- "jsx": "react-jsx",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "paths": {
- "$lib/*": ["src/lib/*"]
- },
- "resolveJsonModule": true,
- "skipLibCheck": true,
- "sourceMap": true,
- "strict": true,
- "target": "ES2022",
- "types": ["node", "bun-types", "react", "react-dom"]
- },
- "include": [
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.ts",
- "vite.config.ts",
- "playwright.config.ts",
- "drizzle.config.ts"
- ],
- "exclude": [
- "build",
- "node_modules",
- ".storybook"
+ "files": [],
+ "references": [
+ { "path": "./packages/dashboard-model" },
+ { "path": "./apps/web" },
+ { "path": "./packages/ui" }
]
}
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 0000000..e1b2e7c
--- /dev/null
+++ b/turbo.json
@@ -0,0 +1,98 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "globalDependencies": [
+ "bun.lock",
+ "package.json",
+ "tsconfig.base.json",
+ "tsconfig.json"
+ ],
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "inputs": ["$TURBO_DEFAULT$", ".env*"],
+ "outputs": ["dist/**", "build/**"],
+ "env": ["NODE_ENV", "VITE_*"]
+ },
+ "check": {
+ "dependsOn": ["^build"],
+ "outputs": []
+ },
+ "test:unit": {
+ "dependsOn": ["^build"],
+ "outputs": [],
+ "env": [
+ "AGENT_CONFIG_TOKEN",
+ "DASHBOARD_MIGRATIONS_DIR",
+ "DATABASE_URL",
+ "DISABLE_LIVE_DATASOURCES",
+ "PROMETHEUS_BASE_URL"
+ ]
+ },
+ "build-storybook": {
+ "dependsOn": ["^build"],
+ "inputs": ["$TURBO_DEFAULT$", ".env*"],
+ "outputs": ["storybook-static/**"],
+ "env": ["NODE_ENV", "STORYBOOK_*", "VITE_*"]
+ },
+ "test:e2e": {
+ "dependsOn": [
+ "build",
+ "^build",
+ "@dimensionlab/ui#build-storybook"
+ ],
+ "outputs": ["test-results/**", "playwright-report/**"],
+ "env": [
+ "AGENT_CONFIG_TOKEN",
+ "CI",
+ "DASHBOARD_MIGRATIONS_DIR",
+ "DATABASE_URL",
+ "DISABLE_LIVE_DATASOURCES",
+ "PLAYWRIGHT_DATABASE_URL",
+ "PLAYWRIGHT_PORT",
+ "PLAYWRIGHT_STORYBOOK_PORT",
+ "PROMETHEUS_BASE_URL",
+ "STORYBOOK_STATIC_PORT"
+ ]
+ },
+ "db:generate": {
+ "cache": false
+ },
+ "db:check": {
+ "outputs": [],
+ "env": ["DATABASE_URL"]
+ },
+ "dev": {
+ "dependsOn": ["^build"],
+ "cache": false,
+ "persistent": true,
+ "env": [
+ "AGENT_CONFIG_TOKEN",
+ "DASHBOARD_DEV_API_HOST",
+ "DASHBOARD_DEV_API_PORT",
+ "DASHBOARD_DEV_API_TARGET",
+ "DASHBOARD_MIGRATIONS_DIR",
+ "DATABASE_URL",
+ "HOST",
+ "PORT",
+ "PROMETHEUS_BASE_URL"
+ ]
+ },
+ "preview": {
+ "cache": false,
+ "persistent": true,
+ "env": [
+ "AGENT_CONFIG_TOKEN",
+ "DASHBOARD_MIGRATIONS_DIR",
+ "DATABASE_URL",
+ "HOST",
+ "PORT",
+ "PROMETHEUS_BASE_URL"
+ ]
+ },
+ "storybook": {
+ "cache": false,
+ "persistent": true,
+ "env": ["HOST", "PORT", "STORYBOOK_*", "VITE_*"]
+ }
+ }
+}