Files
biggus-dickus/docs/superpowers/specs/2026-06-03-frontend-spa-milestone-1-design.md
T
logaritmisk 31e2a3f30a docs(spec): frontend SPA milestone 1 (foundation slice) design
Decomposes the admin SPA into milestones; specs M1 — web/ scaffold,
Vite+React+TS+pnpm+shadcn/ui, openapi-typescript+openapi-fetch typed
client, TanStack Query, react-i18next (sv/en), two-pane master-detail
layout, login/session guard, read-only Objects browse, Vitest+RTL+MSW
tests, memory-serve embed behind a feature gate, 150KB bundle budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:50:30 +02:00

179 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 **masterdetail** — 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 + <Outlet/>
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).