11 KiB
phase: 12-initial-setup-wizard plan: 04 subsystem: pwa, ui, api-client tags: [react, vite, tanstack-query, tdd, setup-wizard, oidc, playwright]
Dependency graph
requires:
- phase: 12-02 provides: /api/setup/* routes (7 handlers, pre-auth mount)
- phase: 12-03 provides: first-login-claims (upsertUser D-08) provides:
- apps/pwa/src/api/client.ts — 7 setup API functions + SetupAlreadyLockedError
- apps/pwa/src/routes/SetupPage.tsx — standalone 4-step wizard + Terminal/Locked screens
- apps/pwa/src/App.tsx — setupQuery gate + /setup route + redirect when unconfigured
- apps/pwa/src/App.test.tsx — gate tests (both branches)
- apps/pwa/src/routes/SetupPage.test.tsx — wizard unit tests
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md — revised (done in prior session
0f3c378) affects: - first-run operator experience (SETUP-01/SETUP-02)
Tech tracking
tech-stack:
added: [] # Zero new packages
patterns:
- TDD RED/GREEN cycle — SetupPage.test.tsx (RED gate eb84e6e) → SetupPage.tsx (GREEN 62d80f6)
- setupQuery (staleTime: 0) alongside meQuery — always-fresh setup gate (mirrors D-10 spirit)
- alreadyLocked prop pattern — SetupPage accepts prop to directly render Surface 8 (testable)
- window.history.pushState({}, '', '/') in beforeEach — URL isolation between BrowserRouter tests
- nested inside route element — outer * route contains inner app-shell routes
key-files:
created:
- apps/pwa/src/routes/SetupPage.tsx
- apps/pwa/src/routes/SetupPage.test.tsx
- apps/pwa/src/App.test.tsx
modified:
- apps/pwa/src/api/client.ts
- apps/pwa/src/App.tsx
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md (prior session 0f3c378)
key-decisions:
- "ALREADYLOCKED-PROP: SetupPage accepts alreadyLocked?: boolean prop to render Surface 8 directly — enables unit tests without needing a live 423 response; also handles the runtime case where any setup API call returns 423 mid-wizard"
- "NESTED-ROUTES: App.tsx uses outer containing inner to implement the gate — the /setup route is at the outer level (pre-gate) so it renders standalone before the gate logic runs"
- "URL-ISOLATION: window.history.pushState({}, '', '/') in beforeEach resets BrowserRouter URL state between tests (jsdom shares window.location across tests in the same file)"
Metrics
duration: 32min completed: 2026-06-15
Phase 12 Plan 04: PWA Setup Wizard Summary
Setup wizard PWA side: 7 API client functions, standalone 4-step SetupPage, App.tsx gate + /setup route; TDD; 236 tests pass; playwright-cli partial smoke pass
Performance
- Duration: 32 min
- Started: 2026-06-15T18:20:37Z
- Completed: 2026-06-15T18:55:00Z (at checkpoint Task 4)
- Tasks completed: 3 of 4 (Task 4 is a human-verify checkpoint)
- Files modified: 5
Accomplishments
Task 1: UI-SPEC Revision (pre-existing, 0f3c378)
The UI-SPEC was revised in a prior planning session (commit 0f3c378). Verified all acceptance criteria pass:
- No
/api/setup/generatereferences (Generate Secrets step dropped per D-05) - Input fields for
oidc_issuer,oidc_client_id,vapid_public_key,app_external_urlpresent - Design system sections retained (Design System, Spacing Scale, Accessibility Contract)
- Step indicator re-numbered to 4 steps (Welcome / Instance / Calendar / Complete)
Task 2: Setup API Client + SetupPage Wizard (TDD RED/GREEN)
RED gate (eb84e6e): 17 failing tests covering all 7 API function exports and SetupPage rendering.
GREEN (62d80f6): Implemented:
fetchSetupStatus()— GETs/api/setup/status; no credentials/redirect:manual (pre-auth endpoint)postSetupConfig(payload)— POSTs non-secret config (app_url, oidc_issuer, oidc_client_id, vapid_public_key)validateSetupDb()— POSTs/api/setup/validate/db; typed error message on failurevalidateSetupOidc()— POSTs/api/setup/validate/oidc; typed error message on failurevalidateSetupVapid()— POSTs/api/setup/validate/vapid; typed error message on failurepostSetupCredential(payload)— POSTs fastmailEmail + appPassword to/api/setup/credentialpostSetupComplete()— POSTs/api/setup/complete; throws SetupAlreadyLockedError on 423SetupAlreadyLockedError— typed error class for 423 responses
SetupPage.tsx:
- Standalone full-page wizard — no AppNav/BottomTabBar imports
role="main"on content column;aria-live="polite"on validation rows- 4 sub-components: StepIndicator, ValidationRow, ActionRow, step cards
- Step 1 (Welcome): orientation text, "Before you start" note block, Continue button
- Step 2 (Instance Configuration): 4 fields (App URL, OIDC issuer, client_id, VAPID public key); Save & Validate triggers sequential DB+OIDC validation; Continue appears only when both pass
- Step 3 (Calendar Credential): email+password fields; CalDAV validation; Complete Setup button
- Surface 7 (Terminal): ShieldCheck icon, "Setup complete" heading, Sign in link
- Surface 8 (Already Locked): via
alreadyLockedprop or any 423 response mid-wizard - All copy is plain-text JSX children — no HTML injection
- Focus management:
stepHeadingRef.current.focus()on step change (a11y)
Task 3: App.tsx Gate + /setup Route (1587bca)
- Added
setupQuery = useQuery({ queryKey: ['setupStatus'], queryFn: fetchSetupStatus, retry: false, staleTime: 0 }) - Added
<Route path="/setup" element={<SetupPage />} />at the outer Routes level (pre-gate) - Redirect gate:
setupLoading → <div aria-hidden>|setupComplete===false → <Navigate to="/setup">|true → full app shell /setuproute renders standalone — AppNav/BottomTabBar only render inside thesetupComplete===truebranch
App.test.tsx:
setupComplete: false→ SetupPage renders, AppNav absent ✓setupComplete: true→ CalendarShell renders, AppNav present ✓- Loading state → CalendarShell absent (no flash) ✓
Task 4: playwright-cli Partial Smoke (CHECKPOINT — awaiting human)
playwright-cli Chromium verified (automated):
/setuprenders standalone wizard:role="main", h1 "FamilySync Setup", 4-step indicator ✓- No
<nav>element on the wizard page ✓ - Welcome step: heading, pre-start block, Continue button ✓
- Clicking Continue advances to step 2 (Instance Configuration) ✓
- Step 2: all 4 input fields with correct placeholders; Save & Validate button; Back button ✓
- Step 1 shows completed checkmark in step indicator ✓
Screenshots saved:
.planning/phases/12-initial-setup-wizard/screenshot-setup-welcome.png.planning/phases/12-initial-setup-wizard/screenshot-setup-step2-config.png
Cannot be automated (requires human):
- The Docker API container needs
docker compose build && docker compose up -dto pick up Phase 12 Plan 02 setup routes — the running container is from an older build withoutsetup.js - Fresh/unconfigured DB (no
setup_complete, nomember_credentials) needed to verify the automatic redirect-to-/setup flow - Live OIDC discovery validation (requires running Authelia reachable from the container)
- Live CalDAV credential validation (requires real Fastmail app password)
Task Commits
- Task 1: UI-SPEC revision —
0f3c378(prior session — docs) - Task 2 RED: failing tests —
eb84e6e(test) - Task 2 GREEN: client.ts + SetupPage —
62d80f6(feat) - Task 3: App.tsx gate + tests —
1587bca(feat) - Task 4: CHECKPOINT — awaiting human verification
Files Created/Modified
apps/pwa/src/api/client.ts— 7 setup functions + SetupAlreadyLockedError appendedapps/pwa/src/routes/SetupPage.tsx— new (standalone wizard, 5 surfaces, 530 lines)apps/pwa/src/routes/SetupPage.test.tsx— new (17 tests, RED gate + implementation tests)apps/pwa/src/App.tsx— setupQuery + /setup route + redirect gate addedapps/pwa/src/App.test.tsx— new (6 tests covering both gate branches).planning/phases/12-initial-setup-wizard/12-UI-SPEC.md— revised (prior session0f3c378)
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] require('./SetupPage.js') pattern incompatible with Vitest ESM mode
- Found during: Task 2 test execution
- Issue: RED test scaffolding used
require('./SetupPage.js')inside test functions to import after mocks — but in Vitest's ESM mode this resolves at runtime and cannot find the.tsxsource file - Fix: Changed to static
import { SetupPage } from './SetupPage.js'at the top of the test file (mocks are hoisted viavi.mockso static imports work correctly) - Files modified:
apps/pwa/src/routes/SetupPage.test.tsx - Commit:
62d80f6(Task 2 GREEN)
2. [Rule 1 - Bug] BrowserRouter URL state persists between tests in jsdom
- Found during: Task 3 App.test.tsx test run
- Issue:
setupComplete:falsetest redirected to/setup, leavingwindow.locationat/setupfor thesetupComplete:truetest. The/setuproute matched the standalone SetupPage instead of the CalendarShell. - Fix: Added
window.history.pushState({}, '', '/')inbeforeEachto reset URL to root before each test - Files modified:
apps/pwa/src/App.test.tsx - Commit:
1587bca(Task 3)
Known Stubs
None — all wizard steps render from live state (no hardcoded empty values). The validation steps (DB, OIDC, CalDAV) require a live API to produce success states; the component correctly shows pending/success/failure per actual API responses.
Threat Surface Scan
No new threat surface beyond what is explicitly modeled in the plan's threat_model:
- T-12-13 (wizard never handles secrets): mitigated — no VAPID_PRIVATE_KEY or SESSION_SECRET inputs
- T-12-14 (XSS via operator input): mitigated — no dangerouslySetInnerHTML in SetupPage.tsx (grep returns 0)
- T-12-15 (app password disclosure): mitigated — type="password", never stored client-side
- T-12-SC (new packages): mitigated — zero new npm packages
TDD Gate Compliance
- RED gate:
eb84e6etest commit (17 failing tests) — PRESENT ✓ - GREEN gate:
62d80f6feat commit (all tests pass) — PRESENT ✓ - REFACTOR: no refactoring commit needed (implementation was clean on first pass)
Self-Check: PASSED
Files exist:
apps/pwa/src/api/client.ts— FOUNDapps/pwa/src/routes/SetupPage.tsx— FOUNDapps/pwa/src/routes/SetupPage.test.tsx— FOUNDapps/pwa/src/App.tsx— FOUNDapps/pwa/src/App.test.tsx— FOUND
Commits exist:
eb84e6e— FOUND62d80f6— FOUND1587bca— FOUND
Test suite: 236 passed | 0 failed TypeCheck: clean (0 errors) Build: green (dist/sw.js emitted) playwright-cli: partial smoke pass (steps 1-2 verified headlessly; steps 3-4 require operator rebuild + fresh DB)