Dashboard application — client SPA, frontend framework, and build-into-plugin
Status: open/accepted
Summary
Section titled “Summary”- The status dashboard is the first application in the
D-0012-monorepo-tooling
apps/tree. This decision fixes how that application is built, served, and shipped; D-0012 fixes the workspace it lives in. See Context. - Serving/shipping: a client-rendered SPA served by the existing Bun/Hono
server (no SSR); the built SPA is committed and drift-gated inside the
plugin (
web-dist/); v1 targets four data sources. See Decision. - Frontend: Vite + Vue 3. A client-rendered Vue 3 SPA built with Vite. The two requirements driving the pick — and v1 success criteria — are: spin up the backend and frontend easily (one-command dev), and easily generate bindings for the frontend to use. See Options considered.
- Dependency direction follows
S-0008-apps-consume-substrate-through-published-surfaces; the committed-bundle
drift gate reuses the
--checkpattern from D-0010-deterministic-site-assembly. See Consequences. - Partially superseded by D-0018-sdf-control-plane-app: the SPA and
the frontend-framework choice are superseded for the operator app
(
apps/dashboardexcised,T-QT7X); the serving model and JSON API remain live as SDF’s data plane.
Context
Section titled “Context”The status dashboard (plugin/lib/services/dashboard/) is today a Bun/Hono
server whose entire UI is a ~180-line HTML/CSS/JS string (INDEX_HTML) in
server.ts. That string has reached the limit of maintainability; the goal is
a component-based frontend.
The dashboard is the first concrete consumer that fired
D-0001-project-structure’s transition trigger and motivated
D-0012-monorepo-tooling. It lives at apps/dashboard/ and consumes the
substrate through the JSON API the server already exposes, per
S-0008-apps-consume-substrate-through-published-surfaces — never by importing
plugin/lib/.
| Fact | Source | Implication |
|---|---|---|
Dashboard JSON API already exists (GET /api/state, POST /api/refresh) | server.ts | The frontend can be a pure client of it |
Server supervised by sdlc dashboard start/stop/list | dashboard-service.ts | The frontend choice must not add a second server process |
| Plugin runs from source, no consumer build step | package.json; D-0006-typescript-substrate | A built SPA must ship inside the plugin |
Docs-site output (sites/df-docs/) is gitignored | .gitignore | Committing built assets is a new pattern here |
Decision
Section titled “Decision”This decision settles the serving/shipping model and the frontend framework.
- The dashboard is a client-rendered SPA, served by the existing Bun/Hono
server. SSR is not adopted — no Nitro/Node server is introduced. The
SPA is built to static assets; the existing server serves those assets and
keeps owning the JSON API. This preserves the
sdlc dashboard start/stop/listsingle-process supervision model and is Tauri-ready (a static bundle is what Tauri’sfrontendDistwants). - The built SPA ships inside the plugin as a committed, generated,
drift-gated artifact (target:
plugin/lib/services/dashboard/web-dist/). Consumers get a working dashboard with no build step, preserving the install-by-symlink, run-from-source model. The SPA’s built output is copied into the plugin tree; its source (underapps/dashboard/) never imports from it — the seam is the JSON API, not the filesystem. A--checkdrift gate — mirroringsdlc docs generate site --checkfrom D-0010-deterministic-site-assembly — fails CI/commit whenweb-dist/would change against source. The directory is markedlinguist-generated. - Dashboard v1 targets four data sources. Two predate this decision and
carry forward — Git lease refs (
refs/sdlc/tasks/*) and task/milestone entities (docs/planning/). Two are net-new dashboard work — local working state (this checkout’s worktrees, git status, and in-flighttask-worksessions) and GitHub PR status (viagh, gated by the existing--no-networkflag). “Active work, locally and on Git/GitHub” is the v1 view. - Frontend framework: Vite + Vue 3. A client-rendered Vue 3 SPA built with
Vite, chosen over Nuxt because the SSR-off dashboard uses none of Nuxt’s
distinguishing engine, and Vite is smaller, has a fast dev server, and is
Tauri’s canonical frontend. Two requirements drive it and become v1 success
criteria: (a) easy dev startup — one command brings up the Bun/Hono
backend and the Vite frontend together (e.g. a moon task /
just dev); and (b) easy binding generation — a generated typed API client for the substrate, per S-0008-apps-consume-substrate-through-published-surfaces, that the frontend consumes (no hand-written request plumbing).
- SSR buys nothing here and costs Tauri-readiness. The dashboard reads local git/filesystem state behind an API; there is no SEO or first-paint-from-server need. An SSR server is a second process to supervise and is not a static bundle, which is what Tauri embeds. Choosing a static SPA now means zero rework for the desktop wrapper later.
- Committed build artifact preserves the no-build consumer model. Today a
consumer installs by symlink and runs from source via Bun
(D-0006-typescript-substrate recommends Bun; transpilation is built in)
— no consumer-side build. A frontend build step would break that. Committing
web-dist/keeps the promise; the drift gate keeps the committed copy honest, reusing machinery the project already trusts (D-0011-markdown-formatting-tool). - A relative API base keeps the data layer swappable. The SPA calls the
API through a relative base (e.g.
/api) since the server serves both the SPA and the API — sidestepping the static runtime-config problem and keeping the transport swappable for Tauri (invokevs HTTP).
Options considered
Section titled “Options considered”Frontend framework
Section titled “Frontend framework”Both options yield a Vue 3 client-rendered SPA; they differ in scaffolding, footprint, and how cleanly the output drops into the Hono server and Tauri.
| Aspect | Nuxt (ssr: false) | Vite + Vue 3 |
|---|---|---|
| Routing | File-based (built-in) | vue-router (explicit) |
| Auto-imports | Built-in | unplugin-auto-import (opt-in) |
| Dependency footprint | Large | Small |
| Static output | nuxi generate → .output/public | vite build → dist |
| Tauri fit | Works | Canonical (Tauri’s reference example) |
| Config gotcha | runtimeConfig.public is build-baked for static | None |
| Nuxt engine used | None — SSR/Nitro is off | n/a |
| Author familiarity | High | High |
Because ssr: false turns off the SSR/Nitro engine that is Nuxt’s reason to
exist, the marginal value of Nuxt here is file-based routing and auto-imports
— both replaceable in Vite + Vue with small plugins. Vite + Vue 3 is chosen
— smaller, a fast dev server, no runtimeConfig static-baking trap, the
cleanest Tauri path, and a clean fit with a generated typed API client. The two
requirements that decided it (easy backend+frontend dev startup, easy binding
generation) become v1 success criteria in the Decision.
Consequences
Section titled “Consequences”- Milestone tasks decompose the implementation; this document records the decision only.
- The committed
web-dist/is a new artifact pattern for this repo (docs-site output is gitignored today). It needs a drift gate (--check), a.gitattributeslinguist-generatedmark, and reviewer awareness that minified-bundle churn is expected on dashboard changes. - Dependency direction follows the contract recorded in
S-0008-apps-consume-substrate-through-published-surfaces: app code reaches the
substrate through a clearly-defined API — a generated typed client or the
JSON API — never by importing
plugin/lib/. - A future Standard will codify the committed-build-artifact contract (generated + drift-gated) once this pipeline is accepted.
Migration / rollout
Section titled “Migration / rollout”Milestone tasks implement the decision:
- Scaffold
apps/dashboard(Vite + Vue 3) with a one-command dev startup for the Bun/Hono backend + Vite frontend (just dev→moon run dashboard:dev), and a generated typed API client the frontend consumes (packages/ts/dashboard-client, generated from the sharedcontract.tszod contract). - Port the dashboard view (the four target sources) to the SPA; extend the
JSON API with the two net-new sources (
local_work,pull_requests). - Wire the build-into-plugin task and the
web-dist/drift gate. Retire theINDEX_HTMLstring inserver.ts; repointGET /to serveweb-dist/index.htmlwith an SPA fallback. - Later: a Rust Tauri wrapper consuming the same static bundle — the validating case for the polyglot workspace (D-0012-monorepo-tooling).
Open questions
Section titled “Open questions”web-dist/location and drift-gate verb name (e.g.sdlc dashboard build --check). Settle at implementation.- Committed bundle vs. API contract. A
web-dist/built against an older API than the server it is served by (or vice versa, on an already-installed consumer) is a runtime drift the commit-time--checkgate cannot catch. Decide whether the server version-checks the bundle it serves. - Local-server exposure. The server binds
127.0.0.1by default (overridable via--host) and serves full task/lease/project-root state plus a network-mutatingPOST /api/refresh. An SPA on a same-origin relative/apibase widens that surface. Decide whetherPOST /api/refreshneeds a guard beyond the--no-networkshort-circuit.
- This is the application complement to D-0012-monorepo-tooling: D-0012 fixes the workspace and tooling; this fixes the dashboard application’s frontend, serving model, and build-into-plugin mechanism.
- Research basis (2026): Nuxt 4.x static SPA via
ssr: false+nitro.preset: 'static'(runtimeConfig.publicis build-time-baked for static targets — use a relative API base); Tauri v2frontendDistexpects a static dist directory; Vite is Tauri’s canonical frontend example. - Filename follows D-0002-entity-identifier-shape:
D-0013-prefix, slugdashboard-app. Hand-authored against the decision body template (nosdlc decision createverb).