Skip to content

Dashboard application — client SPA, frontend framework, and build-into-plugin

Status: open/accepted

  • 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 --check pattern 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/dashboard excised, T-QT7X); the serving model and JSON API remain live as SDF’s data plane.

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

FactSourceImplication
Dashboard JSON API already exists (GET /api/state, POST /api/refresh)server.tsThe frontend can be a pure client of it
Server supervised by sdlc dashboard start/stop/listdashboard-service.tsThe frontend choice must not add a second server process
Plugin runs from source, no consumer build steppackage.json; D-0006-typescript-substrateA built SPA must ship inside the plugin
Docs-site output (sites/df-docs/) is gitignored.gitignoreCommitting built assets is a new pattern here

This decision settles the serving/shipping model and the frontend framework.

  1. 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/list single-process supervision model and is Tauri-ready (a static bundle is what Tauri’s frontendDist wants).
  2. 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 (under apps/dashboard/) never imports from it — the seam is the JSON API, not the filesystem. A --check drift gate — mirroring sdlc docs generate site --check from D-0010-deterministic-site-assembly — fails CI/commit when web-dist/ would change against source. The directory is marked linguist-generated.
  3. 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-flight task-work sessions) and GitHub PR status (via gh, gated by the existing --no-network flag). “Active work, locally and on Git/GitHub” is the v1 view.
  4. 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 (invoke vs HTTP).

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.

AspectNuxt (ssr: false)Vite + Vue 3
RoutingFile-based (built-in)vue-router (explicit)
Auto-importsBuilt-inunplugin-auto-import (opt-in)
Dependency footprintLargeSmall
Static outputnuxi generate.output/publicvite builddist
Tauri fitWorksCanonical (Tauri’s reference example)
Config gotcharuntimeConfig.public is build-baked for staticNone
Nuxt engine usedNone — SSR/Nitro is offn/a
Author familiarityHighHigh

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.

  • 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 .gitattributes linguist-generated mark, 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.

Milestone tasks implement the decision:

  1. Scaffold apps/dashboard (Vite + Vue 3) with a one-command dev startup for the Bun/Hono backend + Vite frontend (just devmoon run dashboard:dev), and a generated typed API client the frontend consumes (packages/ts/dashboard-client, generated from the shared contract.ts zod contract).
  2. 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).
  3. Wire the build-into-plugin task and the web-dist/ drift gate. Retire the INDEX_HTML string in server.ts; repoint GET / to serve web-dist/index.html with an SPA fallback.
  4. Later: a Rust Tauri wrapper consuming the same static bundle — the validating case for the polyglot workspace (D-0012-monorepo-tooling).
  • 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 --check gate cannot catch. Decide whether the server version-checks the bundle it serves.
  • Local-server exposure. The server binds 127.0.0.1 by default (overridable via --host) and serves full task/lease/project-root state plus a network-mutating POST /api/refresh. An SPA on a same-origin relative /api base widens that surface. Decide whether POST /api/refresh needs a guard beyond the --no-network short-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.public is build-time-baked for static targets — use a relative API base); Tauri v2 frontendDist expects a static dist directory; Vite is Tauri’s canonical frontend example.
  • Filename follows D-0002-entity-identifier-shape: D-0013- prefix, slug dashboard-app. Hand-authored against the decision body template (no sdlc decision create verb).

← Back to Decisions