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>
This commit is contained in:
@@ -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 + <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).
|
||||
Reference in New Issue
Block a user