Compare commits

...
Sign in to create a new pull request.

108 commits

Author SHA1 Message Date
a2c998b894 Merge pull request 'fix(web): remove prompt registry shortcut' (#57)
All checks were successful
Dimension Lab website / ci (push) Successful in 20s
Dimension Lab website / deploy (push) Successful in 33s
fix(web): remove prompt registry shortcut
2026-06-22 02:58:09 +02:00
vince
799299efca test(web): update dashboard e2e snapshots
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-22 02:57:13 +02:00
vince
6405254e42 fix(web): remove prompt registry shortcut
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-22 02:49:30 +02:00
8b0ef39cdf Merge pull request 'ci: use dedicated host deploy runner' (#56) from codex/use-host-deploy-runner into main
All checks were successful
Dimension Lab website / ci (push) Successful in 19s
Dimension Lab website / deploy (push) Successful in 33s
2026-06-20 17:41:46 +02:00
vince
d00a7b3b80 ci: use dedicated host deploy runner
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 18s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:37:38 +02:00
da9a5a942d Merge pull request 'docs(ci): document Forgejo deploy runner option' (#55)
Some checks failed
Dimension Lab website / ci (push) Successful in 18s
Dimension Lab website / deploy (push) Failing after 4s
2026-06-20 17:24:41 +02:00
vince
16c69a1065 docs(ci): document Forgejo deploy runner option
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 20s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:22:16 +02:00
b087e65978 Merge pull request 'ci: deploy website from Forgejo Actions' (#54)
Some checks failed
Dimension Lab website / ci (push) Successful in 20s
Dimension Lab website / deploy (push) Failing after 4s
2026-06-20 17:18:41 +02:00
vince
35cb1574b4 fix(ci): allow deploy job Podman socket access
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:12:29 +02:00
vince
d61d296ea1 fix(ci): verify deployment socket and rollback image
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 19s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 17:10:08 +02:00
vince
006c7e041f fix(ci): guard deploy auto fallback
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:59:36 +02:00
vince
a010520a94 fix(ci): run web unit tests with Bun
All checks were successful
Dimension Lab website / ci (pull_request) Successful in 17s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:58:13 +02:00
vince
7e4d13a85c fix(ci): harden website deploy rollback
Some checks failed
Dimension Lab website / ci (pull_request) Failing after 15s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:54:18 +02:00
vince
96fe4d26c7 ci: deploy website from Forgejo Actions
Some checks failed
Dimension Lab website / ci (pull_request) Failing after 35s
Dimension Lab website / deploy (pull_request) Has been skipped
2026-06-20 16:45:40 +02:00
29077923e8 Merge pull request 'fix(web): ignore stale aggregate health snapshots' (#53) 2026-06-20 15:54:33 +02:00
vince
3188d8b905 fix(web): ignore stale aggregate health snapshots 2026-06-20 15:49:22 +02:00
216545b838 Merge pull request 'perf(web): stream dashboard tile events' (#52)
Merge PR #52 from codex/dashboard-sse-events
2026-06-20 15:26:49 +02:00
vince
41fb964ebb perf(web): stream dashboard tile events 2026-06-20 15:23:14 +02:00
a66f1fdd57 Merge pull request 'perf(web): batch dashboard tile hydration' (#51)
Merge PR #51 from codex/dashboard-batch-tiles
2026-06-20 15:18:26 +02:00
vince
a7d0a4c3b2 fix(web): use structured dashboard tile match keys 2026-06-20 15:16:51 +02:00
vince
de87464ef1 perf(web): batch dashboard tile hydration 2026-06-20 15:12:18 +02:00
63b008095f Merge pull request 'perf(web): add dashboard hydration observability' (#50)
Merge PR #50 from codex/dashboard-observability
2026-06-20 15:06:55 +02:00
vince
0d27920a78 fix(web): preserve coalesced tile error logs 2026-06-20 15:04:51 +02:00
vince
328f6e00bf perf(web): add dashboard hydration observability 2026-06-20 14:58:25 +02:00
b6de67f081 Merge pull request 'perf(web): adapt dashboard refresh hydration' (#49)
Merge PR #49 from codex/dashboard-adaptive-hydration
2026-06-20 14:52:01 +02:00
vince
a071df3194 perf(web): adapt dashboard refresh hydration 2026-06-20 14:47:53 +02:00
30bff9da37 Merge pull request 'perf(web): restore last known dashboard tiles' (#48)
Merge PR #48 from codex/dashboard-last-known-tiles
2026-06-20 14:41:14 +02:00
vince
bac809952a fix(web): validate restored dashboard tile shapes 2026-06-20 14:38:19 +02:00
vince
88c448258d fix(web): ignore malformed dashboard tile snapshots 2026-06-20 14:32:09 +02:00
vince
4ce42fb52b perf(web): restore last known dashboard tiles 2026-06-20 14:25:43 +02:00
243d71686d Merge pull request 'perf(web): share service health snapshots' (#47)
Merge PR #47 from codex/dashboard-service-snapshot
2026-06-20 14:19:36 +02:00
vince
5e66367687 perf(web): share service health snapshots 2026-06-20 14:17:31 +02:00
9aa30b07db Merge pull request 'perf(web): cache dashboard tile responses' (#46)
Merge PR #46 from codex/dashboard-tile-cache
2026-06-20 14:09:57 +02:00
vince
bc1f64d566 perf(web): cache dashboard tile responses 2026-06-20 14:07:42 +02:00
cda40401ca Merge pull request 'perf(web): queue dashboard tile hydration' (#45)
Merge PR #45 from codex/dashboard-hydration-scheduler
2026-06-20 14:00:33 +02:00
vince
fd0559a143 perf(web): queue dashboard tile hydration 2026-06-20 13:59:02 +02:00
83c2c0f4a4 Merge pull request 'perf(web): pause dashboard refresh when inactive' (#44)
Merge PR #44 from codex/dashboard-client-lifecycle
2026-06-20 13:54:12 +02:00
vince
74d9aaf2b4 perf(web): pause dashboard refresh when inactive 2026-06-20 13:51:30 +02:00
b0b09eb8ad Merge pull request 'fix(web): skip install scripts in container deps stage' (#43) from codex/fix-website-container-build into main 2026-06-20 13:16:29 +02:00
vince
631e789546 fix(web): skip install scripts in container deps stage 2026-06-20 13:14:45 +02:00
9acaa18e6d Merge pull request 'feat(web): hydrate dashboard tiles independently' (#42) from codex/independent-tile-hydration into main 2026-06-20 12:38:54 +02:00
vince
2a1d94195e fix(web): scope dashboard tile hydration 2026-06-20 12:34:52 +02:00
vince
2963c5dfde Merge remote-tracking branch 'origin/main' into codex/independent-tile-hydration 2026-06-20 12:32:14 +02:00
73a15a2b8b Merge pull request #41 from codex/ui-submodule
refactor: move ui package to submodule
2026-06-20 12:24:59 +02:00
vince
23c8f0964c feat(web): hydrate dashboard tiles independently 2026-06-20 12:21:36 +02:00
vince
831b1b6640 fix: document ui submodule setup 2026-06-20 12:21:18 +02:00
vince
7a3a5b8df7 refactor: move ui package to submodule 2026-06-20 12:11:45 +02:00
021f2cf20e Merge pull request #40 from codex/turbo-env-inputs
fix(turbo): hash local env inputs
2026-06-20 07:52:40 +02:00
vince
24e7e8eda0 fix(turbo): hash local env inputs 2026-06-20 07:48:54 +02:00
3ff41e771b Merge pull request #39 from codex/ui-prop-type-exports
feat(ui): export component prop types
2026-06-20 07:41:51 +02:00
vince
5dccd70e0b test(ui): assert public prop exports are detected 2026-06-20 07:40:52 +02:00
vince
2535dccff2 feat(ui): export component prop types 2026-06-20 07:37:15 +02:00
7b96a7cb76 Merge pull request #38 from codex/turbo-ui-entrypoints
feat(ui): expose grouped component entrypoints
2026-06-20 07:34:36 +02:00
vince
ef43aac6df feat(ui): expose grouped component entrypoints 2026-06-20 07:31:35 +02:00
243d05cef7 Merge pull request #37 from codex/turbo-package-exports
refactor(turbo): consume workspace package exports
2026-06-20 07:27:51 +02:00
vince
2d779165cd fix(turbo): keep package exports fresh in dev 2026-06-20 07:26:29 +02:00
vince
a77826eab9 refactor(turbo): consume workspace package exports 2026-06-20 07:20:18 +02:00
9240ae4925 Merge pull request #36 from codex/turbo-pruned-container
build(container): use turbo pruned web workspace
2026-06-20 07:15:09 +02:00
vince
74e70cf714 fix(container): exclude generated files from pruned build 2026-06-20 07:12:46 +02:00
vince
3e46a2c0cc build(container): use turbo pruned web workspace 2026-06-20 07:01:15 +02:00
220fb55a47 Merge pull request #35 from codex/turbo-ui-filetree
refactor(ui): organize component source tree
2026-06-20 06:52:12 +02:00
vince
fe8d1e5642 test(ui): derive story inventory from component tree 2026-06-20 06:50:10 +02:00
vince
32ad2cebe4 refactor(ui): organize component source tree 2026-06-20 06:43:09 +02:00
8927f2ab8a Merge pull request #34 from codex/turbo-package-boundaries
refactor(model): extract dashboard model package
2026-06-20 06:36:31 +02:00
vince
9311b81102 fix(container): stage model package manifest 2026-06-20 06:34:31 +02:00
vince
7a6ad8ab0e refactor(model): extract dashboard model package 2026-06-20 06:26:19 +02:00
22f50d64f9 Merge pull request #33 from codex/turbo-idiomatic-workspace
build(turbo): normalize workspace task graph
2026-06-20 06:19:39 +02:00
vince
c8183bcc47 fix(turbo): rely on graph build before e2e 2026-06-20 06:16:41 +02:00
vince
ae6eabed6f build(turbo): normalize workspace task graph 2026-06-20 06:10:16 +02:00
4fc89c4d3f Merge pull request #32 from codex/turbo-component-library
refactor: split website and reusable UI package
2026-06-20 05:52:08 +02:00
vince
72309c0c09 docs: fix bun workspace command examples 2026-06-20 05:51:18 +02:00
vince
e93f4aabb7 fix(scripts): run web app wrappers with cwd 2026-06-20 05:49:19 +02:00
vince
b4e626a868 refactor(web): move website into turbo app workspace 2026-06-20 05:41:36 +02:00
vince
2664804e91 refactor(ui): extract reusable component package 2026-06-20 05:27:09 +02:00
vince
87261b5a3f build: add turbo workspace manifests 2026-06-20 05:23:04 +02:00
vince
4eadd21f71 docs: plan turbo component library migration 2026-06-20 05:20:31 +02:00
vince
786a283f31 docs: specify turbo component library migration 2026-06-20 05:17:28 +02:00
6b476bc91a Merge pull request 'feat(ui): sharpen dashboard console visual system' (#31)
feat(ui): sharpen dashboard console visual system
2026-06-20 02:25:14 +02:00
vince
8bb5052dd5 fix(ui): keep console layout usable at mid widths 2026-06-20 02:21:16 +02:00
vince
0c75220adc feat(ui): sharpen dashboard console visual system 2026-06-20 02:11:16 +02:00
739d627ae3 Merge pull request 'fix(ui): align theme toggle with dashboard style' (#30) from codex/brutalist-theme-toggle into main 2026-06-20 01:38:31 +02:00
vince
9c948bd746 fix(ui): align theme toggle with dashboard style 2026-06-20 01:33:27 +02:00
c84de9aaa9 Merge pull request #29 from codex/light-mode-toggle
feat(ui): add light theme toggle
2026-06-20 01:17:48 +02:00
vince
264c3a7edb test(ui): rebuild storybook before e2e 2026-06-20 01:13:33 +02:00
vince
0b2deb8405 test(ui): verify storybook light theme 2026-06-20 01:07:26 +02:00
vince
b923ac8947 fix(ui): harden theme toggle accessibility 2026-06-20 00:58:30 +02:00
vince
cdb269e505 feat(ui): add light theme toggle 2026-06-20 00:49:22 +02:00
bbc81cea0d Merge pull request #28 from codex/react-migration
feat(ui): migrate dashboard runtime to React
2026-06-20 00:15:09 +02:00
vince
b8e39348e8 fix(dev): proxy api during react development 2026-06-20 00:10:26 +02:00
vince
b1d196e4ca fix(ui): restore react dashboard visual parity 2026-06-19 23:56:49 +02:00
vince
72d56cb3c9 refactor: remove svelte runtime 2026-06-19 23:49:48 +02:00
vince
76fe468df5 feat(storybook): migrate stories to react 2026-06-19 23:45:54 +02:00
vince
92a8990e7a feat(app): render dashboard with react 2026-06-19 23:42:25 +02:00
vince
73abcc1b69 feat(ui): port dashboard components to react 2026-06-19 23:41:19 +02:00
vince
a71d66b1b8 feat: add react api and shadcn foundation 2026-06-19 23:35:46 +02:00
vince
f6d6d39a7b build: add react vite scaffold 2026-06-19 23:26:28 +02:00
vince
abfb885851 docs: plan react runtime migration 2026-06-19 23:23:45 +02:00
vince
ef9715c5a6 docs: define react runtime migration 2026-06-19 23:21:09 +02:00
e0a34f7f2f Merge pull request 'feat(dashboard): replace homepage with model-driven console' (#27) from codex/agent-config-tier into main 2026-06-19 13:19:25 +02:00
vince
83efa5b051 build: add internal container deployment 2026-06-19 13:13:20 +02:00
eeac37e58e Merge pull request #26 from codex/agent-config-tier
feat(agent-config): add typed dashboard mutation API
2026-06-19 12:25:31 +02:00
vince
b69de71029 fix(agent-config): stabilize patch and target resolution 2026-06-19 12:21:15 +02:00
vince
9b7f20c486 feat(agent-config): add typed dashboard mutation API 2026-06-19 12:12:16 +02:00
8af0fce6f1 Merge pull request #25 from codex/dashboard-type-balance
fix(dashboard): rebalance type scale
2026-06-19 04:24:39 +02:00
vince
ff4e6c6d36 fix(dashboard): rebalance type scale 2026-06-19 04:21:57 +02:00
bf6e7c0cc9 Merge pull request #24 from codex/dense-bookmark-dividers
fix(dashboard): compact bookmark dividers
2026-06-19 04:14:20 +02:00
vince
ba904ef4f9 fix(dashboard): compact bookmark dividers 2026-06-19 04:11:14 +02:00
769d624799 Merge pull request #23 from codex/issue-1-observability-dashboard
feat(dashboard): add live observability overview
2026-06-19 03:57:28 +02:00
163 changed files with 11303 additions and 4396 deletions

22
.containerignore Normal file
View file

@ -0,0 +1,22 @@
.git
.svelte-kit
build
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

View file

@ -0,0 +1,85 @@
name: Dimension Lab website
on:
pull_request:
types:
- opened
- synchronize
- reopened
push:
branches:
- main
workflow_dispatch:
concurrency:
group: dimensionlab-website-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
ci:
runs-on: docker
timeout-minutes: 30
steps:
- name: Checkout
uses: https://data.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
submodules: false
- name: Initialize submodules
run: |
git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
git submodule update --init --recursive
- name: Install Bun
run: |
curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.14"
"$HOME/.bun/bin/bun" --version
- name: Check, test, and build
run: |
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
bun install --frozen-lockfile
bun run check
bun run test
bun run build
deploy:
needs: ci
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: deploy
timeout-minutes: 30
steps:
- name: Checkout
run: |
if [ -d .git ]; then
git remote set-url origin git@git.dimensionlab.net:vince/dimensionlab-website.git
else
git init
git remote add origin git@git.dimensionlab.net:vince/dimensionlab-website.git
fi
git fetch --force --prune --depth=1 origin "$GITHUB_SHA"
git checkout --force --detach "$GITHUB_SHA"
git clean -ffdx
- name: Initialize submodules
run: |
git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
git submodule update --init --recursive
- name: Verify Podman deployment socket
run: |
command -v podman
command -v systemctl
unit="$(timeout 15s podman inspect dimensionlab-website --format '{{ index .Config.Labels "PODMAN_SYSTEMD_UNIT" }}')"
test "$unit" = "dimensionlab-website.service"
- name: Deploy production website
env:
DEPLOY_CONTAINER_CLI: podman
DEPLOY_EVENT_NAME: ${{ github.event_name }}
DEPLOY_REF: ${{ github.ref }}
DEPLOY_RESTART_STRATEGY: quadlet-container
DEPLOY_SHA: ${{ github.sha }}
run: scripts/deploy-dimensionlab-website.sh

6
.gitignore vendored
View file

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

4
.gitmodules vendored Normal file
View file

@ -0,0 +1,4 @@
[submodule "packages/ui"]
path = packages/ui
url = ssh://git@git.dimensionlab.net/vince/dimensionlab-ui.git
branch = main

View file

@ -1,20 +0,0 @@
import type { StorybookConfig } from "@storybook/sveltekit";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|ts|svelte)"],
staticDirs: ["../static"],
addons: [
"@storybook/addon-svelte-csf",
"@storybook/addon-a11y",
"@storybook/addon-vitest",
],
framework: {
name: "@storybook/sveltekit",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

View file

@ -1,35 +0,0 @@
import "../src/app.css";
import type { Preview } from "@storybook/sveltekit";
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;

143
README.md
View file

@ -1,15 +1,30 @@
# Dimension Lab Website
Standalone SvelteKit 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 development server.
- `bun run check`: run Svelte and 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,23 +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 SvelteKit build uses the Node adapter, and the current persistence runtime
is Bun because the MVP SQLite driver is `bun:sqlite`. Later issues add the
seed data expansion and rendering pipeline.
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
@ -87,10 +106,12 @@ Run the CI-ready release gate with:
bun run test:qa
```
The gate runs Svelte/TypeScript checks, Vitest coverage for model,
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.
@ -104,15 +125,63 @@ belong in validated model data, not reusable components.
## Deployment Notes
The production build uses the SvelteKit Node adapter, but the current SQLite
driver depends on Bun. 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 `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 -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" \
--env-file "$HOME/containers/dimensionlab-website/dimensionlab-website.env" \
localhost/dimensionlab-website:latest
```
The env file must provide `AGENT_CONFIG_TOKEN`. Runtime defaults inside the
image set `HOST=0.0.0.0`, `PORT=3000`,
`DATABASE_URL=file:/data/dimensionlab.sqlite`, and
`DASHBOARD_MIGRATIONS_DIR=/repo/apps/web/drizzle`.
### Forgejo Actions Deployment
Merges to `main` run `.forgejo/workflows/dimensionlab-website.yml`. Pull
requests run check, test, and build only; the deploy job is guarded to run only
for `push` events on `refs/heads/main`.
The workflow uses two runner classes. Pull request CI runs on the containerized
`docker` runner. Production deployment runs on a separate host runner with the
`deploy:host` label so the guarded deploy script can use the user's rootless
`podman` and `systemctl --user` commands directly. The deploy job uses a
shell-only `git fetch` checkout so the host runner does not need a Node runtime
for checkout actions.
```yaml
runner:
labels:
- deploy:host
```
The deploy job also performs a host preflight against the
`dimensionlab-website.service` Podman label before it builds or restarts the
production container. Do not give the general pull request runner deployment
socket access; keep deploy privileges on the dedicated `deploy` runner.

39
apps/web/Containerfile Normal file
View file

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

22
apps/web/index.html Normal file
View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Dimension Lab dashboard runtime" />
<title>Dimension Lab</title>
<script>
try {
const theme = localStorage.getItem("dashboard-ui-theme");
document.documentElement.dataset.uiTheme =
theme === "light" ? "light" : "dark";
} catch {
document.documentElement.dataset.uiTheme = "dark";
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

51
apps/web/package.json Normal file
View file

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

View file

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

238
apps/web/src/App.test.tsx Normal file
View file

@ -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(
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
/>,
);
expect(html).toContain("Loading Dashboard");
expect(html).toContain("Fetching active model");
});
test("renders an accessible theme toggle with the active theme", () => {
const html = renderToString(
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
theme="dark"
onThemeChange={() => 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(
<AppStateView
dashboard={{
state: "ready",
document: genericDashboardFixture,
schemaVersion: "dashboard.v1",
currentRevisionId: "revision-1234567890",
}}
hydratingItemIds={new Set([
"telemetry:service-uptime",
"service:core-services:identity",
"module:ambient",
"status:runtime:status",
])}
/>,
);
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" }),
);
});
});

875
apps/web/src/App.tsx Normal file
View file

@ -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<typeof createDashboardTileSnapshotStore>;
export function AppStateView({
dashboard,
onThemeChange,
theme,
hydratingItemIds,
}: {
dashboard: DashboardRuntimeState;
hydratingItemIds?: ReadonlySet<string>;
onThemeChange?: (theme: UiTheme) => void;
theme?: UiTheme;
}) {
const themeToggle =
theme && onThemeChange ? (
<ThemeToggle theme={theme} onThemeChange={onThemeChange} />
) : null;
if (dashboard.state === "ready") {
const uiDashboard = markHydratingItems(
dashboardDocumentToUiDashboard(dashboard.document),
hydratingItemIds,
);
return (
<DashboardFrame
dashboard={uiDashboard}
actions={themeToggle}
/>
);
}
const detail = `${dashboard.subtitle}: ${dashboard.message}`;
const errors = dashboard.state === "invalid" ? dashboard.errors : [];
return (
<main className="state-shell" data-dashboard-state={dashboard.state}>
{themeToggle ? (
<div className="state-shell__actions">{themeToggle}</div>
) : null}
<SystemState
title={dashboard.title}
detail={detail}
severity={stateSeverity(dashboard.state)}
icon={stateIcon(dashboard.state)}
/>
{errors.length ? (
<ul aria-label="Validation errors">
{errors.map((error) => (
<li key={error}>{error}</li>
))}
</ul>
) : null}
</main>
);
}
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<DashboardRuntimeState | null>(null);
const [hydratingItemIds, setHydratingItemIds] = useState<Set<string>>(
() => new Set(),
);
const [theme, setTheme] = useState<UiTheme>(() => {
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<HTMLMetaElement>(
'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<Array<{ result: DashboardTileHydrationResult; tile: DashboardTileReference }>> {
try {
const response = await fetch("/api/dashboard/tiles", {
body: JSON.stringify({ tiles }),
headers: { "Content-Type": "application/json" },
method: "POST",
signal,
});
const batchResponse = (await response.json()) as DashboardTilesBatchResponse;
if (
cancelled ||
signal.aborted ||
run !== hydrationRun ||
!response.ok ||
batchResponse.state !== "ready"
) {
throw new Error("Dashboard tile batch hydration failed");
}
const responsesByKey = new Map(
batchResponse.tiles.map((tileResponse) => [
dashboardTileMatchKey(tileResponse.tile),
tileResponse,
]),
);
return tiles.map((tile) => {
const key = dashboardTileKey(tile);
try {
return {
tile,
result: applyDashboardTileHydrationResponse(
tile,
responsesByKey.get(dashboardTileMatchKey(tile)),
run,
signal,
snapshotContext,
),
};
} finally {
finishDashboardTileHydration(key, run, signal);
}
});
} catch (error) {
if (signal.aborted || cancelled || run !== hydrationRun) {
return tiles.map((tile) => ({ result: "aborted", tile }));
}
return Promise.all(
tiles.map(async (tile) => ({
tile,
result: await hydrateDashboardTile(tile, run, signal, snapshotContext),
})),
);
}
}
async function hydrateDashboardTile(
tile: DashboardTileReference,
run: number,
signal: AbortSignal,
snapshotContext: DashboardTileSnapshotStoreContext,
): Promise<DashboardTileHydrationResult> {
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 ? (
<AppStateView
dashboard={dashboard}
hydratingItemIds={hydratingItemIds}
theme={theme}
onThemeChange={setTheme}
/>
) : 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<void> {
if (signal.aborted) return;
await new Promise<void>((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<string>;
} {
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<DashboardTileResponse, { state: "ready" }>,
),
},
restoredItemIds: new Set([
...current.restoredItemIds,
dashboardTileKey(snapshot.response.tile),
]),
}),
{
dashboard,
restoredItemIds: new Set<string>(),
},
);
}
function markHydratingItems(
dashboard: UiDashboardPreview,
hydratingItemIds?: ReadonlySet<string>,
): 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<DashboardTileResponse, { state: "ready" }>,
): 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,
),
};
}

170
apps/web/src/app.css Normal file
View file

@ -0,0 +1,170 @@
@import "tailwindcss";
@import "@dimensionlab/ui/styles.css";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-sans);
--font-sans: 'Geist Variable', sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root,
.dark {
--background: var(--ui-color-canvas);
--foreground: var(--ui-color-text);
--card: var(--ui-color-surface);
--card-foreground: var(--ui-color-text);
--popover: var(--ui-color-surface-raised);
--popover-foreground: var(--ui-color-text);
--primary: var(--ui-color-accent);
--primary-foreground: var(--ui-color-canvas);
--secondary: var(--ui-color-surface-raised);
--secondary-foreground: var(--ui-color-text);
--muted: var(--ui-color-surface-raised);
--muted-foreground: var(--ui-color-muted);
--accent: var(--ui-color-accent);
--accent-foreground: var(--ui-color-canvas);
--destructive: var(--ui-color-danger);
--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);
--chart-3: var(--ui-color-warning);
--chart-4: var(--ui-color-danger);
--chart-5: var(--ui-color-stale);
--radius: 0.5rem;
--sidebar: var(--ui-color-surface);
--sidebar-foreground: var(--ui-color-text);
--sidebar-primary: var(--ui-color-accent);
--sidebar-primary-foreground: var(--ui-color-canvas);
--sidebar-accent: var(--ui-color-surface-raised);
--sidebar-accent-foreground: var(--ui-color-text);
--sidebar-border: rgba(244, 244, 244, 0.16);
--sidebar-ring: var(--ui-color-accent);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
html {
background: var(--ui-color-canvas);
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
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: auto, 40px 40px, 40px 40px, 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;
}
.state-shell {
display: grid;
min-height: 100vh;
align-content: center;
gap: var(--ui-space-3);
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: var(--ui-color-surface-module);
color: var(--ui-color-muted);
font-size: 0.76rem;
list-style-position: inside;
padding: var(--ui-space-3);
}
@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;
}
}

View file

@ -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<number, () => 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<void>((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<void>((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<typeof setTimeout>;
},
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<string>) => void) | undefined;
let closed = false;
const unsubscribe = subscribeToDashboardTileEvents({
createEventSource: (url) => {
expect(url).toBe("/api/dashboard/events");
return {
addEventListener(_type, eventListener) {
listener = eventListener;
},
close() {
closed = true;
},
onerror: null,
};
},
onTile: (tile) => received.push(tile),
});
listener?.({ data: JSON.stringify({ state: "ready" }) } as MessageEvent<string>);
expect(received).toEqual([{ state: "ready" }]);
unsubscribe();
expect(closed).toBe(true);
});
test("falls back when dashboard tile events are unavailable or fail", () => {
let unavailableCount = 0;
subscribeToDashboardTileEvents({
createEventSource: undefined,
onTile: () => undefined,
onUnavailable: () => {
unavailableCount += 1;
},
});
let errorHandler: (() => void) | null = null;
const unsubscribe = subscribeToDashboardTileEvents({
createEventSource: () => ({
addEventListener: () => undefined,
close: () => undefined,
get onerror() {
return errorHandler;
},
set onerror(handler) {
errorHandler = handler;
},
}),
onTile: () => undefined,
onUnavailable: () => {
unavailableCount += 1;
},
});
if (!errorHandler) throw new Error("expected error handler");
const triggerError = errorHandler as unknown as () => void;
triggerError();
unsubscribe();
expect(unavailableCount).toBe(2);
});
test("backs off failed tile keys and resets after success", () => {
const backoff = createDashboardTileBackoff();
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<string, string>();
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;
},
};
}

View file

@ -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<string, unknown>;
export type DashboardTileSnapshotItem = {
detail?: string;
id: string;
label?: string;
severity?: string;
value?: SnapshotMetricValue | string;
} & Record<string, unknown>;
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<void>;
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<TItem> {
concurrency: number;
hydrate: (item: TItem) => Promise<void> | void;
items: TItem[];
signal: AbortSignal;
}
export async function runDashboardHydrationQueue<TItem>(
options: DashboardHydrationQueueOptions<TItem>,
): Promise<void> {
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<TItem> {
batchSize?: number;
collectVisibleModelIds: () => Promise<ReadonlySet<string>>;
concurrency: number;
getModelId: (item: TItem) => string;
hydrate: (item: TItem) => Promise<void> | void;
hydrateBatch?: (items: TItem[]) => Promise<void> | void;
items: TItem[];
onAllItemsSettled?: () => void;
onVisibleItemsSettled?: () => void;
signal: AbortSignal;
waitForIdle: () => Promise<void>;
}
export async function runViewportAwareDashboardHydrationQueue<TItem>(
options: DashboardViewportHydrationQueueOptions<TItem>,
): Promise<void> {
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<TItem>(
options: DashboardViewportHydrationQueueOptions<TItem>,
items: TItem[],
): Promise<void> {
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<TItem>(options: {
batchSize: number;
hydrateBatch: (items: TItem[]) => Promise<void> | void;
items: TItem[];
signal: AbortSignal;
}): Promise<void> {
const batchSize = Math.max(1, Math.floor(options.batchSize));
for (let index = 0; index < options.items.length; index += batchSize) {
if (options.signal.aborted) return;
await options.hydrateBatch(options.items.slice(index, index + batchSize));
}
}
export function splitDashboardHydrationItemsByVisibility<TItem>(options: {
getModelId: (item: TItem) => string;
items: TItem[];
visibleModelIds: ReadonlySet<string>;
}): {
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<DashboardModelElement>;
}
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<typeof setTimeout>;
export interface DashboardVisibleModelIdCollectorOptions {
clearTimeout?: (handle: DashboardTimerHandle) => void;
createObserver?: DashboardIntersectionObserverFactory;
documentTarget: DashboardViewportElementSource;
modelIds: Iterable<string>;
setTimeout?: (callback: () => void, timeoutMs: number) => DashboardTimerHandle;
signal: AbortSignal;
timeoutMs?: number;
}
export async function collectVisibleDashboardModelIds(
options: DashboardVisibleModelIdCollectorOptions,
): Promise<Set<string>> {
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<string>();
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<void> {
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<void>((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<string>) => void,
): void;
close(): void;
onerror: (() => void) | null;
}
export interface DashboardTileEventSubscriptionOptions {
createEventSource?: (url: string) => DashboardTileEventSource;
onTile: (data: unknown) => void;
onUnavailable?: () => void;
url?: string;
}
export function subscribeToDashboardTileEvents(
options: DashboardTileEventSubscriptionOptions,
): () => void {
const createEventSource = options.createEventSource ||
(typeof globalThis.EventSource !== "undefined"
? (url: string) => new globalThis.EventSource(url)
: undefined);
if (!createEventSource) {
options.onUnavailable?.();
return () => undefined;
}
const source = createEventSource(options.url || "/api/dashboard/events");
source.addEventListener("dashboard-tile", (event) => {
try {
options.onTile(JSON.parse(event.data));
} catch {
// Ignore malformed diagnostics from an optional live transport.
}
});
source.onerror = () => {
source.close();
options.onUnavailable?.();
};
return () => source.close();
}
const dashboardTileBackoffDelaysMs = [15_000, 30_000, 60_000, 120_000];
export function createDashboardTileBackoff() {
const failures = new Map<string, { attempts: number; nextAttemptAt: number }>();
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<DashboardTileSnapshotResponse, { state: "ready" }>;
}
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<DashboardTileSnapshotPayload>;
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<DashboardTileSnapshotResponse, { state: "ready" }>;
},
): 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<string, unknown> {
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;
}

View file

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

View file

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

View file

@ -0,0 +1,80 @@
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(repoRoot, "packages", "ui", "src"),
join(appRoot, "src", "App.tsx"),
join(appRoot, "src", "app.css"),
join(appRoot, "src", "lib", "ui-adapter"),
];
const forbiddenTerms = [
"dimensionlab",
"dimension lab",
"vaultwarden",
"forgejo",
"grafana",
"uptime kuma",
"prometheus",
"backrest",
"open webui",
"comfyui",
"adminer",
"cockpit",
"ollama",
"dimensionlab.net",
];
describe("presentation content boundary", () => {
test("keeps environment-specific content out of route and UI implementation", () => {
const source = withoutInternalPackageScope(
presentationRoots.map(readPresentationSource).join("\n").toLowerCase(),
);
expect(forbiddenTerms.filter((term) => source.includes(term))).toEqual([]);
});
test("does not keep legacy presentation component files in the React runtime", () => {
const legacyExtension = [".sve", "lte"].join("");
expect(findFiles(join(appRoot, "src"), legacyExtension)).toEqual([]);
});
});
function readPresentationSource(path: string): string {
if (!existsSync(path)) return "";
const stats = statSync(path);
if (stats.isFile()) {
if (path.endsWith(".test.ts")) return "";
if (path.includes(`${join("packages", "ui", "src", "stories")}${"/"}`)) {
return "";
}
if (!/\.(tsx|ts|js|mjs|css|json)$/.test(path)) return "";
return readFileSync(path, "utf8");
}
return readdirSync(path)
.map((entry) => readPresentationSource(join(path, entry)))
.join("\n");
}
function findFiles(path: string, extension: string): string[] {
const stats = statSync(path);
if (stats.isFile()) return path.endsWith(extension) ? [path] : [];
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");
}

View file

@ -0,0 +1,595 @@
import { existsSync, rmSync } from "node:fs";
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 "@dimensionlab/dashboard-model";
import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
import {
AgentConfigAuthorizationError,
authorizeAgentConfigRequest,
handleAgentDashboardRequest,
previewDashboardChanges,
publishDashboardChanges,
rollbackDashboardRevision,
type AgentDashboardOperation,
type JsonPatchOperation,
} from ".";
const stores: DashboardStore[] = [];
const tempRoots: string[] = [];
afterEach(() => {
stores.splice(0).forEach((store) => store.close());
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
});
describe("agent dashboard configuration API", () => {
test("previews typed dashboard operations with an RFC 6902-compatible patch", () => {
const operations = exampleOperations();
const result = previewDashboardChanges(genericDashboardFixture, operations);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected preview success");
expect(result.document.serviceGroups[0]?.id).toBe("edge");
expect(result.document.serviceGroups[0]?.services[0]).toMatchObject({
id: "edge-router",
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://edge.example.test/api/status",
},
});
expect(result.document.layout.telemetry).toEqual([
"service-uptime",
"edge-latency",
"queue-depth",
]);
expect(genericDashboardFixture.serviceGroups.map((group) => group.id)).toEqual([
"core-services",
]);
expect(result.patch.length).toBeGreaterThan(0);
expect(result.patch.every((operation) => operation.path.startsWith("/"))).toBe(true);
expect(result.patch.map((operation) => operation.op)).toContain("add");
});
test("returns structured repairable errors for invalid operations", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "add_service",
groupId: "missing-group",
service: exampleService(),
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected preview failure");
expect(result.errors).toEqual([
expect.objectContaining({
code: "group_not_found",
operationIndex: 0,
path: "/serviceGroups",
}),
]);
});
test("rejects unsupported target-specific mutations", () => {
const datasourceResult = previewDashboardChanges(genericDashboardFixture, [
{
type: "connect_datasource",
target: { kind: "statusItem", stripId: "runtime", id: "status" },
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://status.example.test/api",
},
},
]);
expect(datasourceResult.ok).toBe(false);
if (datasourceResult.ok) throw new Error("expected datasource failure");
expect(datasourceResult.errors[0]).toMatchObject({
code: "unsupported_target",
operationIndex: 0,
path: "/statusStrips",
});
const thresholdResult = previewDashboardChanges(genericDashboardFixture, [
{
type: "set_status_rule",
target: { kind: "service", id: "identity" },
thresholds: { warning: 1 },
},
]);
expect(thresholdResult.ok).toBe(false);
if (thresholdResult.ok) throw new Error("expected threshold failure");
expect(thresholdResult.errors[0]).toMatchObject({
code: "unsupported_target",
operationIndex: 0,
path: "/serviceGroups",
});
});
test("removes status strip items through the shared remove operation", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "remove_item",
target: { kind: "statusItem", stripId: "runtime", id: "sync" },
},
]);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected remove success");
expect(result.document.statusStrips[0]?.items.map((item) => item.id)).toEqual([
"status",
]);
expect(result.patch.map((operation) => operation.op)).toContain("remove");
});
test("returns RFC 6902-applicable patches for multiple array removals", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "remove_item",
target: { kind: "statusItem", stripId: "runtime", id: "status" },
},
{
type: "remove_item",
target: { kind: "statusItem", stripId: "runtime", id: "sync" },
},
]);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected remove success");
expect(
result.patch
.filter((operation) => operation.op === "remove")
.map((operation) => operation.path),
).toEqual(["/statusStrips/0/items/1", "/statusStrips/0/items/0"]);
expect(applyJsonPatch(genericDashboardFixture, result.patch)).toMatchObject({
statusStrips: [{ items: [] }],
});
});
test("rejects ambiguous service and status targets", () => {
const duplicateDocument = documentWithDuplicateNestedIds();
const connect = previewDashboardChanges(duplicateDocument, [
{
type: "connect_datasource",
target: { kind: "service", id: "identity" },
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://identity.example.test/health",
},
},
]);
expect(connect.ok).toBe(false);
if (connect.ok) throw new Error("expected ambiguous service failure");
expect(connect.errors[0]).toMatchObject({
code: "ambiguous_target",
operationIndex: 0,
path: "/serviceGroups",
});
const remove = previewDashboardChanges(duplicateDocument, [
{
type: "remove_item",
target: { kind: "service", id: "identity" },
},
]);
expect(remove.ok).toBe(false);
if (remove.ok) throw new Error("expected ambiguous removal failure");
expect(remove.errors[0]).toMatchObject({
code: "ambiguous_target",
operationIndex: 0,
path: "/serviceGroups",
});
const status = previewDashboardChanges(duplicateDocument, [
{
type: "set_status_rule",
target: { kind: "statusItem", id: "status" },
value: "Healthy",
},
]);
expect(status.ok).toBe(false);
if (status.ok) throw new Error("expected ambiguous status failure");
expect(status.errors[0]).toMatchObject({
code: "ambiguous_target",
operationIndex: 0,
path: "/statusStrips",
});
});
test("rejects create_dashboard when it is not the first operation", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "remove_item",
target: { kind: "telemetry", id: "queue-depth" },
},
{
type: "create_dashboard",
document: genericDashboardFixture,
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected sequence failure");
expect(result.errors[0]).toMatchObject({
code: "invalid_operation_sequence",
operationIndex: 1,
path: "/1",
});
});
test("publishes valid operations as a persisted dashboard revision", async () => {
const { dbPath, store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
message: "initial dashboard",
});
const result = publishDashboardChanges(store, exampleOperations(), {
actor: "agent",
message: "add edge router",
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected publish success");
expect(existsSync(dbPath)).toBe(true);
expect(result.revision.operation).toBe("commit");
expect(result.revision.actor).toBe("agent");
expect(result.revision.message).toBe("add edge router");
expect(result.previousRevisionId).toBe(seed.id);
expect(result.patch.length).toBeGreaterThan(0);
expect(store.getActiveDashboard()?.document.serviceGroups[0]?.id).toBe("edge");
expect(store.listRevisions()).toHaveLength(2);
});
test("does not publish invalid operations", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const result = publishDashboardChanges(store, [
{
type: "remove_item",
target: { kind: "telemetry", id: "missing-metric" },
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected publish failure");
expect(result.errors[0]).toMatchObject({
code: "item_not_found",
operationIndex: 0,
});
expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id);
expect(store.listRevisions()).toHaveLength(1);
});
test("rolls back through the agent-safe revision path", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const publish = publishDashboardChanges(store, exampleOperations(), {
actor: "agent",
});
expect(publish.ok).toBe(true);
const rollback = rollbackDashboardRevision(store, seed.id, {
actor: "agent",
message: "restore previous dashboard",
});
expect(rollback.operation).toBe("rollback");
expect(rollback.actor).toBe("agent");
expect(rollback.sourceRevisionId).toBe(seed.id);
expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console");
expect(store.listRevisions().map((revision) => revision.operation)).toEqual([
"rollback",
"commit",
"seed",
]);
});
test("authenticates agent configuration requests with a shared token", () => {
const authorized = new Request("https://dimensionlab.test/api/agent/dashboard", {
headers: { authorization: "Bearer shared-secret" },
});
const rejected = new Request("https://dimensionlab.test/api/agent/dashboard");
expect(authorizeAgentConfigRequest(authorized, "shared-secret")).toEqual({
ok: true,
});
expect(() => authorizeAgentConfigRequest(rejected, "shared-secret")).toThrow(
AgentConfigAuthorizationError,
);
expect(() => authorizeAgentConfigRequest(authorized, "")).toThrow(
AgentConfigAuthorizationError,
);
});
test("handles preview, publish, and rollback requests through the HTTP adapter", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const preview = await handleAgentDashboardRequest(
jsonRequest({
action: "preview_changes",
operations: exampleOperations(),
}),
{ store, token: "shared-secret" },
);
expect(preview.status).toBe(200);
expect(await preview.json()).toMatchObject({
ok: true,
action: "preview_changes",
patch: expect.any(Array),
});
expect(store.listRevisions()).toHaveLength(1);
const publish = await handleAgentDashboardRequest(
jsonRequest({
action: "publish_changes",
actor: "agent",
message: "publish edge router",
operations: exampleOperations(),
}),
{ store, token: "shared-secret" },
);
expect(publish.status).toBe(200);
expect(await publish.json()).toMatchObject({
ok: true,
action: "publish_changes",
revision: { operation: "commit", actor: "agent" },
});
expect(store.listRevisions()).toHaveLength(2);
const rollback = await handleAgentDashboardRequest(
jsonRequest({
action: "rollback_revision",
actor: "agent",
revisionId: seed.id,
}),
{ store, token: "shared-secret" },
);
expect(rollback.status).toBe(200);
expect(await rollback.json()).toMatchObject({
ok: true,
action: "rollback_revision",
revision: { operation: "rollback", sourceRevisionId: seed.id },
});
});
test("returns a structured error for unknown rollback revisions", async () => {
const { store } = await createTestStore();
store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const rollback = await handleAgentDashboardRequest(
jsonRequest({
action: "rollback_revision",
actor: "agent",
revisionId: "missing-revision",
}),
{ store, token: "shared-secret" },
);
expect(rollback.status).toBe(404);
expect(await rollback.json()).toMatchObject({
ok: false,
errors: [
{
code: "revision_not_found",
path: "/revisionId",
},
],
});
});
test("previews create_dashboard requests against an empty store", async () => {
const { store } = await createTestStore();
const preview = await handleAgentDashboardRequest(
jsonRequest({
action: "preview_changes",
operations: [
{
type: "create_dashboard",
document: genericDashboardFixture,
},
],
}),
{ store, token: "shared-secret" },
);
expect(preview.status).toBe(200);
expect(await preview.json()).toMatchObject({
ok: true,
action: "preview_changes",
document: {
metadata: {
title: "Operations Console",
},
},
});
expect(store.listRevisions()).toHaveLength(0);
});
});
async function createTestStore() {
const root = await mkdtemp(join(tmpdir(), "dimensionlab-agent-config-"));
tempRoots.push(root);
const dbPath = join(root, "dashboard.sqlite");
const store = createDashboardStore({
databaseUrl: `file:${dbPath}`,
});
stores.push(store);
return { dbPath, store };
}
function exampleOperations(): AgentDashboardOperation[] {
return [
{
type: "add_section",
section: {
id: "edge",
title: "Edge",
layout: "list",
},
},
{
type: "add_service",
groupId: "edge",
service: exampleService(),
},
{
type: "add_metric_card",
card: {
id: "edge-latency",
label: "Edge Latency",
value: { kind: "latency", value: 12, precision: 0 },
severity: "ok",
detail: "p95",
datasource: {
type: "external",
adapter: "prometheus",
reference: "histogram_quantile(0.95, edge_request_duration_seconds_bucket)",
},
},
position: { afterId: "service-uptime" },
},
{
type: "connect_datasource",
target: {
kind: "service",
groupId: "edge",
id: "edge-router",
},
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://edge.example.test/api/status",
},
},
{
type: "set_status_rule",
target: { kind: "telemetry", id: "edge-latency" },
thresholds: { warning: 50, danger: 100 },
},
{
type: "arrange_item",
area: "serviceGroups",
id: "edge",
index: 0,
},
];
}
function exampleService() {
return {
id: "edge-router",
label: "Edge Router",
description: "Ingress and routing",
icon: "mdi:router-network",
severity: "ok" as const,
detail: "pending datasource",
datasource: { type: "placeholder" as const, reason: "health adapter pending" },
link: {
href: "https://edge.example.test",
label: "Open Edge Router",
external: true,
},
};
}
function documentWithDuplicateNestedIds(): DashboardDocument {
const document = structuredClone(genericDashboardFixture);
document.layout.serviceGroups.push("secondary-services");
document.serviceGroups.push({
id: "secondary-services",
title: "Secondary Services",
layout: "list",
services: [
{
...document.serviceGroups[0].services[0],
label: "Shadow Identity",
},
],
});
document.layout.statusStrips.push("secondary-runtime");
document.statusStrips.push({
id: "secondary-runtime",
items: [
{
...document.statusStrips[0].items[0],
value: "Healthy",
},
],
});
return document;
}
function applyJsonPatch<T>(value: T, patch: JsonPatchOperation[]): T {
const next = structuredClone(value);
for (const operation of patch) {
const { parent, key } = jsonPointerTarget(next, operation.path);
if (operation.op === "remove") {
if (Array.isArray(parent)) {
parent.splice(Number(key), 1);
} else {
delete parent[key];
}
} else if (operation.op === "add") {
if (Array.isArray(parent)) {
parent.splice(Number(key), 0, operation.value);
} else {
parent[key] = operation.value;
}
} else if (operation.op === "replace") {
if (Array.isArray(parent)) {
parent[Number(key)] = operation.value;
} else {
parent[key] = operation.value;
}
}
}
return next;
}
function jsonPointerTarget(value: unknown, path: string) {
const segments = path
.split("/")
.slice(1)
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
const key = segments.pop();
if (key === undefined) throw new Error(`Invalid JSON pointer: ${path}`);
let parent = value as Record<string, unknown> | unknown[];
for (const segment of segments) {
parent = Array.isArray(parent)
? (parent[Number(segment)] as Record<string, unknown> | unknown[])
: (parent[segment] as Record<string, unknown> | unknown[]);
}
return { parent, key };
}
function jsonRequest(body: unknown) {
return new Request("https://dimensionlab.test/api/agent/dashboard", {
body: JSON.stringify(body),
headers: {
authorization: "Bearer shared-secret",
"content-type": "application/json",
},
method: "POST",
});
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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<Response>((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();

View file

@ -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<DashboardTileResolution> {
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<ServiceGroup[]>;
}
type DatasourceFetch = (input: string, init?: RequestInit) => Promise<Response>;
interface ServiceGroupsSnapshotEntry {
expiresAt: number;
snapshot: Promise<ServiceGroup[]>;
}
const serviceGroupsSnapshotTtlMs = 30_000;
const serviceGroupsSnapshotCache = new Map<string, ServiceGroupsSnapshotEntry>();
const datasourceFetchIdentities = new WeakMap<DatasourceFetch, number>();
let nextDatasourceFetchIdentity = 1;
interface PrometheusVectorResult {
metric?: Record<string, string>;
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<ServiceGroup[]> {
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<StatusItem> {
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}`;
}

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import {
validateDashboardDocument,
type DashboardDocument,
type DashboardValidationFailure,
} from "$lib/model";
} from "@dimensionlab/dashboard-model";
export interface DashboardModelMigrationSuccess {
valid: true;

View file

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

View file

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

View file

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

View file

@ -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<string, string>;
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<string, string>;
};
const webPackage = JSON.parse(
readFileSync(join(root, "apps/web/package.json"), "utf8"),
) as { scripts?: Record<string, string> };
const turboConfig = JSON.parse(
readFileSync(join(root, "turbo.json"), "utf8"),
) as {
globalDependencies?: string[];
tasks?: Record<string, { env?: string[] }>;
};
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<string, { dependsOn?: string[] }>;
};
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<string, { inputs?: string[] }>;
};
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<string, string>; name?: string };
const uiPackage = JSON.parse(
readFileSync(join(root, "packages/ui/package.json"), "utf8"),
) as { exports?: Record<string, unknown>; 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<string, string> };
const modelPackage = JSON.parse(
readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"),
) as { exports?: Record<string, unknown>; 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<string, string[]> } };
const modelPackage = JSON.parse(
readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"),
) as { exports?: Record<string, WorkspacePackageExport> };
const uiPackage = JSON.parse(
readFileSync(join(root, "packages/ui/package.json"), "utf8"),
) as { exports?: Record<string, WorkspacePackageExport> };
const viteConfig = readFileSync(join(root, "apps/web/vite.config.ts"), "utf8");
const turboConfig = JSON.parse(
readFileSync(join(root, "turbo.json"), "utf8"),
) as { tasks?: Record<string, { dependsOn?: string[] }> };
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<string, string> };
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<WorkspacePackageExport, string>,
): void {
expect(actual).toMatchObject(expected);
}
function runDeployScriptWithFakes(
commands: Record<string, string>,
env: Record<string, string>,
): { log: string; status: number | null; stderr: string; stdout: string } {
const tempDir = mkdtempSync(join(tmpdir(), "dimensionlab-deploy-test-"));
const logPath = join(tempDir, "commands.log");
for (const [name, source] of Object.entries(commands)) {
const commandPath = join(tempDir, name);
writeFileSync(commandPath, source);
chmodSync(commandPath, 0o755);
}
const result = spawnSync("bash", [join(root, "scripts/deploy-dimensionlab-website.sh")], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
...env,
DEPLOY_TEST_LOG: logPath,
PATH: `${tempDir}:${process.env.PATH ?? ""}`,
},
});
return {
log: existsSync(logPath) ? readFileSync(logPath, "utf8") : "",
status: result.status,
stderr: result.stderr,
stdout: result.stdout,
};
}
const fakeGitCommand = `#!/usr/bin/env bash
case "$1" in
branch)
echo main
;;
rev-parse)
echo 1234567890abcdef
;;
config|submodule)
exit 0
;;
esac
`;
function fakePodmanCommand(systemdUnit: string): string {
return `#!/usr/bin/env bash
state_file="$DEPLOY_TEST_LOG.state"
[ -f "$state_file" ] || printf 'initial' > "$state_file"
printf '%s\\n' "$*" >> "$DEPLOY_TEST_LOG"
if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then
if [ "$4" = "--format" ]; then
case "$3" in
*:rollback-*)
echo sha256:old
;;
*)
if [ "$(cat "$state_file")" = "rollback" ]; then
echo sha256:old
else
echo sha256:new
fi
;;
esac
fi
exit 0
fi
if [ "$1" = "tag" ] && [ "$2" != "localhost/dimensionlab-website:latest" ]; then
printf 'rollback' > "$state_file"
fi
if [ "$1" = "inspect" ]; then
case "$*" in
*PODMAN_SYSTEMD_UNIT*)
echo ${systemdUnit}
;;
*State.Running*)
echo true
;;
*'.Image'*|*'{{.Image}}'*)
if [ "$(cat "$state_file")" = "rollback" ]; then
echo sha256:old
elif [ "\${DEPLOY_TEST_CONTAINER_IMAGE:-new}" = "wrong" ]; then
echo sha256:wrong
else
echo sha256:new
fi
;;
esac
fi
`;
}
const failingCurlCommand = `#!/usr/bin/env bash
printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
exit 22
`;
const passingCurlCommand = `#!/usr/bin/env bash
printf 'curl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
if [ "$*" = *'/api/dashboard/tiles'* ]; then
printf '{"state":"ready","tiles":[]}'
fi
`;
function fakeSystemctlCommand(options: { active: boolean; show: boolean }): string {
const activeStatus = options.active ? 0 : 3;
const showStatus = options.show ? 0 : 1;
return `#!/usr/bin/env bash
printf 'systemctl %s\\n' "$*" >> "$DEPLOY_TEST_LOG"
if [ "$1" = "--user" ] && [ "$2" = "is-active" ]; then
exit ${activeStatus}
fi
if [ "$1" = "--user" ] && [ "$2" = "show" ]; then
exit ${showStatus}
fi
if [ "$1" = "--user" ] && [ "$2" = "restart" ]; then
exit 0
fi
`;
}

13
apps/web/src/main.tsx Normal file
View file

@ -0,0 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./app.css";
const root = document.getElementById("root");
if (!root) throw new Error("Missing React root element");
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -0,0 +1,97 @@
import { renderToString } from "react-dom/server";
import { describe, expect, test } from "vitest";
import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures";
import { AppStateView, resolveDocumentMetadata } from "./App";
describe("home page model renderer", () => {
test("renders the active dashboard model from the runtime state", () => {
const body = renderToString(
<AppStateView
dashboard={{
state: "ready",
document: genericDashboardFixture,
schemaVersion: "dashboard.v1",
currentRevisionId: "revision-1234567890",
}}
/>,
);
expect(body).toContain("Operations Console");
expect(body).toContain("Service Uptime");
expect(body).toContain("Identity");
expect(body).toContain("data-model-id=\"service-uptime\"");
expect(body).not.toContain("primary");
expect(body).not.toContain("secondary");
});
test("renders invalid model state without crashing", () => {
const body = renderToString(
<AppStateView
dashboard={{
state: "invalid",
title: "Invalid Dashboard",
subtitle: "Validation failed",
message: "Dashboard document is invalid.",
errors: ["/metadata/title is required"],
}}
/>,
);
expect(body).toContain("Invalid Dashboard");
expect(body).toContain("Validation failed");
expect(body).toContain("Dashboard document is invalid.");
expect(body).toContain("/metadata/title is required");
});
test("derives browser metadata from ready and fallback runtime states", () => {
const ready = resolveDocumentMetadata({
state: "ready",
document: genericDashboardFixture,
schemaVersion: "dashboard.v1",
currentRevisionId: "revision-1234567890",
});
const empty = resolveDocumentMetadata({
state: "empty",
title: "No Dashboard Model",
subtitle: "No active document",
message: "No validated dashboard document is active yet.",
});
expect(ready).toEqual({
title: "Operations Console",
description: "Generic environment",
});
expect(empty).toEqual({
title: "No Dashboard Model",
description: "No active document",
});
});
test("renders empty and loading model states without crashing", () => {
const empty = renderToString(
<AppStateView
dashboard={{
state: "empty",
title: "No Dashboard Model",
subtitle: "No active document",
message: "No validated dashboard document is active yet.",
}}
/>,
);
const loading = renderToString(
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
/>,
);
expect(empty).toContain("No Dashboard Model");
expect(empty).toContain("No active document");
expect(loading).toContain("Loading Dashboard");
expect(loading).toContain("Fetching active model");
});
});

View file

@ -0,0 +1,33 @@
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", () => {
const packageJson = JSON.parse(
readFileSync(join(process.cwd(), "package.json"), "utf8"),
) as { scripts?: Record<string, string> };
expect(packageJson.scripts?.dev).toBe("bun src/server/dev.ts");
});
test("proxies dashboard API requests from Vite to the Bun API server", () => {
const server = createDevServerConfig({
DASHBOARD_DEV_API_TARGET: "http://127.0.0.1:5174",
});
expect(server?.proxy?.["/api"]).toMatchObject({
target: "http://127.0.0.1:5174",
changeOrigin: true,
});
});
test("uses development package export conditions for the Bun API server", () => {
expect(apiServerArgs).toEqual([
"--conditions=development",
"src/server/index.ts",
]);
});
});

View file

@ -0,0 +1,76 @@
const webHost = process.env.HOST || "0.0.0.0";
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();
}
export function runDevServers(): void {
const children: Array<ReturnType<typeof Bun.spawn>> = [];
let shuttingDown = false;
function spawn(
label: string,
command: string[],
env: Record<string, string> = {},
): void {
const child = Bun.spawn(command, {
env: {
...process.env,
...env,
},
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
children.push(child);
void child.exited.then((code) => {
if (shuttingDown) return;
console.error(`${label} exited with status ${code}`);
shutdown(code || 1);
});
}
function shutdown(code = 0): void {
if (shuttingDown) return;
shuttingDown = true;
for (const child of children) {
child.kill();
}
void Promise.allSettled(children.map((child) => child.exited)).then(() => {
process.exit(code);
});
}
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));
spawn("api server", [process.execPath, ...apiServerArgs], {
HOST: apiHost,
PORT: apiPort,
});
spawn("vite dev server", [
process.execPath,
"x",
"vite",
"--host",
webHost,
"--port",
webPort,
], {
DASHBOARD_DEV_API_TARGET: apiTarget,
});
console.info(`Dashboard API proxy target: ${apiTarget}`);
}

View file

@ -0,0 +1,77 @@
import { afterAll, describe, expect, test, vi } from "vitest";
import { handleRequest } from "./index";
describe("server request routing", () => {
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
afterAll(() => {
consoleInfo.mockRestore();
});
test("routes dashboard tile batch requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/tiles", {
method: "POST",
body: JSON.stringify({
tiles: [
{
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
],
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
state: "ready",
tiles: [
{
state: "ready",
tile: {
kind: "status",
stripId: "footer-status",
id: "auto-refresh",
},
},
],
});
});
test("rejects non-post dashboard tile batch requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/tiles", {
method: "GET",
}),
);
expect(response.status).toBe(405);
expect(response.headers.get("allow")).toBe("POST");
});
test("routes dashboard event stream requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/events", {
method: "GET",
}),
);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe(
"text/event-stream; charset=utf-8",
);
});
test("rejects non-get dashboard event stream requests", async () => {
const response = await handleRequest(
new Request("https://example.test/api/dashboard/events", {
method: "POST",
}),
);
expect(response.status).toBe(405);
expect(response.headers.get("allow")).toBe("GET");
});
});

