diff --git a/.gitignore b/.gitignore index fedaa2b..95a4505 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target .env + +.superpowers/ diff --git a/docs/superpowers/specs/2026-06-03-frontend-spa-milestone-1-design.md b/docs/superpowers/specs/2026-06-03-frontend-spa-milestone-1-design.md new file mode 100644 index 0000000..7b6a9e6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-frontend-spa-milestone-1-design.md @@ -0,0 +1,178 @@ +# Frontend SPA — Milestone 1 (Foundation Slice) — Design + +**Date:** 2026-06-03 +**Status:** Approved (brainstorming) — ready for implementation planning. + +## Context + +The backend MVP is code-complete: an authenticated admin HTTP API (object CRUD + +flexible fields, vocabulary/authority/term management, stepwise publishing, search, +audit), a public read API, and a code-first OpenAPI document (utoipa). There is **no +frontend yet**. VISION + `docs/specs/2026-06-02-mvp-architecture.md` §17 call for a +**lean React SPA** in `web/` that consumes the OpenAPI, with sv/en localization and an +explicit "potato hardware" bundle-discipline budget. + +The admin UI is a large surface, so it is **decomposed into milestones**, each with its +own spec → plan → implementation cycle: + +1. **Foundation slice (this spec):** scaffold, build/dev/serve, typed API client, app + shell (nav + i18n), login/session, and the Objects screen (paginated list + + read-only detail). +2. Object authoring — create/edit/delete + the dynamic flexible-field form driven by + `field-definitions`. +3. Publishing workflow — stepwise draft→internal→public UI with the required-field gate. +4. Vocabulary & authority management. +5. Search UI. + +## Goal (Milestone 1) + +A walking skeleton that proves the whole pipeline end-to-end — **log in → fetch → +render → embedded in the binary** — and lands a real, usable screen (browse + read +catalogue objects). Every piece of plumbing here is reused by later milestones. + +## Decisions (settled during brainstorming) + +- **Stack:** React + TypeScript + **Vite**, **pnpm**, **shadcn/ui** components, + **TanStack Query** for server state, **react-i18next** for sv/en. +- **API client (Option A):** `openapi-typescript` generates types from the OpenAPI + JSON; `openapi-fetch` (tiny typed wrapper) for calls; TanStack Query hooks written by + hand. Smallest dependency surface, contract auto-synced, query layer under our control. +- **Layout (Option C):** two-pane **master–detail** — object list on the left, the + selected record fills the right pane (inspector style; efficient for browse + later + edit-in-place). +- **Testing (Option A):** **Vitest + React Testing Library + MSW** (Mock Service Worker) + — component/hook tests in-process; MSW intercepts fetches with handlers typed against + the generated schema, so tests stay honest to the OpenAPI contract without a browser. + A Playwright e2e smoke is deferred to a later milestone (fits once `--seed` data exists). +- **Bundle budget:** initial JS **≤ 150 KB gzipped**, tracked in CI. + +## Scope (YAGNI) + +**In:** +- `web/` scaffold + build/dev/serve (incl. release embedding). +- Generated typed API client + TanStack Query hooks. +- App shell: compact icon sidebar (Objects active; later items shown **disabled / + "coming soon"**), top bar with sv/en switch + user menu (logout). +- Auth: login page, logout, session guard. +- **Objects two-pane:** paginated list (left) + **read-only** detail (right) showing the + inventory-minimum fields and flexible-field *values*, with a visibility badge. + +**Out (later milestones):** create/edit/delete and dynamic field *forms* (M2), publish +workflow (M3), vocabulary/authority/term management (M4), search UI (M5), media/ +thumbnails. Later nav items appear as disabled stubs so the shell's shape is visible. + +## Architecture + +### Project layout (`web/`) + +``` +web/ + package.json pnpm; scripts: dev, build, test, typecheck, lint, gen:api + vite.config.ts dev proxy /api,/api-docs,/health -> http://localhost:8080 + tsconfig.json + index.html + components.json shadcn/ui + src/ + main.tsx entry: QueryClientProvider, i18n, router + app.tsx route table + api/ + schema.d.ts generated (openapi-typescript) — committed + client.ts openapi-fetch client; credentials:'include'; 401 middleware + queries.ts useMe, useObjectsPage, useObject, useLogin, useLogout + auth/ + session.tsx session context (via /api/admin/me); RequireAuth guard + login-page.tsx + shell/ + app-shell.tsx icon sidebar + top bar + + lang-switch.tsx + objects/ + objects-page.tsx two-pane container (list + detail by :id) + object-list.tsx + object-detail.tsx + visibility-badge.tsx + i18n/ + index.ts react-i18next init + en.json sv.json + components/ui/... shadcn primitives + test/ + setup.ts Vitest + RTL + MSW server + handlers.ts MSW handlers typed against schema.d.ts +``` + +### Serve / build model + +- **Dev:** `pnpm dev` runs Vite on `:5173`, proxying `/api`, `/api-docs`, `/health` to + the Rust server on `:8080` (run separately). Session cookies stay same-origin through + the proxy. Backend dev/test loop is unchanged and independent of the frontend. +- **Release:** `pnpm build` → `web/dist`, embedded into the **`server`** binary via the + **`memory-serve`** crate and served at `/` with an SPA fallback (any non-`/api`, + non-`/health`, non-`/api-docs` path → `index.html` for client-side routing). +- **Feature gate `embed-web`** (off by default) on the `server` crate guards the + memory-serve embedding, so `cargo build`/`cargo test` never require a built `web/dist`. + CI builds the SPA first, then `cargo build -p server --features embed-web` for the + release artifact. **Milestone 1 proves this embed path end-to-end.** + +### Data flow + +1. App mounts → `useMe()` queries `/api/admin/me`. 200 → session established; 401 → the + client middleware redirects to `/login`. +2. `/objects` → `useObjectsPage(limit, offset)` → `GET /api/admin/objects` (the paginated + admin list already built). Left pane renders rows; selecting one routes to + `/objects/:id`. +3. `/objects/:id` → `useObject(id)` → `GET /api/admin/objects/{id}`. Right pane renders + inventory-minimum fields + flexible-field values + visibility badge; 404 → not-found + state. +4. Login: `useLogin` → `POST /api/admin/login` (session cookie set); on success invalidate + `useMe` and navigate to `/objects`. Logout: `POST /api/admin/logout` → clear → `/login`. + +### Routing + +| Path | Access | Renders | +|------|--------|---------| +| `/login` | public | login page (redirects to `/objects` if already authed) | +| `/` | protected | redirect → `/objects` | +| `/objects` | protected | two-pane; empty right pane prompt | +| `/objects/:id` | protected | two-pane; right pane = record | +| `*` | protected | redirect → `/objects` | + +`RequireAuth` wraps protected routes; unauthenticated → `/login`. + +## Error / loading / empty states + +- **List:** loading skeleton; empty ("no objects yet"); error with retry. +- **Detail:** loading; 404 not-found; error with retry. +- **Login:** inline invalid-credentials (401) and network-error messages. +- **Visibility:** i18n'd badge — draft (neutral) / internal (amber) / public (green). +- All user-facing copy lives in `en.json` + `sv.json`; language switch persists to + `localStorage`. + +## Testing & CI + +- **Vitest + RTL + MSW.** `test/setup.ts` starts an MSW server; `handlers.ts` returns + realistic responses typed against `schema.d.ts`. Coverage for M1: + - API client + query hooks (success + error mapping). + - Login flow: success → navigates; 401 → inline error. + - Objects list: renders rows, pagination controls, empty + error states. + - Object detail: renders fields + visibility; 404 state. + - Language switch toggles copy. + - `RequireAuth` redirects unauthenticated users to `/login`. +- **CI (new `web` job):** `pnpm install` → `tsc` typecheck → eslint → `vitest run` → + `vite build` → **bundle-size check (initial JS ≤ 150 KB gz)**. + +## Acceptance criteria (Milestone 1 "done") + +1. `pnpm dev` + the running server: can log in, see a paginated object list, select a + row, read its detail (incl. flexible-field values), switch sv/en, and log out. +2. Unauthenticated access to a protected route redirects to `/login`; a 401 mid-session + bounces to `/login`. +3. `cargo build -p server --features embed-web` (after `pnpm build`) produces a binary + that serves the SPA at `/` with working client-side routing; backend `cargo test` + still passes **without** a built frontend. +4. `web` CI job green: typecheck, lint, tests, build, bundle-size within budget. +5. Later nav items are visible but disabled. + +## Out of scope / follow-ups + +- Playwright e2e smoke (later milestone, once `--seed` data exists; relates to issue #14). +- Object create/edit/delete + dynamic field forms (Milestone 2). +- Any write operations from the UI (M1 is read-only beyond auth).