Update SUMMARY.md with bug fix details, TDD gate compliance, and playwright-cli verification confirming /api/setup/config returns 200 with camelCase payload. Update STATE.md session info. Phase 12 all 4 plans complete.
15 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 |
|
|
|
|
|
|
|
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; 245 tests pass; playwright-cli full no-credential smoke pass (/config 200 confirmed)
Performance
- Duration: 50 min
- Started: 2026-06-15T18:20:37Z
- Completed: 2026-06-15T18:52:00Z
- Tasks completed: 4 of 4 (Task 4 verified to no-credential boundary; Fastmail step reserved for human)
- Files modified: 6
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 (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey)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: 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: RenamedSetupConfigPayloadinterface fields to camelCase matching the API contractSetupPage.tsx: UpdatedhandleSaveAndValidatecall toconfigMutation.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:
- If
body.erroris a string: use it directly - If
body.erroris an object withissues[0].message: extract that as the error message - 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/completeto flip setup_complete
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 RED: contract regression tests —
9f20c8b(test) - Task 4 GREEN: BUG 1+2 fixes —
120ce85(fix)
Files Created/Modified
apps/pwa/src/api/client.ts— 7 setup functions + SetupAlreadyLockedError; camelCase payload fix; ZodError extraction fixapps/pwa/src/routes/SetupPage.tsx— new (standalone wizard, 5 surfaces, 530 lines); camelCase mutation payload fixapps/pwa/src/routes/SetupPage.test.tsx— new (17 tests, RED gate + implementation tests)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 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)
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:
SetupConfigPayloadinterface used snake_case field names (app_url,oidc_issuer,oidc_client_id,vapid_public_key). APIconfigSchemarequires camelCase (appExternalUrl,oidcIssuer,oidcClientId,vapidPublicKey). Every/api/setup/configPOST 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: [...] } },postSetupConfigdidbody.error ?? fallbackyielding the ZodError object, thennew Error(object)→"[object Object]"in UI - Fix: Extract
issues[0].messagefrom ZodError object; fall back to stringerrorif present; fall back to status code - Files modified:
apps/pwa/src/api/client.ts - Commit:
120ce85(Task 4 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:
eb84e6etest commit (17 failing tests — Task 2) — PRESENT - GREEN gate:
62d80f6feat commit (all tests pass — Task 2) — PRESENT - RED gate:
9f20c8btest commit (2 failing contract tests — Task 4 BUG 2) — PRESENT - GREEN gate:
120ce85fix commit (all 245 tests pass — Task 4) — PRESENT - REFACTOR: no refactoring commit needed
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/api/setupClient.contract.test.ts— FOUNDapps/pwa/src/App.tsx— FOUNDapps/pwa/src/App.test.tsx— FOUND
Commits verified:
eb84e6e— Task 2 RED62d80f6— Task 2 GREEN1587bca— Task 39f20c8b— Task 4 RED120ce85— Task 4 GREEN
Test suite: 245 passed | 0 failed TypeCheck: clean (0 errors) playwright-cli: /config 200 confirmed; redirect gate confirmed; DB validation 200; OIDC 400 (expected — Authelia unreachable)