View file

@ -0,0 +1,104 @@
import { extname, normalize } from "node:path";
import { handleAgentDashboardRoute } from "./routes/agent-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);
const distRoot = `${process.cwd()}/dist`;
const contentTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
[".js", "text/javascript; charset=utf-8"],
[".json", "application/json; charset=utf-8"],
[".svg", "image/svg+xml"],
[".wasm", "application/wasm"],
]);
export async function handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/dashboard") {
if (request.method !== "GET") return methodNotAllowed(["GET"]);
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);
}
if (url.pathname.startsWith("/api/")) {
return Response.json({ ok: false, message: "Not found" }, { status: 404 });
}
return serveStaticAsset(url.pathname);
}
async function serveStaticAsset(pathname: string): Promise<Response> {
const safePath = normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, "");
const assetPath = safePath === "/" || safePath === "." ? "/index.html" : safePath;
const file = Bun.file(`${distRoot}${assetPath}`);
if (await file.exists()) {
return new Response(file, {
headers: contentTypeHeaders(assetPath),
});
}
const fallback = Bun.file(`${distRoot}/index.html`);
if (await fallback.exists()) {
return new Response(fallback, {
headers: contentTypeHeaders(".html"),
});
}
return new Response("Build output not found", { status: 404 });
}
function methodNotAllowed(allowedMethods: string[]): Response {
return Response.json(
{ ok: false, message: "Method not allowed" },
{
status: 405,
headers: {
Allow: allowedMethods.join(", "),
},
},
);
}
function contentTypeHeaders(pathname: string): HeadersInit {
const contentType = contentTypes.get(extname(pathname));
return contentType ? { "Content-Type": contentType } : {};
}
if (import.meta.main) {
Bun.serve({
hostname: host,
port,
fetch: handleRequest,
});
console.info(`Dimension Lab website listening on http://${host}:${port}`);
}

