Files
biggus-dickus/docs/superpowers/specs/2026-06-03-frontend-spa-milestone-1-design.md
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

8.5 KiB
Raw Permalink Blame History

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 buildweb/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. /objectsuseObjectsPage(limit, offset)GET /api/admin/objects (the paginated admin list already built). Left pane renders rows; selecting one routes to /objects/:id.
  3. /objects/:iduseObject(id)GET /api/admin/objects/{id}. Right pane renders inventory-minimum fields + flexible-field values + visibility badge; 404 → not-found state.
  4. Login: useLoginPOST /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 installtsc typecheck → eslint → vitest runvite buildbundle-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).