# Web UI Scaffold — Design **Status:** Approved (brainstorm), ready for implementation plan **Date:** 2026-04-20 **Milestone:** M1.5 (new) — scaffold for the web UI that M2–M5 layer features onto ## Purpose Minstrel needs a built-in web UI with feature parity to the planned Flutter client. The M1 Subsonic baseline is verified end-to-end via third-party clients (Feishin), but Minstrel's own UI surface is currently empty. This spec describes the scaffold: the stack, the packaging, the auth model, the API surface the SPA consumes, the shell layout, and the first-cut feature set. Features that depend on backend work not yet built (events, recommendations, radio, Lidarr) are explicitly deferred and will land alongside their respective backend milestones. The web UI is not a late-stage M6 milestone — it advances through M2–M5 in step with the server. ## Non-goals - OIDC / SSO login (deferred until after a solid UI exists; possibly post-v1). - Light-theme support (dark theme only for first cut; light mode follows). - Mobile-first responsive layouts (desktop-first; the web UI is expected to be *more dense* than the Flutter client, which will own the mobile experience). - SSR / server-rendered initial HTML (pure SPA — SvelteKit static adapter). - Reusing the `/rest/*` Subsonic surface from the SPA. ## Stack - **Framework:** SvelteKit + TypeScript + Vite. - **Adapter:** `@sveltejs/adapter-static`, building to `web/build/` as fully-static HTML/CSS/JS. - **SPA fallback:** `adapter-static` configured with `fallback: 'index.html'` so deep links (`/artists/abc`) load the SPA shell and client-side routing resolves the path. - **Styling:** Tailwind CSS + component-scoped Svelte styles. No UI kit — hand-rolled components kept minimal. - **State:** Svelte stores. No external state library. - **HTTP:** Native `fetch`; a thin wrapper (`web/src/lib/api.ts`) adds credentials, parses errors, returns typed responses. ### Why Svelte over React/Solid - Compiled output is small and fast. Matters when rendering large library views (paginated at first; virtualization if we hit perf walls later). - Built-in routing / stores / forms mean fewer library decisions for a solo project. - The trade-off (smaller ecosystem) is acceptable because the components we need — list views, player UI, form controls — are trivial to build or exist as libraries. ## Packaging - **Single binary, single image.** SvelteKit builds to static assets; Go embeds `web/build/` via `//go:embed` and serves from root (`/`) with SPA fallback (unmatched paths return `index.html`). - **Dockerfile:** multi-stage. 1. `FROM node: AS web` — `npm ci`, `npm run build`, produces `web/build/`. 2. `FROM golang: AS go` — `go build`, with `web/build/` copied in before build so `//go:embed` picks it up. 3. `FROM debian:stable-slim` (or existing base) — copy binary, ffmpeg, run. - Release artifact remains a single container image. `docker compose up` stays one service for the app. ## Dev workflow - Two processes during development: 1. `docker compose up` — Go server on `:4533`, Postgres, scanner. 2. `cd web && npm run dev` — Vite dev server on `:5173` with HMR. - Vite dev server proxies `/api/*` and `/rest/*` to `http://localhost:4533`. The SPA is loaded from `:5173` in dev; cookies on `:5173` work because the proxy rewrites the origin. - Production build: `npm run build` runs in CI / Docker build; static assets get embedded. ## API surface ### Principle - `/rest/*` is the Subsonic-compatibility surface for third-party clients. It keeps salted-md5 + apiKey auth, Subsonic envelope, XML/JSON shape. **The SPA does not call `/rest/*`.** - `/api/*` is the native surface for Minstrel's own clients (web SPA now, Flutter later). Clean JSON, cookie-or-bearer auth, designed for modern consumers. ### First-cut endpoints | Method | Path | Purpose | | --- | --- | --- | | POST | `/api/auth/login` | Verify credentials. Returns `{token, user}`, sets `httpOnly` session cookie. | | POST | `/api/auth/logout` | Invalidate session cookie and the bearer token associated with the caller's session. | | GET | `/api/me` | Return the authenticated user (username, is_admin, etc.). | | GET | `/api/artists` | Paginated artist list with sort keys. | | GET | `/api/artists/{id}` | Artist with albums array. | | GET | `/api/albums/{id}` | Album with tracks array. | | GET | `/api/tracks/{id}` | Single track (used for now-playing detail / direct-link). | | GET | `/api/search?q=` | Unified search returning `{artists, albums, tracks}` arrays. | | GET | `/api/stream/{trackId}` | Audio stream with `Accept-Ranges: bytes` for scrubbing. Cookie-auth works because `