diff --git a/.gitignore b/.gitignore index 5b90e79f..8c2acc30 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ go.work.sum # env file .env + +# Superpowers brainstorming companion sessions +.superpowers/ diff --git a/docs/superpowers/specs/2026-04-20-web-ui-scaffold-design.md b/docs/superpowers/specs/2026-04-20-web-ui-scaffold-design.md new file mode 100644 index 00000000..568f4493 --- /dev/null +++ b/docs/superpowers/specs/2026-04-20-web-ui-scaffold-design.md @@ -0,0 +1,268 @@ +# 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 `