Files
familysync/.planning/milestones/v1.1-phases/12-initial-setup-wizard/12-04-SUMMARY.md
T
2026-06-18 22:21:38 -04:00

17 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions duration completed
12-initial-setup-wizard 04 pwa, ui, api-client
react
vite
tanstack-query
tdd
setup-wizard
oidc
playwright
phase provides
12-02 /api/setup/* routes (7 handlers, pre-auth mount)
phase provides
12-03 first-login-claims (upsertUser D-08)
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
apps/pwa/src/api/setupClient.contract.test.ts — contract regression tests (BUG 1+2 guards)
.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md — revised (done in prior session 0f3c378)
first-run operator experience (SETUP-01/SETUP-02)
added 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 <Routes> inside route element — outer * route contains inner app-shell routes
camelCase API contract enforcement — SetupConfigPayload fields match API configSchema exactly
ZodError object-to-string extraction — issues[0].message extracted to prevent [object Object]
created modified
apps/pwa/src/routes/SetupPage.tsx
apps/pwa/src/routes/SetupPage.test.tsx
apps/pwa/src/App.test.tsx
apps/pwa/src/api/setupClient.contract.test.ts
apps/pwa/src/api/client.ts
apps/pwa/src/App.tsx
.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md (prior session 0f3c378)
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 <Route path='*'> containing inner <Routes> 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)
CAMELCASE-CONTRACT: SetupConfigPayload interface renamed to camelCase (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey) to match the API configSchema exactly — the original snake_case interface caused every /config POST to return 400 ZodError
ZODERROR-EXTRACTION: postSetupConfig now extracts issues[0].message when body.error is an object; falls back to status code message when no issues — prevents [object Object] in UI
50min 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; 249 tests pass; playwright-cli no-credential smoke pass (/config 200 confirmed); VAPID validation wired (CR-01 closed, SETUP-02 satisfied)

Performance

  • Duration: 50 min (original) + gap closure (CR-01 fix, 2026-06-15T19:14Z)
  • Started: 2026-06-15T18:20:37Z
  • Completed: 2026-06-15T19:15:00Z (gap closed)
  • Tasks completed: 4 of 4 + gap closure (CR-01 VAPID wiring)
  • Files modified: 7 (includes gap closure)

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 (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey)
  • 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→VAPID validation; Continue appears only when ALL THREE pass (CR-01 gap closure)
  • 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 <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
  • /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: Bug Fixes + playwright-cli Full No-Credential Verification

BUG 1 — Field-name contract mismatch (FIXED, 120ce85)

Root cause: SetupConfigPayload interface had snake_case fields (app_url, oidc_issuer, oidc_client_id, vapid_public_key). The API's configSchema expects camelCase (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey). Every /config POST returned 400 ZodError.

Fix:

  • client.ts: Renamed SetupConfigPayload interface fields to camelCase matching the API contract
  • SetupPage.tsx: Updated handleSaveAndValidate call to configMutation.mutate({ appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey })

Verified: playwright-cli request-body 72 shows {"appExternalUrl":"...","oidcIssuer":"...","oidcClientId":"...","vapidPublicKey":"..."} — exact API contract match. Response: 200 OK.

BUG 2 — Error status renders [object Object] (FIXED, 120ce85)

Root cause: When /config returned 400, the response body error field was a ZodError object { name: "ZodError", issues: [...] }, not a string. The client did body.error ?? fallback which yielded the object, then new Error(object) → message "[object Object]".

Fix: client.ts postSetupConfig now:

  1. If body.error is a string: use it directly
  2. If body.error is an object with issues[0].message: extract that as the error message
  3. Otherwise: fall back to POST /api/setup/config failed: {status}

playwright-cli Verification (no-credential path):

Step Result
/ → redirect to /setup PASS (URL confirmed /setup)
Welcome step renders PASS (h1, 4-step indicator, Continue button)
Continue → Step 2 (Instance Configuration) PASS (all 4 fields render with correct placeholders)
Step 1 shows completion checkmark PASS (img element in step indicator)
Fill 4 fields + click "Save & Validate" PASS
POST /api/setup/config 200 OK (camelCase body verified via request-body)
DB validation 200 OK ("Database connection verified." row)
OIDC validation 400 Bad Request (Authelia unreachable from container — EXPECTED, ACCEPTABLE)
OIDC error display Readable string "OIDC discovery failed..." (no [object Object])
No [object Object] in UI PASS

Screenshot: .planning/phases/12-initial-setup-wizard/screenshot-setup-config-200-fixed.png

Cannot be automated (reserved for human):

  • Fastmail app password entry (Step 3 — CalDAV credential) requires real credentials
  • Live OIDC discovery validation (requires Authelia reachable from the container)
  • Final POST /api/setup/complete to flip setup_complete

Task Commits

  1. Task 1: UI-SPEC revision0f3c378 (prior session — docs)
  2. Task 2 RED: failing testseb84e6e (test)
  3. Task 2 GREEN: client.ts + SetupPage62d80f6 (feat)
  4. Task 3: App.tsx gate + tests1587bca (feat)
  5. Task 4 RED: contract regression tests9f20c8b (test)
  6. Task 4 GREEN: BUG 1+2 fixes120ce85 (fix)
  7. CR-01 RED: VAPID validation gate tests7d0205d (test)
  8. CR-01 GREEN: wire validateSetupVapid0d53249 (fix)

Files Created/Modified

  • apps/pwa/src/api/client.ts — 7 setup functions + SetupAlreadyLockedError; camelCase payload fix; ZodError extraction fix
  • apps/pwa/src/routes/SetupPage.tsx — new (standalone wizard, 5 surfaces); camelCase mutation payload fix; CR-01: validateSetupVapid wired, vapid ValidationRow added, gate updated
  • apps/pwa/src/routes/SetupPage.test.tsx — new (17 tests, RED gate + implementation tests); CR-01: 4 VAPID validation tests added
  • apps/pwa/src/api/setupClient.contract.test.ts — new (9 contract regression tests for BUG 1+2)
  • apps/pwa/src/App.tsx — setupQuery + /setup route + redirect gate added
  • apps/pwa/src/App.test.tsx — new (6 tests covering both gate branches)
  • .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md — revised (prior session 0f3c378)

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 .tsx source file
  • Fix: Changed to static import { SetupPage } from './SetupPage.js' at the top of the test file (mocks are hoisted via vi.mock so 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:false test redirected to /setup, leaving window.location at /setup for the setupComplete:true test. The /setup route matched the standalone SetupPage instead of the CalendarShell.
  • Fix: Added window.history.pushState({}, '', '/') in beforeEach to reset URL to root before each test
  • Files modified: apps/pwa/src/App.test.tsx
  • Commit: 1587bca (Task 3)

3. [Rule 1 - Bug] BUG 1 — SetupConfigPayload snake_case vs API camelCase mismatch

  • Found during: Task 4 human-verify checkpoint (returned as blocking bug)
  • Issue: SetupConfigPayload interface used snake_case field names (app_url, oidc_issuer, oidc_client_id, vapid_public_key). API configSchema requires camelCase (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey). Every /api/setup/config POST returned 400 ZodError, blocking wizard completion.
  • Fix: Renamed interface fields + updated SetupPage mutation call to use camelCase
  • Files modified: apps/pwa/src/api/client.ts, apps/pwa/src/routes/SetupPage.tsx
  • Commit: 120ce85 (Task 4 GREEN)

4. [Rule 1 - Bug] BUG 2 — ZodError object serializes as [object Object] in error message

  • Found during: Task 4 human-verify checkpoint (returned as blocking bug)
  • Issue: When API returns { error: { name: "ZodError", issues: [...] } }, postSetupConfig did body.error ?? fallback yielding the ZodError object, then new Error(object)"[object Object]" in UI
  • Fix: Extract issues[0].message from ZodError object; fall back to string error if present; fall back to status code
  • Files modified: apps/pwa/src/api/client.ts
  • Commit: 120ce85 (Task 4 GREEN)

5. [CR-01 Gap Closure] SETUP-02 — validateSetupVapid never called in wizard (BLOCKER)

  • Found during: Phase 12 verification (12-VERIFICATION.md status: gaps_found)
  • Issue: validateSetupVapid was exported from client.ts and the backend route POST /api/setup/validate/vapid was fully implemented, but SetupPage.tsx Step2Config never imported or called it. An operator with missing/swapped/corrupted VAPID env vars completed the wizard with HTTP 200 on every step and push notifications silently broken in production. REQUIREMENTS.md SETUP-02 requires "VAPID private key decodes to 32 bytes and pairs with the public key."
  • Fix:
    • Import validateSetupVapid in SetupPage.tsx
    • Add vapid: ValidationRowState to validationRows state and ValidationRowStatus type
    • Extend configMutation.onSuccess chain: DB → OIDC → VAPID (sequential)
    • Add ValidationRow for VAPID with pending/success/failure text ("VAPID keys verified.")
    • Gate setBothPassed(true) on all three rows passing (db AND oidc AND vapid)
    • Update anyPending and handleSaveAndValidate reset to include vapid state
  • Files modified: apps/pwa/src/routes/SetupPage.tsx, apps/pwa/src/routes/SetupPage.test.tsx
  • Commits: 7d0205d (RED), 0d53249 (GREEN)

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: eb84e6e test commit (17 failing tests — Task 2) — PRESENT
  • GREEN gate: 62d80f6 feat commit (all tests pass — Task 2) — PRESENT
  • RED gate: 9f20c8b test commit (2 failing contract tests — Task 4 BUG 2) — PRESENT
  • GREEN gate: 120ce85 fix commit (all 245 tests pass — Task 4) — PRESENT
  • RED gate: 7d0205d test commit (3 failing VAPID tests — CR-01 gap) — PRESENT
  • GREEN gate: 0d53249 fix commit (all 249 tests pass — CR-01 gap closure) — PRESENT
  • REFACTOR: no refactoring commit needed

Self-Check: PASSED

Files exist:

  • apps/pwa/src/api/client.ts — FOUND
  • apps/pwa/src/routes/SetupPage.tsx — FOUND
  • apps/pwa/src/routes/SetupPage.test.tsx — FOUND
  • apps/pwa/src/api/setupClient.contract.test.ts — FOUND
  • apps/pwa/src/App.tsx — FOUND
  • apps/pwa/src/App.test.tsx — FOUND

Commits verified:

  • eb84e6e — Task 2 RED
  • 62d80f6 — Task 2 GREEN
  • 1587bca — Task 3
  • 9f20c8b — Task 4 RED
  • 120ce85 — Task 4 GREEN
  • 7d0205d — CR-01 RED (VAPID tests)
  • 0d53249 — CR-01 GREEN (VAPID wired)

Test suite: 249 passed | 0 failed TypeCheck: clean (0 errors) playwright-cli: /config 200 confirmed; redirect gate confirmed; DB validation 200; OIDC 400 (expected — Authelia unreachable from container); VAPID endpoint live (curl POST /api/setup/validate/vapid returns 200); VAPID row wired in Step 2 chain