Phase 12: Initial Setup Wizard #22

Merged
luckberg merged 76 commits from gsd/phase-12-initial-setup-wizard into main 2026-06-16 19:10:33 -04:00
3 changed files with 89 additions and 40 deletions
Showing only changes of commit 7a1801f47d - Show all commits
+3 -3
View File
@@ -3,8 +3,8 @@ gsd_state_version: 1.0
milestone: v1.1 milestone: v1.1
milestone_name: Operability & Polish milestone_name: Operability & Polish
status: verifying status: verifying
stopped_at: Phase 12 Plan 03 complete — first-login-claims implemented; plan 4 of 4 is next stopped_at: Phase 12 Plan 04 Tasks 1-3 complete — paused at Task 4 human-verify checkpoint (Docker rebuild + fresh-DB e2e verification needed)
last_updated: "2026-06-15T18:35:51.373Z" last_updated: "2026-06-15T18:51:36.012Z"
last_activity: 2026-06-15 last_activity: 2026-06-15
progress: progress:
total_phases: 24 total_phases: 24
@@ -264,7 +264,7 @@ Recent decisions affecting current work:
## Session Continuity ## Session Continuity
Last session: 2026-06-15T18:35:51.354Z Last session: 2026-06-15T18:51:35.993Z
Stopped at: Phase 12 Plan 04 Tasks 1-3 complete — paused at Task 4 human-verify checkpoint (Docker rebuild + fresh-DB e2e verification needed) Stopped at: Phase 12 Plan 04 Tasks 1-3 complete — paused at Task 4 human-verify checkpoint (Docker rebuild + fresh-DB e2e verification needed)
Resume file: None Resume file: None
@@ -16,6 +16,7 @@ provides:
- apps/pwa/src/App.tsx — setupQuery gate + /setup route + redirect when unconfigured - 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/App.test.tsx — gate tests (both branches)
- apps/pwa/src/routes/SetupPage.test.tsx — wizard unit tests - 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) - .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md — revised (done in prior session 0f3c378)
affects: affects:
- first-run operator experience (SETUP-01/SETUP-02) - first-run operator experience (SETUP-01/SETUP-02)
@@ -27,14 +28,17 @@ tech-stack:
- TDD RED/GREEN cycle — SetupPage.test.tsx (RED gate eb84e6e) → SetupPage.tsx (GREEN 62d80f6) - 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) - 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) - alreadyLocked prop pattern — SetupPage accepts prop to directly render Surface 8 (testable)
- `window.history.pushState({}, '', '/')` in beforeEach — URL isolation between BrowserRouter tests - window.history.pushState({}, '', '/') in beforeEach — URL isolation between BrowserRouter tests
- nested <Routes> inside route element — outer * route contains inner app-shell routes - 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]
key-files: key-files:
created: created:
- apps/pwa/src/routes/SetupPage.tsx - apps/pwa/src/routes/SetupPage.tsx
- apps/pwa/src/routes/SetupPage.test.tsx - apps/pwa/src/routes/SetupPage.test.tsx
- apps/pwa/src/App.test.tsx - apps/pwa/src/App.test.tsx
- apps/pwa/src/api/setupClient.contract.test.ts
modified: modified:
- apps/pwa/src/api/client.ts - apps/pwa/src/api/client.ts
- apps/pwa/src/App.tsx - apps/pwa/src/App.tsx
@@ -44,23 +48,25 @@ 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" - "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" - "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)" - "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"
# Metrics # Metrics
duration: 32min duration: 50min
completed: 2026-06-15 completed: 2026-06-15
--- ---
# Phase 12 Plan 04: PWA Setup Wizard Summary # 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** **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 ## Performance
- **Duration:** 32 min - **Duration:** 50 min
- **Started:** 2026-06-15T18:20:37Z - **Started:** 2026-06-15T18:20:37Z
- **Completed:** 2026-06-15T18:55:00Z (at checkpoint Task 4) - **Completed:** 2026-06-15T18:52:00Z
- **Tasks completed:** 3 of 4 (Task 4 is a human-verify checkpoint) - **Tasks completed:** 4 of 4 (Task 4 verified to no-credential boundary; Fastmail step reserved for human)
- **Files modified:** 5 - **Files modified:** 6
## Accomplishments ## Accomplishments
@@ -77,7 +83,7 @@ The UI-SPEC was revised in a prior planning session (commit 0f3c378). Verified a
**GREEN (62d80f6):** Implemented: **GREEN (62d80f6):** Implemented:
- `fetchSetupStatus()` — GETs `/api/setup/status`; no credentials/redirect:manual (pre-auth endpoint) - `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) - `postSetupConfig(payload)` — POSTs non-secret config (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey)
- `validateSetupDb()` — POSTs `/api/setup/validate/db`; typed error message on failure - `validateSetupDb()` — POSTs `/api/setup/validate/db`; typed error message on failure
- `validateSetupOidc()` — POSTs `/api/setup/validate/oidc`; 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 - `validateSetupVapid()` — POSTs `/api/setup/validate/vapid`; typed error message on failure
@@ -109,25 +115,48 @@ The UI-SPEC was revised in a prior planning session (commit 0f3c378). Verified a
- `setupComplete: true` → CalendarShell renders, AppNav present ✓ - `setupComplete: true` → CalendarShell renders, AppNav present ✓
- Loading state → CalendarShell absent (no flash) ✓ - Loading state → CalendarShell absent (no flash) ✓
### Task 4: playwright-cli Partial Smoke (CHECKPOINT — awaiting human) ### Task 4: Bug Fixes + playwright-cli Full No-Credential Verification
playwright-cli Chromium verified (automated): #### BUG 1 — Field-name contract mismatch (FIXED, 120ce85)
- `/setup` renders 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: **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.
- `.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):** **Fix:**
1. The Docker API container needs `docker compose build && docker compose up -d` to pick up Phase 12 Plan 02 setup routes — the running container is from an older build without `setup.js` - `client.ts`: Renamed `SetupConfigPayload` interface fields to camelCase matching the API contract
2. Fresh/unconfigured DB (no `setup_complete`, no `member_credentials`) needed to verify the automatic redirect-to-/setup flow - `SetupPage.tsx`: Updated `handleSaveAndValidate` call to `configMutation.mutate({ appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey })`
3. Live OIDC discovery validation (requires running Authelia reachable from the container)
4. Live CalDAV credential validation (requires real Fastmail app password) **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 ## Task Commits
@@ -135,13 +164,15 @@ Screenshots saved:
2. **Task 2 RED: failing tests**`eb84e6e` (test) 2. **Task 2 RED: failing tests**`eb84e6e` (test)
3. **Task 2 GREEN: client.ts + SetupPage**`62d80f6` (feat) 3. **Task 2 GREEN: client.ts + SetupPage**`62d80f6` (feat)
4. **Task 3: App.tsx gate + tests**`1587bca` (feat) 4. **Task 3: App.tsx gate + tests**`1587bca` (feat)
5. **Task 4: CHECKPOINT** — awaiting human verification 5. **Task 4 RED: contract regression tests**`9f20c8b` (test)
6. **Task 4 GREEN: BUG 1+2 fixes**`120ce85` (fix)
## Files Created/Modified ## Files Created/Modified
- `apps/pwa/src/api/client.ts` — 7 setup functions + SetupAlreadyLockedError appended - `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, 530 lines) - `apps/pwa/src/routes/SetupPage.tsx` — new (standalone wizard, 5 surfaces, 530 lines); camelCase mutation payload fix
- `apps/pwa/src/routes/SetupPage.test.tsx` — new (17 tests, RED gate + implementation tests) - `apps/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 added - `apps/pwa/src/App.tsx` — setupQuery + /setup route + redirect gate added
- `apps/pwa/src/App.test.tsx` — new (6 tests covering both gate branches) - `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) - `.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md` — revised (prior session 0f3c378)
@@ -164,6 +195,20 @@ Screenshots saved:
- **Files modified:** `apps/pwa/src/App.test.tsx` - **Files modified:** `apps/pwa/src/App.test.tsx`
- **Commit:** `1587bca` (Task 3) - **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)
## Known Stubs ## 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. 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.
@@ -178,9 +223,11 @@ No new threat surface beyond what is explicitly modeled in the plan's threat_mod
## TDD Gate Compliance ## TDD Gate Compliance
- RED gate: `eb84e6e` test commit (17 failing tests) — PRESENT - RED gate: `eb84e6e` test commit (17 failing tests — Task 2) — PRESENT
- GREEN gate: `62d80f6` feat commit (all tests pass) — PRESENT - GREEN gate: `62d80f6` feat commit (all tests pass — Task 2) — PRESENT
- REFACTOR: no refactoring commit needed (implementation was clean on first pass) - 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
- REFACTOR: no refactoring commit needed
## Self-Check: PASSED ## Self-Check: PASSED
@@ -188,15 +235,17 @@ Files exist:
- `apps/pwa/src/api/client.ts` — FOUND - `apps/pwa/src/api/client.ts` — FOUND
- `apps/pwa/src/routes/SetupPage.tsx` — FOUND - `apps/pwa/src/routes/SetupPage.tsx` — FOUND
- `apps/pwa/src/routes/SetupPage.test.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.tsx` — FOUND
- `apps/pwa/src/App.test.tsx` — FOUND - `apps/pwa/src/App.test.tsx` — FOUND
Commits exist: Commits verified:
- `eb84e6e`FOUND - `eb84e6e`Task 2 RED
- `62d80f6`FOUND - `62d80f6`Task 2 GREEN
- `1587bca`FOUND - `1587bca`Task 3
- `9f20c8b` — Task 4 RED
- `120ce85` — Task 4 GREEN
Test suite: 236 passed | 0 failed Test suite: 245 passed | 0 failed
TypeCheck: clean (0 errors) TypeCheck: clean (0 errors)
Build: green (dist/sw.js emitted) playwright-cli: /config 200 confirmed; redirect gate confirmed; DB validation 200; OIDC 400 (expected — Authelia unreachable)
playwright-cli: partial smoke pass (steps 1-2 verified headlessly; steps 3-4 require operator rebuild + fresh DB)
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB