--- 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/generate` references (Generate Secrets step dropped per D-05) - Input fields for `oidc_issuer`, `oidc_client_id`, `vapid_public_key`, `app_external_url` present - 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 failure - `validateSetupOidc()` — POSTs `/api/setup/validate/oidc`; typed error message on failure - `validateSetupVapid()` — POSTs `/api/setup/validate/vapid`; typed error message on failure - `postSetupCredential(payload)` — POSTs fastmailEmail + appPassword to `/api/setup/credential` - `postSetupComplete()` — POSTs `/api/setup/complete`; throws SetupAlreadyLockedError on 423 - `SetupAlreadyLockedError` — 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 `alreadyLocked` prop 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 `} />` at the outer Routes level (pre-gate) - Redirect gate: `setupLoading →
` | `setupComplete===false → ` | `true → full app shell` - `/setup` route renders standalone — AppNav/BottomTabBar only render inside the `setupComplete===true` branch **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): - `/setup` renders standalone wizard: `role="main"`, h1 "FamilySync Setup", 4-step indicator ✓ - No `