View file

@ -0,0 +1,12 @@
import { describe, expect, test } from "vitest";
import { handleAgentDashboardRoute } from "./agent-dashboard";
describe("agent dashboard API route", () => {
test("delegates unauthorized requests to the existing agent handler", async () => {
const response = await handleAgentDashboardRoute(
new Request("http://localhost/api/agent/dashboard", { method: "POST" }),
);
expect(response.status).toBe(401);
});
});

View file

@ -0,0 +1,5 @@
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
export function handleAgentDashboardRoute(request: Request): Promise<Response> {
return handleAgentDashboardRequest(request);
}

View file

@ -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<Response>((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<never>((_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<string, () => void>();
const service = dimensionLabDashboardFixture.serviceGroups
.flatMap((group) => group.services)[0];
if (!service) throw new Error("missing service fixture");
const tiles = Array.from({ length: 7 }, (_, index) => ({
kind: "service" as const,
groupId: "essentials",
id: `service-${index}`,
}));
const batch = loadDashboardTilesResponse(tiles, {
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: {
async resolve(key) {
active += 1;
maxActive = Math.max(maxActive, active);
started.push(key);
await new Promise<void>((resolve) => releases.set(key, resolve));
active -= 1;
return {
cache: "miss",
coalesced: false,
response: {
state: "ready",
tile: JSON.parse(key),
item: service,
},
};
},
},
});
await waitFor(() => started.length === 6);
expect(maxActive).toBe(6);
releases.get(started[0])?.();
await waitFor(() => started.length === 7);
for (const release of releases.values()) release();
await expect(batch).resolves.toMatchObject({
state: "ready",
tiles: expect.arrayContaining([
expect.objectContaining({
state: "ready",
tile: tiles[0],
}),
]),
});
expect(maxActive).toBe(6);
});
test("does not hydrate tile routes when live datasources are disabled", async () => {
const previous = process.env.DISABLE_LIVE_DATASOURCES;
process.env.DISABLE_LIVE_DATASOURCES = "1";
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}`);
});
}

View file

@ -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<DashboardTileResolution>,
): Promise<DashboardTileCacheResult>;
}
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<string, DashboardTileCacheEntry>();
const inFlight = new Map<string, Promise<DashboardTileResolution>>();
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<DashboardRuntimeState> {
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<DashboardTileResolution> {
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<DashboardTilesBatchResponse> {
return {
state: "ready",
tiles: await resolveDashboardTilesBatch(tiles, options),
};
}
export async function handleDashboardRoute(): Promise<Response> {
return Response.json(await loadDashboardResponse());
}
export async function handleDashboardTileRoute(
pathname: string,
options: LoadDashboardResponseOptions = {},
): Promise<Response> {
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<Response> {
const tiles = await parseDashboardTilesBatchRequest(request);
if (!tiles) {
return Response.json(
{ ok: false, message: "Invalid dashboard tiles request" },
{ status: 400 },
);
}
return Response.json(await loadDashboardTilesResponse(tiles, options), {
headers: dashboardTileResponseHeaders,
});
}
export async function handleDashboardEventsRoute(
options: DashboardEventsRouteOptions = {},
): Promise<Response> {
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const encoder = new TextEncoder();
const dashboard = await loadDashboardResponse({
...options,
hydrateLiveDatasources: false,
});
if (dashboard.state !== "ready" || options.signal?.aborted) {
controller.close();
return;
}
try {
for (const tile of dashboardEventTiles(dashboard.document)) {
if (options.signal?.aborted) break;
const resolution = await loadDashboardTileResponse(tile, options);
if (resolution.state === "ready") {
controller.enqueue(
encoder.encode(dashboardTileEventChunk(resolution)),
);
}
}
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "text/event-stream; charset=utf-8",
"X-Accel-Buffering": "no",
},
});
}
export function dashboardTileCacheKey(tile: DashboardTileReference): string {
return JSON.stringify(tile);
}
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<DashboardTileResolution[]> {
const results: DashboardTileResolution[] = new Array(tiles.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < tiles.length) {
const index = nextIndex;
nextIndex += 1;
results[index] = await loadDashboardTileResponse(tiles[index], options);
}
}
await Promise.all(
Array.from(
{ length: Math.min(dashboardBatchTileConcurrency, tiles.length) },
() => worker(),
),
);
return results;
}
async function parseDashboardTilesBatchRequest(
request: Request,
): Promise<DashboardTileReference[] | null> {
try {
const body = await request.json() as unknown;
if (
typeof body !== "object" ||
body === null ||
!("tiles" in body) ||
!Array.isArray(body.tiles)
) {
return null;
}
const tiles = body.tiles.map(parseDashboardTileReference);
return tiles.every((tile): tile is DashboardTileReference => tile !== null)
? tiles
: null;
} catch {
return null;
}
}
function parseDashboardTileReference(value: unknown): DashboardTileReference | null {
if (typeof value !== "object" || value === null || !("kind" in value)) {
return null;
}
if (
(value.kind === "telemetry" || value.kind === "module") &&
"id" in value &&
typeof value.id === "string"
) {
return { kind: value.kind, id: value.id };
}
if (
value.kind === "service" &&
"groupId" in value &&
typeof value.groupId === "string" &&
"id" in value &&
typeof value.id === "string"
) {
return {
kind: "service",
groupId: value.groupId,
id: value.id,
};
}
if (
value.kind === "status" &&
"stripId" in value &&
typeof value.stripId === "string" &&
"id" in value &&
typeof value.id === "string"
) {
return {
kind: "status",
stripId: value.stripId,
id: value.id,
};
}
return null;
}
function dashboardEventTiles(document: DashboardDocument): DashboardTileReference[] {
const status = document.statusStrips.flatMap((strip) =>
strip.items.map((item): DashboardTileReference => ({
kind: "status",
stripId: strip.id,
id: item.id,
})),
);
const telemetry = document.telemetry
.filter((card) => card.datasource?.type === "external")
.map((card): DashboardTileReference => ({ kind: "telemetry", id: card.id }));
const modules = (document.modules || [])
.filter((module) =>
module.datasource?.type === "external" ||
module.id === "runtime-health-summary"
)
.map((module): DashboardTileReference => ({ kind: "module", id: module.id }));
const services = document.serviceGroups.flatMap((group) =>
group.services
.filter((service) => service.datasource?.type === "external")
.map((service): DashboardTileReference => ({
kind: "service",
groupId: group.id,
id: service.id,
})),
);
return [...status, ...telemetry, ...modules, ...services];
}
function dashboardTileEventChunk(resolution: DashboardTileResolution): string {
return [
"event: dashboard-tile",
`data: ${JSON.stringify(resolution)}`,
"",
"",
].join("\n");
}
class DashboardTileCacheResolutionError extends Error {
readonly cache: "hit" | "miss";
readonly coalesced: boolean;
override readonly cause: unknown;
constructor(
cause: unknown,
metadata: Pick<DashboardTileCacheResult, "cache" | "coalesced">,
) {
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;
}

3
apps/web/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare module "*.css" {}

View file

@ -0,0 +1,381 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test, type Page } from "@playwright/test";
const linkedServiceIds = [
"vaultwarden",
"forgejo",
"wiki",
"aws-start",
"adguard-primary",
"adguard-secondary",
"grafana",
"uptime-kuma",
"prometheus",
"backrest",
"n8n",
"open-webui",
"comfyui",
"models",
"adminer",
"assistant",
"suna",
"cockpit-infra",
"cockpit-gpu",
"cockpit-network-core",
"forgejo-ssh-relay",
];
test.describe("dashboard page QA gate", () => {
test("renders the model-driven dashboard on desktop", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop");
await page.goto("/");
await waitForDashboardReady(page);
await expect(page.getByRole("main")).toHaveAttribute(
"aria-labelledby",
/title/,
);
await expect(
page.getByRole("heading", { level: 1, name: "System Overview" }),
).toBeVisible();
await expect(page.getByText("Infra RAM")).toBeVisible();
await expect(page.getByRole("link", { name: /Vaultwarden/i })).toBeVisible();
await expect(page.locator("[data-model-id='vaultwarden']")).toBeVisible();
await expect(page.locator(".service-row .icon-glyph svg").first()).toBeVisible();
const bodyBox = await page.locator("body").boundingBox();
expect(bodyBox?.width).toBeGreaterThan(1000);
await expect(page).toHaveScreenshot("dashboard-desktop.png", {
fullPage: true,
});
});
test("fits the operational dashboard into a 1470 by 956 viewport", async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop");
await page.setViewportSize({ width: 1470, height: 956 });
await page.goto("/");
await waitForDashboardReady(page);
const metrics = await page.evaluate(() => {
const footer = document.querySelector("[data-model-id='footer-status']");
const runtime = document.querySelector("[data-model-id='runtime-health']");
const visibleItems = [
...document.querySelectorAll(".telemetry-card"),
...document.querySelectorAll(".service-row"),
...document.querySelectorAll(".footer-cell"),
].map((element) => {
const rect = element.getBoundingClientRect();
return {
bottom: rect.bottom,
height: rect.height,
id: element.getAttribute("data-model-id"),
top: rect.top,
width: rect.width,
};
});
return {
clippedItems: visibleItems.filter((item) =>
item.top < 0 ||
item.bottom > window.innerHeight ||
item.width <= 0 ||
item.height <= 0
),
footerCellCount: document.querySelectorAll(".footer-cell").length,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
serviceRowCount: document.querySelectorAll(".service-row").length,
telemetryCardCount: document.querySelectorAll(".telemetry-card").length,
footerBottom: footer?.getBoundingClientRect().bottom ?? 0,
runtimeBottom: runtime?.getBoundingClientRect().bottom ?? 0,
};
});
expect(metrics.scrollWidth).toBeLessThanOrEqual(1470);
expect(metrics.scrollHeight).toBeLessThanOrEqual(956);
expect(metrics.runtimeBottom).toBeLessThanOrEqual(956);
expect(metrics.footerBottom).toBeLessThanOrEqual(956);
expect(metrics.telemetryCardCount).toBe(16);
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");
await page.goto("/");
await waitForDashboardReady(page);
const bookmarkDensity = await page.evaluate(() => {
const panelBody = document.querySelector(".service-panel .panel__body");
const rows = Array.from(document.querySelectorAll<HTMLElement>(".service-panel .service-row"));
const first = rows[0];
const second = rows[1];
if (!panelBody || !first || !second) {
throw new Error("expected at least two bookmark rows");
}
const bodyStyle = window.getComputedStyle(panelBody);
const rowStyle = window.getComputedStyle(first);
const firstRect = first.getBoundingClientRect();
const secondRect = second.getBoundingClientRect();
return {
backgroundColor: rowStyle.backgroundColor,
borderBottomWidth: Number.parseFloat(rowStyle.borderBottomWidth),
borderLeftWidth: Number.parseFloat(rowStyle.borderLeftWidth),
borderRightWidth: Number.parseFloat(rowStyle.borderRightWidth),
columnGap: Number.parseFloat(rowStyle.columnGap),
paddingBottom: Number.parseFloat(rowStyle.paddingBottom),
paddingTop: Number.parseFloat(rowStyle.paddingTop),
panelGap: Number.parseFloat(bodyStyle.rowGap),
rowGap: secondRect.top - firstRect.bottom,
};
});
expect(bookmarkDensity.panelGap).toBe(0);
expect(bookmarkDensity.rowGap).toBeLessThanOrEqual(1);
expect(bookmarkDensity.paddingTop).toBeLessThanOrEqual(3);
expect(bookmarkDensity.paddingBottom).toBeLessThanOrEqual(3);
expect(bookmarkDensity.columnGap).toBeLessThanOrEqual(4);
expect(bookmarkDensity.borderBottomWidth).toBeGreaterThanOrEqual(1);
expect(bookmarkDensity.borderLeftWidth).toBe(0);
expect(bookmarkDensity.borderRightWidth).toBe(0);
expect(bookmarkDensity.backgroundColor).toBe("rgba(0, 0, 0, 0)");
});
test("balances dashboard typography at the target viewport", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop");
await page.setViewportSize({ width: 1470, height: 956 });
await page.goto("/");
await waitForDashboardReady(page);
const typeScale = await page.evaluate(() => {
const fontSize = (selector: string) => {
const element = document.querySelector(selector);
if (!element) throw new Error(`missing ${selector}`);
return Number.parseFloat(window.getComputedStyle(element).fontSize);
};
return {
h1: fontSize("h1"),
panelTitle: fontSize(".service-panel h2"),
serviceDescription: fontSize(".service-row p"),
serviceLabel: fontSize(".service-row h3"),
telemetryLabel: fontSize(".telemetry-card h3"),
telemetryValue: fontSize(".telemetry-card strong"),
};
});
expect(typeScale.serviceLabel).toBeGreaterThanOrEqual(11.4);
expect(typeScale.serviceDescription).toBeGreaterThanOrEqual(8.8);
expect(typeScale.h1).toBeLessThanOrEqual(49);
expect(typeScale.panelTitle).toBeLessThanOrEqual(28);
expect(typeScale.telemetryLabel).toBeLessThanOrEqual(9.5);
expect(typeScale.telemetryValue).toBeLessThanOrEqual(36);
});
test("keeps the first screen usable on mobile", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-mobile");
await page.goto("/");
await waitForDashboardReady(page);
await expect(
page.getByRole("heading", { level: 1, name: "System Overview" }),
).toBeVisible();
await expect(page.getByLabel("Service groups")).toBeVisible();
await expect(page.locator(".service-row .icon-glyph svg").first()).toBeVisible();
await expect(page).toHaveScreenshot("dashboard-mobile.png", {
fullPage: true,
});
});
test("exposes usable landmarks and a visible keyboard focus state", async ({
page,
}) => {
await page.goto("/");
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");
const focused = page.locator(":focus");
await expect(focused).toHaveAttribute("data-model-id", serviceId);
const focusBoxShadow = await focused.evaluate((element) => {
return window.getComputedStyle(element).boxShadow;
});
expect(focusBoxShadow).not.toBe("none");
}
});
test("passes automated accessibility checks", async ({ page }) => {
await page.goto("/");
await waitForDashboardReady(page);
const results = await new AxeBuilder({ page }).analyze();
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" });
await page.goto("/");
await waitForDashboardReady(page);
const durations = await page.evaluate(() => {
const element = document.createElement("div");
element.style.animation = "qa-motion-check 10s infinite";
element.style.transition = "opacity 10s linear";
document.body.append(element);
const styles = window.getComputedStyle(element);
return {
animation: styles.animationDuration,
transition: styles.transitionDuration,
};
});
expect(cssDurationToMilliseconds(durations.animation)).toBeLessThanOrEqual(
0.01,
);
expect(cssDurationToMilliseconds(durations.transition)).toBeLessThanOrEqual(
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<void> {
await expect(
page.getByRole("heading", { level: 1, name: "System Overview" }),
).toBeVisible();
await expect(page.locator("[data-model-id='vaultwarden']")).toBeVisible();
}
function cssDurationToMilliseconds(duration: string): number {
if (duration.endsWith("ms")) return Number.parseFloat(duration);
if (duration.endsWith("s")) return Number.parseFloat(duration) * 1000;
return Number.NaN;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

View file

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

View file

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

19
apps/web/tsconfig.json Normal file
View file

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

34
apps/web/vite.config.ts Normal file
View file

@ -0,0 +1,34 @@
import { fileURLToPath } from "node:url";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig, configDefaults } from "vitest/config";
import type { UserConfig } from "vite";
export function createDevServerConfig(
env: NodeJS.ProcessEnv = process.env,
): UserConfig["server"] {
const apiTarget = env.DASHBOARD_DEV_API_TARGET;
if (!apiTarget) return undefined;
return {
proxy: {
"/api": {
target: apiTarget,
changeOrigin: true,
},
},
};
}
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
$lib: fileURLToPath(new URL("./src/lib", import.meta.url)),
},
},
server: createDevServerConfig(),
test: {
exclude: [...configDefaults.exclude, "tests/e2e/**"],
},
});

1120
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,592 @@
# React Runtime Migration 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:** Replace the SvelteKit dashboard app with a React-based Vite app and Bun production server while preserving model-driven dashboard behavior and reusable presentation boundaries.
**Architecture:** The browser runtime becomes React mounted from `src/main.tsx`. A Bun server at `src/server/index.ts` serves the static React build and exposes JSON API routes for dashboard reads and agent dashboard mutations. Existing model, persistence, datasource, and agent-config modules remain framework-agnostic TypeScript with import path updates as needed.
**Tech Stack:** Bun, Vite, React, React DOM, TypeScript, Tailwind CSS v4, shadcn/ui, Vitest, Storybook React Vite, Playwright, Drizzle ORM, Bun SQLite, MSW, uPlot, Iconify React.
---
## File Structure
- Create `index.html`: Vite app shell with `<div id="root"></div>` and `/src/main.tsx`.
- Create `src/main.tsx`: React DOM bootstrap and global CSS import.
- Create `src/App.tsx`: Dashboard fetch, refresh interval, ready/empty/loading/invalid rendering.
- Create `src/App.test.tsx`: React server-rendering and hook behavior tests for dashboard states.
- Create `src/server/index.ts`: Bun HTTP server, static asset serving, API router, production entry.
- Create `src/server/routes/dashboard.ts`: `GET /api/dashboard` runtime loader and datasource resolver.
- Create `src/server/routes/agent-dashboard.ts`: `POST /api/agent/dashboard` adapter for `handleAgentDashboardRequest`.
- Create `src/server/routes/dashboard.test.ts`: dashboard API state and datasource-disable tests.
- Create `src/server/routes/agent-dashboard.test.ts`: agent API delegation/auth tests.
- Create `src/lib/ui/components/*.tsx`: React ports of current reusable Svelte components.
- Create `src/lib/ui/components/styles.css`: component CSS migrated from Svelte style blocks.
- Create `components.json`: shadcn/ui configuration for Vite, Radix, Nova, Tailwind v4, and `$lib` aliases.
- Create `src/lib/components/ui/*.tsx`: selected shadcn primitives, not the full registry.
- Create `src/lib/utils.ts`: `cn()` helper for shadcn and dashboard components.
- Replace `src/lib/ui/components/render.test.ts`: React `renderToString` component tests.
- Replace `src/lib/ui/stories/*.stories.svelte`: React `.stories.tsx` stories.
- Modify `.storybook/main.ts`: use `@storybook/react-vite` and React story globs.
- Modify `.storybook/preview.ts`: use React Storybook types and keep MSW setup/global CSS.
- Modify `vite.config.ts`: use React and Tailwind plugins, alias `$lib` to `src/lib`, build client, and keep Vitest config.
- Modify `tsconfig.json`: remove `.svelte-kit` inheritance, enable JSX, and define path aliases.
- Modify `package.json`: swap Svelte/SvelteKit dependencies for React/Tailwind/shadcn tooling and update scripts.
- Modify `playwright.config.ts`: build React client and Bun server before e2e.
- Modify `Containerfile`: copy React/Bun build artifacts and keep `bun build/index.js` command.
- Modify `README.md`: document React runtime, Bun server, scripts, QA gate, deployment.
- Remove `svelte.config.js`, `src/app.html`, `src/routes/**`, and all `.svelte` files after replacements pass.
## Task 1: React Toolchain And Typecheck Scaffold
**Files:**
- Modify: `package.json`
- Modify: `bun.lock`
- Modify: `tsconfig.json`
- Modify: `vite.config.ts`
- Create: `index.html`
- Create: `src/main.tsx`
- Create: `src/App.tsx`
- Create: `src/App.test.tsx`
- [ ] **Step 1: Write the failing React scaffold test**
Create `src/App.test.tsx`:
```tsx
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(
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
/>,
);
expect(html).toContain("Loading Dashboard");
expect(html).toContain("Fetching active model");
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun run test:unit src/App.test.tsx`
Expected: FAIL because React dependencies and `src/App.tsx` do not exist.
- [ ] **Step 3: Add React dependencies and scaffold files**
Update `package.json` scripts and dependencies:
```json
{
"scripts": {
"dev": "vite --host 0.0.0.0",
"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"
}
}
```
Install React, Tailwind, and shadcn packages with Bun so `bun.lock` updates:
```sh
bun add @iconify/react @vitejs/plugin-react react react-dom
bun add class-variance-authority clsx lucide-react radix-ui tailwind-merge tw-animate-css @fontsource-variable/geist
bun add -d @storybook/react-vite @tailwindcss/vite @types/react @types/react-dom shadcn tailwindcss
```
Create `index.html`:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dimension Lab</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
Create minimal `src/App.tsx`:
```tsx
import type { DashboardRuntimeState } from "$lib/server/dashboard";
export function AppStateView({ dashboard }: { dashboard: DashboardRuntimeState }) {
if (dashboard.state === "ready") {
return <main>{dashboard.document.metadata.title}</main>;
}
return (
<main className="state-shell" data-dashboard-state={dashboard.state}>
<h1>{dashboard.title}</h1>
<p>{dashboard.subtitle}</p>
<p>{dashboard.message}</p>
</main>
);
}
export default function App() {
return (
<AppStateView
dashboard={{
state: "loading",
title: "Loading Dashboard",
subtitle: "Fetching active model",
message: "Waiting for the active dashboard document.",
}}
/>
);
}
```
Create `src/main.tsx`:
```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./app.css";
const root = document.getElementById("root");
if (!root) throw new Error("Missing React root element");
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);
```
Update `tsconfig.json` with React JSX and path aliases.
Update `vite.config.ts` to use `@vitejs/plugin-react`, `@tailwindcss/vite`,
and the `$lib` alias. Initialize shadcn with:
```sh
bunx --bun shadcn@latest init --template vite --base radix --preset nova --yes --css-variables
bunx --bun shadcn@latest add badge card progress separator skeleton alert
```
Do not run `bunx --bun shadcn@latest add --all`; the dashboard should only
vendor primitives it actually uses.
- [ ] **Step 4: Run checks for the scaffold**
Run: `bun run test:unit src/App.test.tsx && bun run check`
Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git add package.json bun.lock tsconfig.json vite.config.ts index.html src/main.tsx src/App.tsx src/App.test.tsx
git commit -m "build: add react vite scaffold"
```
## Task 2: Bun Server And Dashboard API
**Files:**
- Create: `src/server/index.ts`
- Create: `src/server/routes/dashboard.ts`
- Create: `src/server/routes/dashboard.test.ts`
- Create: `src/server/routes/agent-dashboard.ts`
- Create: `src/server/routes/agent-dashboard.test.ts`
- Modify: `src/App.tsx`
- Modify: `playwright.config.ts`
- [ ] **Step 1: Write failing API route tests**
Create `src/server/routes/dashboard.test.ts`:
```ts
import { describe, expect, test } from "vitest";
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).toContain("Dimension Lab");
});
});
```
Create `src/server/routes/agent-dashboard.test.ts`:
```ts
import { describe, expect, test } from "vitest";
import { handleAgentDashboardRoute } from "./agent-dashboard";
describe("agent dashboard API route", () => {
test("delegates unauthorized requests to the existing agent handler", async () => {
const response = await handleAgentDashboardRoute(
new Request("http://localhost/api/agent/dashboard", { method: "POST" }),
);
expect(response.status).toBe(401);
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `bun run test:unit src/server/routes/dashboard.test.ts src/server/routes/agent-dashboard.test.ts`
Expected: FAIL because the route modules do not exist.
- [ ] **Step 3: Implement server route modules and server entry**
Implement `loadDashboardResponse()` by calling `loadDashboardRuntime()` and
`resolveDashboardDatasources()` exactly like the current SvelteKit load
function. Implement `handleAgentDashboardRoute()` by returning
`handleAgentDashboardRequest(request)`. Implement `src/server/index.ts` with
Bun.serve routes for `/api/dashboard`, `/api/agent/dashboard`, static Vite
assets, and SPA fallback to `index.html`.
- [ ] **Step 4: Update React app to fetch `/api/dashboard`**
`src/App.tsx` should export `AppStateView` for tests and make the default
`App` fetch dashboard state with `useEffect`. It should clear refresh timers
when state changes and on unmount.
- [ ] **Step 5: Run route and app tests**
Run:
```sh
bun run test:unit src/server/routes/dashboard.test.ts src/server/routes/agent-dashboard.test.ts src/App.test.tsx
```
Expected: PASS.
- [ ] **Step 6: Commit**
```sh
git add src/server src/App.tsx src/App.test.tsx playwright.config.ts
git commit -m "feat(server): add bun dashboard api"
```
## Task 3: React UI Component Library
**Files:**
- Create: `src/lib/ui/components/*.tsx`
- Create: `src/lib/ui/components/styles.css`
- Modify: `src/lib/ui/components/render.test.ts`
- Modify: `src/lib/ui/index.ts`
- Keep: `src/lib/ui/types.ts`
- Keep: `src/lib/ui/model-renderer.ts`
- Keep: `src/lib/ui/fixtures.ts`
- [ ] **Step 1: Replace Svelte SSR tests with failing React render tests**
Rewrite `src/lib/ui/components/render.test.ts` to import React components and
`renderToString` from `react-dom/server`. Keep the current assertions for:
- dashboard fixture content
- generic secondary fixture content
- optional service/status links
- stable model IDs
- progress bar behavior
- uPlot chart surface marker
- native attributes on Button and IconButton
Run: `bun run test:unit src/lib/ui/components/render.test.ts`
Expected: FAIL because React component files do not exist yet.
- [ ] **Step 2: Port atomic components**
Create React equivalents for `Badge`, `Button`, `IconGlyph`, `IconButton`,
`ProgressMeter`, `Separator`, `Sparkline`, `SignalTrace`, `LineChart`, and
`StatusBadge`. Preserve class names and `data-*` attributes from Svelte.
Run: `bun run test:unit src/lib/ui/components/render.test.ts`
Expected: remaining FAILs only for dashboard composite components.
- [ ] **Step 3: Port layout and card components**
Create React equivalents for `Panel`, `CornerBracketFrame`, `GridFrame`,
`DiagonalStripeField`, `ModuleCard`, `TelemetryCard`, `TelemetryGrid`,
`FooterCell`, `FooterStatusCell`, and `StatusStrip`.
Run: `bun run test:unit src/lib/ui/components/render.test.ts`
Expected: remaining FAILs only for service/dashboard shell components.
- [ ] **Step 4: Port service and dashboard shell components**
Create React equivalents for `ServiceRow`, `ServicePanel`,
`ServiceGroupPanel`, `SystemState`, `DashboardHeader`, `DashboardFrame`,
`TelemetryStrip`, and `WeatherModule`.
Run: `bun run test:unit src/lib/ui/components/render.test.ts`
Expected: PASS.
- [ ] **Step 5: Export React components**
Update `src/lib/ui/index.ts` to export `.tsx` React components and continue
exporting fixtures, renderer, and UI types.
Run:
```sh
bun run test:unit src/lib/ui/components/render.test.ts src/lib/ui/model-renderer.test.ts src/lib/ui/content-boundary.test.ts
```
Expected: PASS.
- [ ] **Step 6: Commit**
```sh
git add src/lib/ui/components src/lib/ui/index.ts
git commit -m "feat(ui): port dashboard components to react"
```
## Task 4: React Dashboard App Rendering
**Files:**
- Modify: `src/App.tsx`
- Modify: `src/App.test.tsx`
- Modify: `src/app.css`
- Delete after `src/page.test.tsx` passes: `src/routes/page.test.ts`
- Create: `src/page.test.tsx`
- [ ] **Step 1: Write failing React page tests**
Create `src/page.test.tsx` with React `renderToString` assertions equivalent to
the current Svelte `src/routes/page.test.ts`:
- ready dashboard renders model content
- invalid model state renders validation errors
- empty and loading states render without crashing
Run: `bun run test:unit src/page.test.tsx`
Expected: FAIL until `AppStateView` uses the React `DashboardFrame` and
`SystemState` components.
- [ ] **Step 2: Implement app state rendering**
Use `dashboardDocumentToUiDashboard()` and `DashboardFrame` for ready state.
Use `SystemState` for empty/loading/invalid states. Preserve state shell CSS
and validation error list markup.
- [ ] **Step 3: Run page tests**
Run: `bun run test:unit src/page.test.tsx src/App.test.tsx`
Expected: PASS.
- [ ] **Step 4: Commit**
```sh
git add src/App.tsx src/App.test.tsx src/page.test.tsx src/app.css
git commit -m "feat(app): render dashboard with react"
```
## Task 5: React Storybook
**Files:**
- Modify: `.storybook/main.ts`
- Modify: `.storybook/preview.ts`
- Create: `src/lib/ui/stories/*.stories.tsx`
- Delete after replacement: `src/lib/ui/stories/*.stories.svelte`
- Delete after replacement: `src/lib/ui/stories/FocusPreview.svelte`
- Modify: `src/lib/ui/storybook.test.ts`
- [ ] **Step 1: Update storybook boundary test first**
Change `src/lib/ui/storybook.test.ts` so it requires React `.stories.tsx`
files and rejects `.stories.svelte` files.
Run: `bun run test:unit src/lib/ui/storybook.test.ts`
Expected: FAIL while Svelte stories still exist.
- [ ] **Step 2: Configure React Storybook**
Update `.storybook/main.ts` to use `@storybook/react-vite`, React story globs,
and the existing addons. Update `.storybook/preview.ts` type imports to React
Storybook while preserving global CSS, MSW setup, backgrounds, controls, and
fullscreen layout.
- [ ] **Step 3: Port stories to React**
Create `.stories.tsx` files for each existing Svelte story. Import React
components from `src/lib/ui` and generic story data from
`src/lib/ui/stories/story-data.ts`.
- [ ] **Step 4: Remove Svelte stories and run Storybook checks**
Run:
```sh
bun run test:unit src/lib/ui/storybook.test.ts
bun run build-storybook
```
Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git add .storybook src/lib/ui/stories src/lib/ui/storybook.test.ts
git commit -m "feat(storybook): migrate stories to react"
```
## Task 6: Remove SvelteKit Runtime
**Files:**
- Delete: `svelte.config.js`
- Delete: `src/app.html`
- Delete: `src/routes/**`
- Delete: all remaining `*.svelte`
- Modify: `package.json`
- Modify: `bun.lock`
- Modify: `README.md`
- Modify: `Containerfile`
- Modify: `playwright.config.ts`
- Modify: `src/lib/presentation-boundary.test.ts`
- [ ] **Step 1: Write/adjust cleanup tests**
Add assertions to presentation or storybook boundary tests that no `.svelte`
files remain under `src/`.
Run: `bun run test:unit src/lib/presentation-boundary.test.ts src/lib/ui/storybook.test.ts`
Expected: FAIL while Svelte files remain.
- [ ] **Step 2: Delete Svelte runtime and dependencies**
Remove all Svelte files and Svelte dependencies. Run `bun install
--frozen-lockfile` only after `package.json` and `bun.lock` are consistent, or
run `bun remove` commands to update both together:
```sh
bun remove @iconify/svelte @storybook/addon-svelte-csf @storybook/sveltekit @sveltejs/adapter-node @sveltejs/kit @sveltejs/vite-plugin-svelte svelte svelte-check
```
- [ ] **Step 3: Update docs, container, and e2e build command**
README should describe React, Vite, Bun server, and unchanged persistence.
`Containerfile` should copy the Vite client output and Bun server output.
`playwright.config.ts` should build and start the Bun server.
- [ ] **Step 4: Run cleanup checks**
Run:
```sh
rg -n "\\.svelte|svelte" package.json src .storybook vite.config.ts tsconfig.json README.md Containerfile
bun run check
bun run test:unit
```
Expected: `rg` finds no Svelte app/runtime references except historical docs in
the committed design/plan, and checks pass.
- [ ] **Step 5: Commit**
```sh
git add -A
git commit -m "refactor: remove svelte runtime"
```
## Task 7: QA Gate, PR, Review, And Merge
**Files:**
- Modify only files needed to fix failures found by this task.
- [ ] **Step 1: Run full QA gate**
Run: `bun run test:qa`
Expected: PASS for check, unit tests, production build, Storybook build, and
Playwright desktop/mobile tests.
- [ ] **Step 2: Inspect current diff**
Run:
```sh
git status --short
git diff --stat main...HEAD
git diff --name-only main...HEAD
```
Expected: only React migration files and docs changed.
- [ ] **Step 3: Push and open ready PR**
Run:
```sh
git push -u origin codex/react-migration
```
Open a ready PR against `main` with title:
```text
refactor: migrate dashboard runtime to react
```
- [ ] **Step 4: Independent review**
Dispatch an independent reviewer to inspect the issue goal, spec, plan, and PR
diff in code-review mode. Blocking findings must be fixed on the same branch.
- [ ] **Step 5: Fix review findings and re-run QA**
For each blocking finding, write or update the relevant failing test first,
make the minimal fix, and run the focused test plus `bun run test:qa`.
- [ ] **Step 6: Merge only after green checks and no blocking review findings**
Merge the PR into `main`, sync the worktree back to `main`, and mark the goal
complete only after the completion criteria in the spec are proven by current
state.
## Plan Self-Review
- Spec coverage: Tasks cover React scaffold, Bun API server, UI component
migration, app rendering, Storybook migration, Svelte removal, QA, PR,
independent review, and merge.
- Red-flag scan: The plan has no incomplete-work markers and no unspecified
acceptance gates.
- Type consistency: Public names used across tasks are `AppStateView`,
`loadDashboardResponse`, and `handleAgentDashboardRoute`.

View file

@ -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<string, string>;
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<string, string>; name?: string };
const uiPackage = JSON.parse(
readFileSync(join(root, "packages/ui/package.json"), "utf8"),
) as { exports?: Record<string, unknown>; 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`.

View file

@ -0,0 +1,177 @@
# React Runtime Migration Design
## Context
The current Dimension Lab website is a SvelteKit application. It uses Bun,
Vite, Svelte 5, SvelteKit server routes, Svelte Storybook stories, and
Svelte SSR component tests. The app has one dashboard route, one agent
configuration API route, reusable Svelte UI components, typed dashboard model
data, Drizzle-backed SQLite persistence, datasource resolution, Playwright
desktop/mobile checks, and a Bun-based container runtime.
The migration goal is to make the project React-based so dashboard UI
components can be reused outside the current app. A partial React island inside
SvelteKit would leave the app split across two component systems, so the target
state is a React runtime and React component library.
## Target Architecture
Use Vite React for the browser app and a small Bun HTTP server for production
runtime. This keeps the existing Bun SQLite persistence model and avoids adding
a heavier React framework where the current route/API surface is small. Use
Tailwind CSS v4 and shadcn/ui as the reusable primitive layer for React
components, but do not vendor the full shadcn registry. Add only primitives
that map to the dashboard surface.
The migrated app will have these boundaries:
- `src/main.tsx` mounts the React application in the browser.
- `src/App.tsx` owns dashboard loading, refresh timing, document-to-UI mapping,
and non-ready dashboard states.
- `src/server/index.ts` serves the Vite build output in production and exposes
JSON API routes.
- `src/server/routes/dashboard.ts` loads the dashboard runtime and resolves live
datasources unless `DISABLE_LIVE_DATASOURCES=1`.
- `src/server/routes/agent-dashboard.ts` delegates POST requests to the existing
`handleAgentDashboardRequest` function.
- `src/lib/model/**`, `src/lib/server/db/**`, `src/lib/server/dashboard.ts`,
`src/lib/server/datasources/**`, and `src/lib/server/agent-config/**` remain
TypeScript business logic with minimal import-path updates.
- `src/lib/ui/components/*.tsx` contains the reusable React component library.
- `src/lib/ui/stories/*.stories.tsx` contains React Storybook stories.
- `src/lib/components/ui/*.tsx` contains shadcn/ui primitives used by the
dashboard component library.
- `components.json` records the shadcn configuration with Vite, Radix, the Nova
preset, and `$lib` import aliases.
## Component Migration
Every current Svelte UI component will be ported to React with typed props:
- Badge
- Button
- CornerBracketFrame
- DashboardFrame
- DashboardHeader
- DiagonalStripeField
- FooterCell
- FooterStatusCell
- GridFrame
- IconButton
- IconGlyph
- LineChart
- ModuleCard
- Panel
- ProgressMeter
- ScanlineField
- Separator
- ServiceGroupPanel
- ServicePanel
- ServiceRow
- SignalTrace
- Sparkline
- StatusBadge
- StatusStrip
- SystemState
- TelemetryCard
- TelemetryGrid
- TelemetryStrip
- WeatherModule
The visual design, CSS custom property tokens, accessibility attributes,
data-model identifiers, severity attributes, focusable links, reduced-motion
behavior, and screenshot-tested dashboard layout must stay equivalent to the
current Svelte implementation.
CSS will keep the existing design tokens in `src/lib/ui/tokens.css`.
`src/app.css` imports Tailwind, shadcn CSS, font assets, and the existing
Dimension Lab token file. shadcn semantic variables must be mapped to the dark
console palette so generated primitives fit the dashboard instead of resetting
the app to a light generic theme. Shared UI types and
`dashboardDocumentToUiDashboard` remain framework-agnostic TypeScript.
## Runtime And API Behavior
The React app will fetch `GET /api/dashboard` on page load. When the runtime
state is ready, the response includes the dashboard document and metadata. When
the runtime state is empty, loading, or invalid, the React app renders the same
state shell that the Svelte page currently renders.
Ready dashboard documents use `metadata.refreshIntervalSeconds` to schedule a
refresh. The React implementation will clear old timers when the dashboard
state changes and on unmount.
The existing agent configuration endpoint remains available at
`POST /api/agent/dashboard`. The request validation, token authorization,
preview, publish, rollback, JSON patch behavior, and persistence behavior remain
owned by `src/lib/server/agent-config/index.ts`.
## Build, Storybook, And Deployment
`package.json` will move from Svelte/SvelteKit dependencies to React tooling:
- Runtime dependencies include `react`, `react-dom`, `@iconify/react`,
selected shadcn primitive dependencies, Tailwind merge helpers, and existing
non-Svelte libraries that still apply.
- Dev dependencies include `@vitejs/plugin-react`, `@tailwindcss/vite`,
`tailwindcss`, and the shadcn package needed by the generated CSS import.
- Storybook moves from `@storybook/sveltekit` and Svelte CSF to
`@storybook/react-vite`.
- `svelte.config.js`, `src/app.html`, `src/routes/**`, and `.svelte` files are
removed after equivalent React/server files exist.
The production build still creates `build/index.js` as the Bun server entry so
the current container command remains:
```sh
DATABASE_URL=file:/data/dimensionlab.sqlite HOST=0.0.0.0 PORT=3000 bun build/index.js
```
The `Containerfile` continues to install with Bun, build with Bun, copy the
client/server build output plus `drizzle/`, and run the Bun server.
## Testing Strategy
The migration is verified with equivalent or stronger tests:
- TypeScript check covers React TSX, server modules, and shared model code.
- Current model, validation, database, datasource, and agent-config unit tests
remain in place.
- Svelte SSR component tests become React `react-dom/server` tests.
- Page rendering tests become React app/server response tests.
- Storybook boundary tests are updated to require React story files and continue
preventing Dimension Lab-specific content in reusable presentation stories.
- Playwright desktop/mobile tests continue to run against the production Bun
server and the built React app.
- The full QA gate remains `bun run test:qa`.
## Completion Criteria
The migration is complete only when current evidence proves all of these:
- There are no `.svelte` app, component, route, or story files left.
- `package.json` has no Svelte, SvelteKit, or Svelte Storybook dependencies.
- shadcn is configured for Vite/Radix with `$lib` aliases and only selected
primitives, not the full registry.
- `bun run check` passes.
- `bun run test:unit` passes.
- `bun run build` produces the React client build and Bun server entry.
- `bun run build-storybook` passes with React stories.
- `bun run test:e2e` passes on desktop and mobile.
- The dashboard renders from the same validated dashboard model data.
- `POST /api/agent/dashboard` still exercises the existing agent config logic.
- The production container still runs with `bun build/index.js`.
## Migration Approach
The work should be implemented in focused commits on `codex/react-migration`:
1. Establish React/Vite/Bun server scaffolding and tests while keeping the
current Svelte code available for reference.
2. Port reusable UI components to React and update render tests.
3. Port the dashboard app route and refresh behavior to React.
4. Port Storybook stories and presentation boundary tests to React.
5. Remove SvelteKit runtime files and dependencies.
6. Update build, e2e, README, and container behavior.
7. Run the full QA gate, push the branch, open a ready PR, perform independent
review, fix blockers, and merge only after checks and review pass.

View file

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

View file

@ -1,54 +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": "vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0",
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
"build-storybook": "storybook build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"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": {
"@iconify/svelte": "^5.2.2",
"@sinclair/typebox": "^0.34.49",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"drizzle-orm": "^0.45.2",
"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-svelte-csf": "^5.1.2",
"@storybook/addon-vitest": "^10.4.6",
"@storybook/sveltekit": "^10.4.6",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.65.2",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@types/bun": "^1.3.14",
"@types/node": "^25.9.3",
"drizzle-kit": "^0.31.10",
"msw": "^2.14.6",
"storybook": "^10.4.6",
"svelte": "^5.56.3",
"svelte-check": "^4.6.0",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.9"
},
"msw": {
"workerDirectory": [
"static"
]
"turbo": "^2.5.0"
}
}

View file

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

View file

@ -0,0 +1 @@
export { genericDashboardFixture } from "./generic";

View file

@ -1,6 +1,11 @@
export {
DASHBOARD_SCHEMA_VERSION,
DashboardDocumentSchema,
DatasourceReferenceSchema,
ServiceEntrySchema,
ServiceGroupSchema,
TelemetryCardSchema,
ThresholdSchema,
dashboardDocumentJsonSchema,
type DashboardDocument,
type DashboardModule,

View file

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

View file

@ -27,7 +27,7 @@ const LinkSchema = Type.Object(
{ additionalProperties: false },
);
const StaticDatasourceSchema = Type.Object(
export const StaticDatasourceSchema = Type.Object(
{
type: Type.Literal("static"),
label: Type.Optional(Type.String({ minLength: 1 })),
@ -36,7 +36,7 @@ const StaticDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const PlaceholderDatasourceSchema = Type.Object(
export const PlaceholderDatasourceSchema = Type.Object(
{
type: Type.Literal("placeholder"),
reason: Type.String({ minLength: 1 }),
@ -44,7 +44,7 @@ const PlaceholderDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const ExternalDatasourceSchema = Type.Object(
export const ExternalDatasourceSchema = Type.Object(
{
type: Type.Literal("external"),
adapter: Type.Union([
@ -58,7 +58,7 @@ const ExternalDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const DatasourceReferenceSchema = Type.Union([
export const DatasourceReferenceSchema = Type.Union([
StaticDatasourceSchema,
PlaceholderDatasourceSchema,
ExternalDatasourceSchema,
@ -98,13 +98,13 @@ const TextMetricValueSchema = Type.Object(
{ additionalProperties: false },
);
const MetricValueSchema = Type.Union([
export const MetricValueSchema = Type.Union([
PercentMetricValueSchema,
NonNegativeMetricValueSchema,
TextMetricValueSchema,
]);
const ThresholdSchema = Type.Object(
export const ThresholdSchema = Type.Object(
{
warning: Type.Optional(Type.Number({ minimum: 0 })),
danger: Type.Optional(Type.Number({ minimum: 0 })),

View file

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

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"types": ["node", "bun-types"]
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

1
packages/ui Submodule

@ -0,0 +1 @@
Subproject commit a7a472083555152b8e1a2dc018d3be5b3b10d60b

View file

@ -0,0 +1,362 @@
#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="${APP_NAME:-dimensionlab-website}"
SERVICE_NAME="${SERVICE_NAME:-dimensionlab-website.service}"
CONTAINER_NAME="${CONTAINER_NAME:-dimensionlab-website}"
IMAGE_REPO="${IMAGE_REPO:-localhost/dimensionlab-website}"
CONTAINERFILE="${CONTAINERFILE:-apps/web/Containerfile}"
PUBLIC_URL="${PUBLIC_URL:-https://dimensionlab.net/}"
TILE_BATCH_URL="${TILE_BATCH_URL:-https://dimensionlab.net/api/dashboard/tiles}"
DEPLOY_RESTART_STRATEGY="${DEPLOY_RESTART_STRATEGY:-auto}"
DEPLOY_SMOKE_TIMEOUT_SECONDS="${DEPLOY_SMOKE_TIMEOUT_SECONDS:-120}"
DEPLOY_CONTAINER_START_TIMEOUT_SECONDS="${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS:-90}"
dry_run=false
rollback_tag=""
release_tag=""
latest_tag="${IMAGE_REPO}:latest"
container_cli=""
deployment_started=false
rollback_done=false
rollback_in_progress=false
usage() {
cat <<USAGE
Usage: $0 [--dry-run]
Build and deploy ${APP_NAME} for the production ${SERVICE_NAME} unit.
Options:
--dry-run Print the deployment actions without changing the host.
USAGE
}
log() {
printf '[deploy:%s] %s\n' "$APP_NAME" "$*"
}
fail() {
printf '[deploy:%s] ERROR: %s\n' "$APP_NAME" "$*" >&2
if [ "${deployment_started:-false}" = "true" ] && [ "${rollback_in_progress:-false}" != "true" ]; then
rollback || true
fi
exit 1
}
run() {
if "$dry_run"; then
printf '[deploy:%s] DRY-RUN:' "$APP_NAME"
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}
for arg in "$@"; do
case "$arg" in
--dry-run)
dry_run=true
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
fail "unknown argument: $arg"
;;
esac
done
deployment_ref() {
printf '%s' "${DEPLOY_REF:-${GITHUB_REF:-${FORGEJO_REF:-}}}"
}
deployment_event() {
printf '%s' "${DEPLOY_EVENT_NAME:-${GITHUB_EVENT_NAME:-${FORGEJO_EVENT_NAME:-}}}"
}
require_main_push() {
local event
local ref
event="$(deployment_event)"
ref="$(deployment_ref)"
if [ -n "$event" ] && [ "$event" != "push" ]; then
fail "refusing to deploy for event '$event'; production deploys only run for push"
fi
if [ -n "$ref" ]; then
[ "$ref" = "refs/heads/main" ] || fail "refusing to deploy ref '$ref'; expected refs/heads/main"
return 0
fi
local branch
branch="$(git branch --show-current 2>/dev/null || true)"
[ "$branch" = "main" ] || fail "refusing to deploy branch '$branch'; expected main"
}
select_container_cli() {
if [ -n "${DEPLOY_CONTAINER_CLI:-}" ]; then
command -v "$DEPLOY_CONTAINER_CLI" >/dev/null 2>&1 || fail "container CLI not found: $DEPLOY_CONTAINER_CLI"
container_cli="$DEPLOY_CONTAINER_CLI"
return 0
fi
if command -v podman >/dev/null 2>&1; then
container_cli="podman"
return 0
fi
if command -v docker >/dev/null 2>&1; then
container_cli="docker"
return 0
fi
fail "podman or docker is required"
}
current_sha() {
if [ -n "${DEPLOY_SHA:-${GITHUB_SHA:-}}" ]; then
printf '%s' "${DEPLOY_SHA:-${GITHUB_SHA:-}}"
return 0
fi
git rev-parse HEAD
}
tag_existing_latest_for_rollback() {
rollback_tag="${IMAGE_REPO}:rollback-$(date -u +%Y%m%d%H%M%S)"
if "$container_cli" image inspect "$latest_tag" >/dev/null 2>&1; then
log "tagging current latest image as $rollback_tag"
run "$container_cli" tag "$latest_tag" "$rollback_tag"
else
log "no existing $latest_tag image found; rollback image tag will not be created"
rollback_tag=""
fi
}
container_systemd_unit() {
"$container_cli" inspect "$CONTAINER_NAME" \
--format '{{ index .Config.Labels "PODMAN_SYSTEMD_UNIT" }}' 2>/dev/null || true
}
require_container_managed_by_service() {
local unit
if "$dry_run"; then
log "DRY-RUN: would require $CONTAINER_NAME to be managed by $SERVICE_NAME"
return 0
fi
unit="$(container_systemd_unit)"
[ "$unit" = "$SERVICE_NAME" ] || fail "refusing to stop $CONTAINER_NAME; expected PODMAN_SYSTEMD_UNIT=$SERVICE_NAME, got '${unit:-unset}'"
}
validate_restart_strategy() {
case "$DEPLOY_RESTART_STRATEGY" in
systemctl)
if ! "$dry_run" && ! systemctl --user show "$SERVICE_NAME" >/dev/null 2>&1; then
fail "systemctl --user cannot access $SERVICE_NAME"
fi
;;
quadlet-container|kill-container)
require_container_managed_by_service
;;
auto)
if "$dry_run"; then
log "DRY-RUN: would validate automatic restart strategy"
elif ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user show "$SERVICE_NAME" >/dev/null 2>&1; then
require_container_managed_by_service
fi
;;
*)
fail "unknown DEPLOY_RESTART_STRATEGY: $DEPLOY_RESTART_STRATEGY"
;;
esac
}
initialize_submodules() {
log "initializing submodules"
run git config --global url."https://git.dimensionlab.net/".insteadOf "ssh://git@git.dimensionlab.net/"
run git submodule update --init --recursive
}
build_image() {
local sha
local short_sha
sha="$(current_sha)"
short_sha="${sha:0:12}"
release_tag="${IMAGE_REPO}:${short_sha}"
[ -f "$CONTAINERFILE" ] || fail "containerfile not found: $CONTAINERFILE"
log "building $release_tag and $latest_tag from $CONTAINERFILE"
run "$container_cli" build -f "$CONTAINERFILE" -t "$release_tag" -t "$latest_tag" .
}
restart_service() {
log "restarting $SERVICE_NAME with strategy $DEPLOY_RESTART_STRATEGY"
case "$DEPLOY_RESTART_STRATEGY" in
systemctl)
run systemctl --user restart "$SERVICE_NAME"
;;
quadlet-container|kill-container)
require_container_managed_by_service
run "$container_cli" stop "$CONTAINER_NAME"
;;
auto)
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active "$SERVICE_NAME" >/dev/null 2>&1; then
run systemctl --user restart "$SERVICE_NAME"
else
require_container_managed_by_service
run "$container_cli" stop "$CONTAINER_NAME"
fi
;;
*)
fail "unknown DEPLOY_RESTART_STRATEGY: $DEPLOY_RESTART_STRATEGY"
;;
esac
}
latest_image_id() {
"$container_cli" image inspect "$latest_tag" --format '{{.Id}}' 2>/dev/null || true
}
container_image_id() {
"$container_cli" inspect "$CONTAINER_NAME" --format '{{.Image}}' 2>/dev/null || true
}
container_running() {
local running
running="$("$container_cli" inspect "$CONTAINER_NAME" --format '{{.State.Running}}' 2>/dev/null || true)"
[ "$running" = "true" ]
}
wait_for_container_restart() {
local expected_image
if "$dry_run"; then
log "DRY-RUN: would wait for $CONTAINER_NAME to run $latest_tag"
return 0
fi
expected_image="$(latest_image_id)"
[ -n "$expected_image" ] || fail "could not resolve image id for $latest_tag"
wait_for_container_image "$expected_image" "new image" || fail "$CONTAINER_NAME did not restart on $latest_tag within ${DEPLOY_CONTAINER_START_TIMEOUT_SECONDS}s"
}
wait_for_container_image() {
local expected_image="$1"
local label="$2"
local deadline
deadline=$((SECONDS + DEPLOY_CONTAINER_START_TIMEOUT_SECONDS))
while [ "$SECONDS" -lt "$deadline" ]; do
if container_running && [ "$(container_image_id)" = "$expected_image" ]; then
log "$CONTAINER_NAME is running the $label"
return 0
fi
sleep 2
done
return 1
}
smoke_get() {
local url="$1"
curl -fsS --max-time 10 -o /dev/null "$url"
}
smoke_tiles() {
local response
response="$(
curl -fsS --max-time 20 \
-H "content-type: application/json" \
--data '{"tiles":[{"kind":"status","stripId":"footer-status","id":"system-status"}]}' \
"$TILE_BATCH_URL"
)"
[[ "$response" == *'"state":"ready"'* ]]
}
wait_for_smoke() {
local deadline
if "$dry_run"; then
log "DRY-RUN: would smoke check $PUBLIC_URL and $TILE_BATCH_URL"
return 0
fi
deadline=$((SECONDS + DEPLOY_SMOKE_TIMEOUT_SECONDS))
until smoke_get "$PUBLIC_URL" && smoke_tiles; do
if [ "$SECONDS" -ge "$deadline" ]; then
fail "smoke checks failed for $PUBLIC_URL and $TILE_BATCH_URL"
fi
sleep 3
done
log "smoke checks passed"
}
rollback() {
local rollback_image
if [ "$deployment_started" != "true" ] || [ -z "$rollback_tag" ] || [ "$rollback_done" = "true" ]; then
return 0
fi
rollback_done=true
rollback_in_progress=true
printf '[deploy:%s] rolling back to %s\n' "$APP_NAME" "$rollback_tag" >&2
rollback_image="$("$container_cli" image inspect "$rollback_tag" --format '{{.Id}}' 2>/dev/null || true)"
"$container_cli" tag "$rollback_tag" "$latest_tag" || true
if container_running; then
restart_service || true
else
printf '[deploy:%s] waiting for %s to recover with rollback image\n' "$APP_NAME" "$SERVICE_NAME" >&2
fi
if [ -n "$rollback_image" ] && wait_for_container_image "$rollback_image" "rollback image"; then
printf '[deploy:%s] rollback image is running\n' "$APP_NAME" >&2
else
printf '[deploy:%s] ERROR: rollback image did not become healthy\n' "$APP_NAME" >&2
fi
rollback_in_progress=false
}
on_error() {
local status=$?
rollback
exit "$status"
}
trap on_error ERR
require_main_push
select_container_cli
validate_restart_strategy
log "using container CLI: $container_cli"
log "target image: $latest_tag"
log "target service: $SERVICE_NAME"
initialize_submodules
tag_existing_latest_for_rollback
build_image
deployment_started=true
restart_service
wait_for_container_restart
wait_for_smoke
log "deployment finished"

View file

@ -1 +0,0 @@
@import "./lib/ui/tokens.css";

View file

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,2 +0,0 @@
export { dimensionLabDashboardFixture } from "./dimensionlab";
export { genericDashboardFixture } from "./generic";

View file

@ -1,47 +0,0 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const presentationRoots = [
join(process.cwd(), "src", "lib", "ui"),
join(process.cwd(), "src", "routes"),
];
const forbiddenTerms = [
"dimensionlab",
"dimension lab",
"vaultwarden",
"forgejo",
"grafana",
"uptime kuma",
"prometheus",
"backrest",
"open webui",
"comfyui",
"adminer",
"cockpit",
"ollama",
"dimensionlab.net",
];
describe("presentation content boundary", () => {
test("keeps environment-specific content out of route and UI implementation", () => {
const source = presentationRoots.map(readPresentationSource).join("\n").toLowerCase();
expect(forbiddenTerms.filter((term) => source.includes(term))).toEqual([]);
});
});
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 (!/\.(svelte|svelte\.js|ts|js|mjs|css|json)$/.test(path)) return "";
return readFileSync(path, "utf8");
}
return readdirSync(path)
.map((entry) => readPresentationSource(join(path, entry)))
.join("\n");
}

View file

@ -1,14 +0,0 @@
<script lang="ts">
import type { UiSeverity } from "../types";
import StatusBadge from "./StatusBadge.svelte";
let {
label,
severity = "neutral",
}: {
label: string;
severity?: UiSeverity;
} = $props();
</script>
<StatusBadge {label} {severity} />

View file

@ -1,85 +0,0 @@
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
import IconGlyph from "./IconGlyph.svelte";
type ButtonProps = Omit<HTMLButtonAttributes, "type"> & {
label: string;
variant?: "primary" | "secondary" | "danger" | "ghost";
size?: "default" | "compact";
icon?: string;
loading?: boolean;
type?: "button" | "submit" | "reset";
};
let {
label,
variant = "primary",
size = "default",
icon,
disabled = false,
loading = false,
type = "button",
class: className = "",
...buttonProps
}: ButtonProps = $props();
</script>
<button
{...buttonProps}
class={className ? `ui-button ${className}` : "ui-button"}
data-variant={variant}
data-size={size}
data-loading={loading}
{disabled}
{type}
>
{#if icon}
<IconGlyph name={icon} size="sm" />
{/if}
<span>{loading ? "Loading" : label}</span>
</button>
<style>
.ui-button {
display: inline-grid;
grid-auto-flow: column;
gap: var(--ui-space-2);
align-items: center;
justify-content: center;
min-height: 2.5rem;
border: var(--ui-border);
background: var(--ui-color-accent);
color: var(--ui-color-canvas);
cursor: pointer;
font-size: 0.76rem;
font-weight: 850;
letter-spacing: 0;
padding: 0 var(--ui-space-4);
text-transform: uppercase;
}
.ui-button[data-size="compact"] {
min-height: 2rem;
padding-inline: var(--ui-space-3);
}
.ui-button[data-variant="secondary"] {
background: var(--ui-color-surface-raised);
color: var(--ui-color-text);
}
.ui-button[data-variant="danger"] {
background: var(--ui-color-danger);
color: var(--ui-color-text);
}
.ui-button[data-variant="ghost"] {
background: transparent;
color: var(--ui-color-muted);
}
.ui-button:disabled {
cursor: not-allowed;
opacity: 0.48;
}
</style>

View file

@ -1,97 +0,0 @@
<script lang="ts">
let {
density = "regular",
size = "md",
tone = "neutral",
}: {
density?: "regular" | "tight";
size?: "sm" | "md" | "lg";
tone?: "neutral" | "accent" | "danger";
} = $props();
</script>
<div
class="corner-bracket-frame"
data-density={density}
data-size={size}
data-tone={tone}
aria-hidden="true"
>
<span data-corner="top-left"></span>
<span data-corner="top-right"></span>
<span data-corner="bottom-left"></span>
<span data-corner="bottom-right"></span>
</div>
<style>
.corner-bracket-frame {
--corner-color: var(--ui-color-line-strong);
--corner-offset: 0.5rem;
--corner-size: 1.45rem;
position: relative;
min-height: 5rem;
border: 1px solid transparent;
background:
linear-gradient(90deg, color-mix(in srgb, var(--corner-color), transparent 78%) 1px, transparent 1px),
linear-gradient(color-mix(in srgb, var(--corner-color), transparent 84%) 1px, transparent 1px),
rgba(2, 3, 2, 0.64);
background-size: 2rem 2rem, 2rem 2rem, auto;
}
span {
position: absolute;
width: var(--corner-size);
height: var(--corner-size);
border-color: var(--corner-color);
}
[data-corner="top-left"] {
top: var(--corner-offset);
left: var(--corner-offset);
border-top: 1px solid;
border-left: 1px solid;
}
[data-corner="top-right"] {
top: var(--corner-offset);
right: var(--corner-offset);
border-top: 1px solid;
border-right: 1px solid;
}
[data-corner="bottom-left"] {
bottom: var(--corner-offset);
left: var(--corner-offset);
border-bottom: 1px solid;
border-left: 1px solid;
}
[data-corner="bottom-right"] {
right: var(--corner-offset);
bottom: var(--corner-offset);
border-right: 1px solid;
border-bottom: 1px solid;
}
.corner-bracket-frame[data-density="tight"] {
--corner-offset: 0.28rem;
}
.corner-bracket-frame[data-size="sm"] {
--corner-size: 0.95rem;
min-height: 3rem;
}
.corner-bracket-frame[data-size="lg"] {
--corner-size: 2.25rem;
min-height: 8rem;
}
.corner-bracket-frame[data-tone="accent"] {
--corner-color: var(--ui-color-accent);
}
.corner-bracket-frame[data-tone="danger"] {
--corner-color: var(--ui-color-danger);
}
</style>

View file

@ -1,154 +0,0 @@
<script lang="ts">
import type { UiDashboardPreview } from "../types";
import ModuleCard from "./ModuleCard.svelte";
import ServicePanel from "./ServicePanel.svelte";
import StatusStrip from "./StatusStrip.svelte";
import TelemetryGrid from "./TelemetryGrid.svelte";
const generatedTitleId = $props.id();
let {
dashboard,
titleId = `${generatedTitleId}-title`,
}: {
dashboard: UiDashboardPreview;
titleId?: string;
} = $props();
</script>
<main class="dashboard-frame" aria-labelledby={titleId}>
<header class="dashboard-frame__header">
<div>
{#if dashboard.eyebrow}
<p>{dashboard.eyebrow}</p>
{/if}
<h1 id={titleId}>{dashboard.title}</h1>
{#if dashboard.subtitle}
<span>{dashboard.subtitle}</span>
{/if}
</div>
{#if dashboard.modules.length}
<div class="dashboard-frame__modules">
{#each dashboard.modules as module (module.id)}
<ModuleCard {module} />
{/each}
</div>
{/if}
</header>
<TelemetryGrid cards={dashboard.telemetry} />
<section class="dashboard-frame__panels" aria-label="Service groups">
{#each dashboard.serviceGroups as group (group.id)}
<ServicePanel {group} />
{/each}
</section>
<StatusStrip id={dashboard.statusStripId} items={dashboard.statusItems} />
</main>
<style>
.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 {
display: grid;
grid-template-columns: minmax(30rem, 1fr) minmax(30rem, 0.9fr);
gap: 0.5rem;
align-items: start;
border-bottom: var(--ui-border);
min-height: 5.35rem;
padding-bottom: 0.34rem;
}
.dashboard-frame__header p,
.dashboard-frame__header span,
h1 {
margin: 0;
text-transform: uppercase;
}
.dashboard-frame__header p,
.dashboard-frame__header span {
color: var(--ui-color-muted);
font-size: 0.78rem;
font-weight: 800;
}
h1 {
max-width: 100%;
overflow-wrap: anywhere;
font-family: var(--ui-font-display);
font-size: clamp(2.35rem, 3.65vw, 3.25rem);
font-weight: 850;
letter-spacing: 0;
line-height: 0.78;
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 :global(.service-panel[data-layout="grid"]) {
grid-column: 1 / -1;
}
.dashboard-frame__panels
:global(.service-panel[data-layout="grid"] .panel__body) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 780px) {
.dashboard-frame {
height: auto;
min-height: 100vh;
overflow: visible;
}
.dashboard-frame__header {
grid-template-columns: 1fr;
}
.dashboard-frame__modules {
grid-template-columns: 1fr;
}
h1 {
white-space: normal;
}
.dashboard-frame__panels {
grid-template-columns: 1fr;
overflow: visible;
}
.dashboard-frame__panels
:global(.service-panel[data-layout="grid"] .panel__body) {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,71 +0,0 @@
<script lang="ts">
import type { UiModuleBlock } from "../types";
import ModuleCard from "./ModuleCard.svelte";
let {
title,
subtitle,
eyebrow,
module,
}: {
title: string;
subtitle?: string;
eyebrow?: string;
module?: UiModuleBlock;
} = $props();
const titleId = $props.id();
</script>
<header class="dashboard-header" aria-labelledby={titleId}>
<div>
{#if eyebrow}
<p>{eyebrow}</p>
{/if}
<h1 id={titleId}>{title}</h1>
{#if subtitle}
<span>{subtitle}</span>
{/if}
</div>
{#if module}
<ModuleCard {module} />
{/if}
</header>
<style>
.dashboard-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--ui-space-4);
align-items: start;
border-bottom: var(--ui-border);
padding: var(--ui-space-3);
}
p,
span,
h1 {
margin: 0;
text-transform: uppercase;
}
p,
span {
color: var(--ui-color-muted);
font-size: 0.78rem;
font-weight: 800;
}
h1 {
overflow-wrap: anywhere;
font-family: var(--ui-font-display);
font-size: clamp(2.45rem, 5vw, 4.2rem);
line-height: 0.85;
}
@media (max-width: 780px) {
.dashboard-header {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,77 +0,0 @@
<script lang="ts">
let {
density = "regular",
direction = "forward",
size = "md",
tone = "accent",
}: {
density?: "open" | "regular" | "tight";
direction?: "forward" | "backward";
size?: "sm" | "md" | "lg";
tone?: "neutral" | "accent" | "warning" | "danger";
} = $props();
</script>
<div
class="diagonal-stripe-field"
data-density={density}
data-direction={direction}
data-size={size}
data-tone={tone}
aria-hidden="true"
></div>
<style>
.diagonal-stripe-field {
--stripe-angle: 135deg;
--stripe-color: var(--ui-color-line-strong);
--stripe-gap: 1.1rem;
--stripe-width: 0.18rem;
display: block;
min-height: 3rem;
border: var(--ui-border);
background:
repeating-linear-gradient(
var(--stripe-angle),
transparent 0 var(--stripe-gap),
color-mix(in srgb, var(--stripe-color), transparent 26%) var(--stripe-gap)
calc(var(--stripe-gap) + var(--stripe-width))
),
linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent 42%),
rgba(2, 3, 2, 0.82);
}
.diagonal-stripe-field[data-direction="backward"] {
--stripe-angle: 45deg;
}
.diagonal-stripe-field[data-density="open"] {
--stripe-gap: 1.65rem;
--stripe-width: 0.16rem;
}
.diagonal-stripe-field[data-density="tight"] {
--stripe-gap: 0.72rem;
--stripe-width: 0.14rem;
}
.diagonal-stripe-field[data-size="sm"] {
min-height: 1.75rem;
}
.diagonal-stripe-field[data-size="lg"] {
min-height: 5.5rem;
}
.diagonal-stripe-field[data-tone="accent"] {
--stripe-color: var(--ui-color-accent);
}
.diagonal-stripe-field[data-tone="warning"] {
--stripe-color: var(--ui-color-warning);
}
.diagonal-stripe-field[data-tone="danger"] {
--stripe-color: var(--ui-color-danger);
}
</style>

View file

@ -1,90 +0,0 @@
<script lang="ts">
import type { UiStatusItem } from "../types";
let { item }: { item: UiStatusItem } = $props();
let target = $derived(item.link?.external ? "_blank" : undefined);
let rel = $derived(item.link?.external ? "noreferrer" : undefined);
</script>
{#snippet cellContent()}
<span>{item.label}</span>
<strong>{item.value}</strong>
{/snippet}
{#if item.link}
<a
class="footer-cell"
data-severity={item.severity || "neutral"}
data-model-id={item.id}
href={item.link.href}
target={target}
rel={rel}
aria-label={item.link.label}
>
{@render cellContent()}
</a>
{:else}
<div class="footer-cell" data-severity={item.severity || "neutral"} data-model-id={item.id}>
{@render cellContent()}
</div>
{/if}
<style>
.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;
}
span,
strong {
min-width: 0;
padding: 0.34rem 0.52rem;
overflow-wrap: anywhere;
text-transform: uppercase;
}
span {
height: 100%;
border-right: var(--ui-border);
color: var(--ui-color-muted);
font-size: 0.52rem;
font-weight: 800;
}
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);
}
</style>

View file

@ -1,8 +0,0 @@
<script lang="ts">
import type { UiStatusItem } from "../types";
import FooterCell from "./FooterCell.svelte";
let { item }: { item: UiStatusItem } = $props();
</script>
<FooterCell {item} />

View file

@ -1,30 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
let {
children,
density = "dense",
}: {
children?: Snippet;
density?: "compact" | "dense";
} = $props();
</script>
<section class="grid-frame" data-density={density}>
{@render children?.()}
</section>
<style>
.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);
}
.grid-frame[data-density="compact"] {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
gap: var(--ui-space-2);
padding: var(--ui-space-2);
}
</style>

View file

@ -1,57 +0,0 @@
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
import IconGlyph from "./IconGlyph.svelte";
type IconButtonProps = Omit<HTMLButtonAttributes, "type"> & {
icon: string;
label: string;
active?: boolean;
type?: "button" | "submit" | "reset";
};
let {
icon,
label,
active = false,
disabled = false,
type = "button",
class: className = "",
...buttonProps
}: IconButtonProps = $props();
</script>
<button
{...buttonProps}
class={className ? `icon-button ${className}` : "icon-button"}
aria-label={label}
data-active={active}
{disabled}
{type}
>
<IconGlyph name={icon} size="sm" />
</button>
<style>
.icon-button {
display: inline-grid;
width: 2.5rem;
height: 2.5rem;
place-items: center;
border: var(--ui-border);
background: rgba(2, 3, 2, 0.92);
color: var(--ui-color-muted);
cursor: pointer;
padding: 0;
}
.icon-button[data-active="true"],
.icon-button:hover {
border-color: var(--ui-color-accent);
color: var(--ui-color-accent);
}
.icon-button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
</style>

View file

@ -1,53 +0,0 @@
<script lang="ts">
import Icon from "@iconify/svelte";
let {
name,
label,
size = "md",
}: {
name?: string;
label?: string;
size?: "sm" | "md" | "lg";
} = $props();
</script>
<span
class="icon-glyph"
data-size={size}
data-icon-name={name}
aria-label={label}
aria-hidden={label ? undefined : "true"}
>
{#if name}
<Icon icon={name} />
{/if}
</span>
<style>
.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 :global(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;
}
</style>

View file

@ -1,147 +0,0 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import "uplot/dist/uPlot.min.css";
import type { UiSeverity } from "../types";
let {
values = [],
severity = "neutral",
label = "Telemetry trend",
}: {
values?: number[];
severity?: UiSeverity;
label?: string;
} = $props();
let chartElement: HTMLDivElement;
let chart: ChartInstance | null = null;
let resizeObserver: ResizeObserver | null = null;
onMount(async () => {
const module = await import("uplot");
chart = new module.default(chartOptions(chartElement, severity), chartData(values), chartElement);
resizeObserver = new ResizeObserver(() => {
chart?.setSize(chartSize(chartElement));
});
resizeObserver.observe(chartElement);
});
$effect(() => {
chart?.setData(chartData(values));
chart?.setSize(chartSize(chartElement));
});
onDestroy(() => {
resizeObserver?.disconnect();
chart?.destroy();
});
</script>
<div
class="line-chart"
data-chart-library="uplot"
data-severity={severity}
role="img"
aria-label={label}
>
<div class="line-chart__canvas" bind:this={chartElement} aria-hidden="true"></div>
</div>
<style>
.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 :global(.uplot) {
width: 100% !important;
height: 100% !important;
background: transparent;
font-family: var(--ui-font-mono);
}
.line-chart__canvas :global(.u-over),
.line-chart__canvas :global(.u-under) {
overflow: visible;
}
</style>
<script lang="ts" module>
type ChartInstance = {
destroy(): void;
setData(data: import("uplot").AlignedData): 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: import("../types").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: import("../types").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";
}
</script>

View file

@ -1,104 +0,0 @@
<script lang="ts">
import type { UiModuleBlock } from "../types";
import IconGlyph from "./IconGlyph.svelte";
const generatedId = $props.id();
let { module }: { module: UiModuleBlock } = $props();
let titleId = $derived(`${generatedId}-title`);
let ariaLabel = $derived(module.title ? undefined : module.label || module.id);
</script>
<aside
class="module-card"
data-severity={module.severity || "neutral"}
data-model-id={module.id}
aria-labelledby={module.title ? titleId : undefined}
aria-label={ariaLabel}
>
<div>
{#if module.title}
<h2 id={titleId}>{module.title}</h2>
{/if}
{#if module.value}
<strong>{module.value}</strong>
{/if}
{#if module.detail || module.label}
<p>{module.detail || module.label}</p>
{/if}
</div>
{#if module.icon}
<IconGlyph name={module.icon} size="lg" />
{/if}
</aside>
<style>
.module-card {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.5rem;
align-items: start;
min-width: 0;
min-height: 4.15rem;
border: var(--ui-border);
background: rgba(6, 8, 7, 0.82);
padding: 0.5rem;
}
h2,
p {
margin: 0;
}
h2 {
color: var(--ui-color-muted);
font-size: 0.6rem;
line-height: 1;
text-transform: uppercase;
}
strong {
display: block;
margin-top: 0.15rem;
font-family: var(--ui-font-display);
font-size: clamp(1.45rem, 2vw, 2rem);
line-height: 0.82;
overflow-wrap: anywhere;
}
p {
margin-top: 0.2rem;
color: var(--ui-color-muted);
font-size: 0.52rem;
line-height: 1.05;
overflow: hidden;
text-overflow: ellipsis;
text-transform: uppercase;
white-space: nowrap;
}
.module-card[data-severity="ok"] :global(.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);
}
</style>

View file

@ -1,81 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
let {
title,
density = "dense",
children,
}: {
title?: string;
density?: "compact" | "dense";
children?: Snippet;
} = $props();
</script>
<section class="panel" data-density={density}>
{#if title}
<header class="panel__header">
<h2>{title}</h2>
</header>
{/if}
<div class="panel__body">
{@render children?.()}
</div>
</section>
<style>
.panel {
position: relative;
min-width: 0;
border: var(--ui-border);
background: rgba(3, 4, 3, 0.86);
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;
}
h2 {
margin: 0;
font-family: var(--ui-font-display);
font-size: clamp(1.45rem, 2.05vw, 1.95rem);
font-weight: 800;
letter-spacing: 0;
line-height: 0.9;
text-transform: uppercase;
}
.panel__body {
display: grid;
gap: 0.25rem;
padding: 0.42rem 0.55rem 0.55rem;
}
.panel[data-density="compact"] .panel__body {
padding: var(--ui-space-2);
}
</style>

View file

@ -1,58 +0,0 @@
<script lang="ts">
import { clampPercent } from "../format";
import type { UiSeverity } from "../types";
let {
value,
severity = "neutral",
label = "Progress",
}: {
value?: number;
severity?: UiSeverity;
label?: string;
} = $props();
let progress = $derived(value === undefined ? null : clampPercent(value));
</script>
<div
class="progress-meter"
data-severity={severity}
role="meter"
aria-label={label}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progress ?? undefined}
data-empty={progress === null}
>
{#if progress !== null}
<span style={`--meter-progress: ${progress}%`}></span>
{/if}
</div>
<style>
.progress-meter {
height: 0.4rem;
border: var(--ui-border);
background: #030403;
}
.progress-meter span {
display: block;
width: var(--meter-progress);
height: 100%;
background: var(--ui-color-accent);
}
.progress-meter[data-severity="warning"] span {
background: var(--ui-color-warning);
}
.progress-meter[data-severity="danger"] span {
background: var(--ui-color-danger);
}
.progress-meter[data-empty="true"] {
opacity: 0.46;
}
</style>

View file

@ -1,66 +0,0 @@
<script lang="ts">
let {
intensity = "medium",
size = "md",
tone = "neutral",
}: {
intensity?: "soft" | "medium" | "hard";
size?: "sm" | "md" | "lg";
tone?: "neutral" | "accent" | "warning";
} = $props();
</script>
<div
class="scanline-field"
data-intensity={intensity}
data-size={size}
data-tone={tone}
aria-hidden="true"
></div>
<style>
.scanline-field {
--scan-color: rgba(243, 244, 237, 0.12);
--scan-gap: 0.48rem;
--scan-weight: 1px;
display: block;
min-height: 4rem;
border: var(--ui-border);
background:
radial-gradient(circle at 18% 24%, color-mix(in srgb, var(--scan-color), transparent 35%) 0 1px, transparent 2px),
radial-gradient(circle at 76% 62%, color-mix(in srgb, var(--scan-color), transparent 44%) 0 1px, transparent 2px),
repeating-linear-gradient(
180deg,
transparent 0 calc(var(--scan-gap) - var(--scan-weight)),
var(--scan-color) calc(var(--scan-gap) - var(--scan-weight)) var(--scan-gap)
),
rgba(6, 7, 6, 0.78);
background-size: 3.1rem 3.1rem, 4.7rem 4.7rem, auto, auto;
}
.scanline-field[data-intensity="soft"] {
--scan-color: rgba(243, 244, 237, 0.07);
--scan-gap: 0.62rem;
}
.scanline-field[data-intensity="hard"] {
--scan-color: rgba(215, 255, 0, 0.2);
--scan-gap: 0.36rem;
}
.scanline-field[data-size="sm"] {
min-height: 2rem;
}
.scanline-field[data-size="lg"] {
min-height: 6.25rem;
}
.scanline-field[data-tone="accent"] {
--scan-color: rgba(215, 255, 0, 0.16);
}
.scanline-field[data-tone="warning"] {
--scan-color: rgba(255, 176, 32, 0.16);
}
</style>

View file

@ -1,39 +0,0 @@
<script lang="ts">
let {
orientation = "horizontal",
dense = false,
}: {
orientation?: "horizontal" | "vertical";
dense?: boolean;
} = $props();
</script>
<div
class="separator"
data-orientation={orientation}
data-dense={dense}
role="separator"
aria-orientation={orientation}
></div>
<style>
.separator {
background: var(--ui-color-line);
}
.separator[data-orientation="horizontal"] {
width: 100%;
height: 1px;
margin-block: var(--ui-space-3);
}
.separator[data-orientation="vertical"] {
width: 1px;
min-height: 3rem;
align-self: stretch;
}
.separator[data-dense="true"] {
margin-block: var(--ui-space-1);
}
</style>

View file

@ -1,8 +0,0 @@
<script lang="ts">
import type { UiServiceGroup } from "../types";
import ServicePanel from "./ServicePanel.svelte";
let { group }: { group: UiServiceGroup } = $props();
</script>
<ServicePanel {group} />

Some files were not shown because too many files have changed in this diff Show more