---
phase: 12-initial-setup-wizard
plan: 04
type: execute
wave: 3
depends_on: ["12-02"]
files_modified:
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
- apps/pwa/src/api/client.ts
- apps/pwa/src/routes/SetupPage.tsx
- apps/pwa/src/App.tsx
- apps/pwa/src/App.test.tsx
autonomous: false
requirements: [SETUP-01, SETUP-02]
must_haves:
truths:
- "On a fresh instance (GET /api/setup/status → {setupComplete:false}), the app redirects to /setup and renders the wizard with no AppNav/BottomTabBar"
- "The revised wizard collects non-secret config (OIDC issuer/client_id, VAPID public key, app URL) as input fields, then validates DB/OIDC/VAPID/CalDAV before completing"
- "There is no in-wizard secret-generation step (D-05 — generation is the repo helper, pre-boot)"
- "Completing the wizard (POST /api/setup/complete) shows the terminal 'Setup complete' screen with a Sign in link to /"
- "Navigating to /setup after completion (423) renders the 'Already Locked' screen"
- "When setupComplete:true, normal app boot proceeds (no /setup redirect)"
artifacts:
- path: ".planning/phases/12-initial-setup-wizard/12-UI-SPEC.md"
provides: "Revised Wizard-Steps + Interaction-Contract (Step 2 dropped, Steps 3/4 collect config)"
contains: "config"
- path: "apps/pwa/src/routes/SetupPage.tsx"
provides: "The standalone multi-step wizard component"
min_lines: 80
- path: "apps/pwa/src/App.tsx"
provides: "setup-status gate + /setup route"
contains: "setup"
key_links:
- from: "apps/pwa/src/App.tsx"
to: "/api/setup/status"
via: "setupQuery on load → redirect to /setup when unconfigured"
pattern: "setup/status|setupStatus"
- from: "apps/pwa/src/routes/SetupPage.tsx"
to: "/api/setup/* (config, validate, credential, complete)"
via: "TanStack Query mutations"
pattern: "setup/(config|validate|credential|complete)"
---
Deliver the PWA side of the wizard: revise `12-UI-SPEC.md` (drop the Generate-Secrets step per D-05;
make the OIDC/VAPID step collect non-secret config inputs per D-02), build `SetupPage.tsx` (the
standalone full-page wizard following the revised UI-SPEC and the AdminPage/CredentialSheet patterns),
add the App.tsx setup-status gate + `/setup` route, and wire the `apps/pwa/src/api/client.ts` setup
client functions. Verify the flow with playwright-cli (desktop Chromium) per the CLAUDE.md convention.
Purpose: This is the operator-facing surface that closes SETUP-01 (guided bootstrap instead of
hand-editing files) and surfaces SETUP-02's per-input validation. The API routes (Plan 02) are the
contract this consumes.
Output: A working /setup wizard, the App-level gate, and a revised UI-SPEC matching D-02/D-04/D-05.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-initial-setup-wizard/12-CONTEXT.md
@.planning/phases/12-initial-setup-wizard/12-RESEARCH.md
@.planning/phases/12-initial-setup-wizard/12-PATTERNS.md
@.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
@apps/pwa/src/routes/AdminPage.tsx
@apps/pwa/src/components/CredentialSheet.tsx
@apps/pwa/src/App.tsx
@apps/pwa/src/api/client.ts
## Artifacts this phase produces (Plan 04 portion)
- Revised `12-UI-SPEC.md`: Step 2 (Generate Secrets) dropped; the OIDC/VAPID step gains input fields for oidc_issuer/oidc_client_id/vapid_public_key (+ app URL); 4-step flow (Welcome / Config / Validate / Credential — or planner-chosen equivalent) consistent with D-02/D-04/D-05
- `apps/pwa/src/api/client.ts`: `fetchSetupStatus`, `postSetupConfig`, `validateSetupDb/Oidc/Vapid`, `postSetupCredential`, `postSetupComplete`
- `apps/pwa/src/routes/SetupPage.tsx`: standalone wizard (no AppNav/BottomTabBar), Surfaces 1-8 per the revised UI-SPEC, plain-text JSX (no dangerouslySetInnerHTML)
- `apps/pwa/src/App.tsx`: `setupQuery` on /api/setup/status (staleTime 0) + `/setup` route + redirect gate when `setupComplete:false`
Task 1: Revise 12-UI-SPEC.md (drop Generate-Secrets; config-collect inputs per D-02/D-04/D-05)
.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md (the file being revised — §Surface 2 step labels, §Surface 4 Generated-Secret block, §Wizard Steps, §Copywriting Contract)
- .planning/phases/12-initial-setup-wizard/12-RESEARCH.md §UI-SPEC Revision Requirements (the authoritative table of what changes vs stays)
- .planning/phases/12-initial-setup-wizard/12-CONTEXT.md D-02/D-04/D-05 + the ⚠ Supersedes notes
Revise ONLY the Wizard-Steps, Interaction-Contract, Step-Indicator labels, Surface-4, and
Copywriting sections per RESEARCH.md §UI-SPEC Revision Requirements. DROP Step 2 "Generate Secrets"
entirely (no Secret Blocks, no acknowledgement checkboxes, no POST /api/setup/generate — generation
is the pre-boot repo helper, D-05); remove the Surface-4 Generated-Secret-Block section (or mark it
removed). Re-number the step indicator to the revised set (planner's call per CONTEXT discretion,
e.g. Welcome / Config / Validate / Credential — 4 steps). Convert the OIDC/VAPID step to COLLECT
non-secret config via input fields (oidc_issuer, oidc_client_id, vapid_public_key, app_external_url)
that POST to /api/setup/config, THEN validate (D-02). Update Step-1 description copy to remove the
"copy of docker-compose.yml to paste generated secrets into" reference. Leave the design system,
tokens, spacing, typography, color, a11y contract, security display rules, the Credential step, and
the Terminal/Locked screens UNCHANGED — do NOT re-derive the design system.
- source: the Generated-Secrets step is gone (`grep -ic "Generate Secrets\|Generated Secrets" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md` returns 0, or any remaining hit is explicitly marked "REMOVED")
- source: the OIDC/config step now references input fields for the config keys (`grep -Ec "oidc_issuer|oidc_client_id|vapid_public_key|app_external_url|/api/setup/config" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md` >= 2)
- source: no in-wizard generate endpoint (`grep -c "/api/setup/generate" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md` returns 0)
- source: design-system sections retained (`grep -c "Design System\|Spacing Scale\|Accessibility Contract" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md` >= 3)
! grep -iq "/api/setup/generate" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md && grep -Eq "oidc_issuer|/api/setup/config" .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
UI-SPEC steps revised: no generate-secrets step, config-collect inputs for the OIDC/VAPID step, step indicator re-numbered; design system untouched.
Task 2: Setup API client + SetupPage wizard component
apps/pwa/src/api/client.ts, apps/pwa/src/routes/SetupPage.tsx
- apps/pwa/src/api/client.ts (the file being extended — fetchMe l.74, saveCredential l.429 patterns)
- apps/pwa/src/routes/AdminPage.tsx (analog: page component, useQuery/useMutation, section-label/button styles, PATTERNS.md §SetupPage.tsx)
- apps/pwa/src/components/CredentialSheet.tsx (analog: credential field layout, validation-state row, helper link, plain-text JSX — Step Credential reuses this exactly)
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md (the REVISED contract from Task 1 — surfaces, copy, a11y)
- .planning/phases/12-initial-setup-wizard/12-PATTERNS.md §SetupPage.tsx (imports, mutation, step-state patterns)
- fetchSetupStatus() GETs /api/setup/status → { setupComplete: boolean }
- postSetupConfig(payload) POSTs the four non-secret config values to /api/setup/config
- validateSetupDb/Oidc/Vapid() POST the three validation routes; map non-200 to a typed failure
- postSetupCredential({fastmailEmail, appPassword}) POSTs /api/setup/credential
- postSetupComplete() POSTs /api/setup/complete
- SetupPage renders the revised steps (Welcome → Config → Validate → Credential), the step indicator (Surface 2), per-step validation-state rows (Surface 5), the terminal "Setup complete" screen (Surface 7) on success, and the "Already Locked" screen (Surface 8) when status/complete returns 423
- No AppNav/BottomTabBar; role="main"; step heading h2; aria-live status rows; all copy plain-text JSX (no dangerouslySetInnerHTML)
Add the setup client functions to apps/pwa/src/api/client.ts following the existing fetch/JSON
conventions (same error-shape handling as fetchMe/saveCredential). Build
apps/pwa/src/routes/SetupPage.tsx per the REVISED UI-SPEC (Task 1) and PATTERNS.md §SetupPage.tsx:
local `useState` step cursor (no URL params, D-06 stateless); a TanStack `useMutation` per
POST step advancing the cursor onSuccess and surfacing a Surface-5 failure row onError; reuse the
CredentialSheet field/validation idiom verbatim for the Credential step; render Surface 7 on
/complete success and Surface 8 when an API call returns 423. Use the existing tokens.css custom
properties and lucide-react icons named in the UI-SPEC. All copy must be plain-text JSX children —
NO dangerouslySetInnerHTML (UI-SPEC security contract). Render standalone — no AppNav/BottomTabBar.
- source: client.ts exports the setup functions (`grep -Ec "fetchSetupStatus|postSetupConfig|postSetupComplete|postSetupCredential" apps/pwa/src/api/client.ts` >= 4)
- source: SetupPage references all setup routes (`grep -Ec "setup/config|setup/validate|setup/credential|setup/complete|setup/status" apps/pwa/src/routes/SetupPage.tsx` >= 4 — directly or via the client imports)
- source: no dangerouslySetInnerHTML (`grep -c "dangerouslySetInnerHTML" apps/pwa/src/routes/SetupPage.tsx` returns 0)
- source: standalone — SetupPage does not import AppNav/BottomTabBar (`grep -Ec "AppNav|BottomTabBar" apps/pwa/src/routes/SetupPage.tsx` returns 0)
- source: a11y — role="main" + aria-live present (`grep -Ec "role=\"main\"|aria-live" apps/pwa/src/routes/SetupPage.tsx` >= 1)
- test: `pnpm --filter @familysync/pwa typecheck` and `pnpm --filter @familysync/pwa build` green
cd apps/pwa && pnpm typecheck && pnpm build
Setup client functions added; SetupPage renders the revised 4-step wizard standalone with terminal/locked screens, no dangerouslySetInnerHTML; pwa typecheck + build green.
Task 3: App.tsx setup-status gate + /setup route + redirect
apps/pwa/src/App.tsx, apps/pwa/src/App.test.tsx
- apps/pwa/src/App.tsx (the file being modified — meQuery l.65-70, Routes block l.133-153, isAdmin loading-gate l.144-150)
- apps/pwa/src/App.test.tsx (existing App routing tests to extend, if present; else mirror the meQuery test setup)
- .planning/phases/12-initial-setup-wizard/12-PATTERNS.md §App.tsx (setupQuery + gate + Navigate pattern)
- .planning/phases/12-initial-setup-wizard/12-UI-SPEC.md §Routing & App-Level Gate
In apps/pwa/src/App.tsx add `import { SetupPage } from './routes/SetupPage.js';` and a
`setupQuery = useQuery({ queryKey: ['setupStatus'], queryFn: fetchSetupStatus, retry: false,
staleTime: 0 })` alongside meQuery (staleTime 0 — the gate must not be stale, mirrors D-10 spirit).
Add `} />` to the Routes block. Add the redirect gate:
while setupQuery is loading render nothing (prevent flash, mirror the isAdmin loading-gate l.144-150);
when `setupQuery.data?.setupComplete === false`, redirect all non-/setup routes to /setup
(``); when true, normal app boot proceeds. The /setup route renders
standalone — ensure the gate prevents AppNav/BottomTabBar from rendering over the wizard when
unconfigured (per UI-SPEC §Routing). Extend App.test.tsx: setupComplete:false → SetupPage/redirect
rendered; setupComplete:true → normal calendar route.
- source: setupQuery present (`grep -Ec "setupStatus|fetchSetupStatus" apps/pwa/src/App.tsx` >= 1)
- source: /setup route added (`grep -c "/setup" apps/pwa/src/App.tsx` >= 1)
- source: SetupPage imported (`grep -c "SetupPage" apps/pwa/src/App.tsx` >= 1)
- source: redirect gate keyed on setupComplete (`grep -Ec "setupComplete === false|setupComplete\\?" apps/pwa/src/App.tsx` >= 1)
- test: `pnpm --filter @familysync/pwa test -- App` green (both setupComplete branches)
cd apps/pwa && pnpm test -- App && pnpm typecheck
App.tsx queries /api/setup/status, exposes the /setup route, and redirects to /setup when unconfigured (no flash, no nav over wizard); App.test.tsx covers both branches.
Task 4: Verify the /setup wizard flow end-to-end (playwright-cli desktop)
Drive the /setup flow with playwright-cli (desktop Chromium) against a fresh/unconfigured DB per the verification steps below; escalate to the human only for steps playwright-cli cannot perform (e.g. a live Authelia/Fastmail round-trip).
The /setup wizard flow end-to-end in the PWA: redirect-to-/setup when unconfigured, the revised 4-step flow (Welcome → Config → Validate → Credential), validation-state rows, and the terminal "Setup complete" screen. Per CLAUDE.md the executor MUST first drive this with playwright-cli (desktop Chromium) — only fall back to a human if a step genuinely cannot be driven headlessly.
1. Bring up the dev stack against a FRESH/unconfigured DB (no setup_complete, no member_credentials) — see MEMORY familysync-dev-stack-setup; the API + PWA dev servers + MariaDB.
2. Using playwright-cli (`/usr/local/bin/playwright-cli`), navigate to the app root and confirm it redirects to /setup and renders the wizard with NO AppNav/BottomTabBar.
3. Drive the wizard: Config step accepts the OIDC issuer/client_id + VAPID public key + app URL inputs and POSTs /api/setup/config; Validate step shows pending→success rows for DB/OIDC/VAPID (mock or live as available); Credential step accepts a Fastmail email + app password (use a known-good or mocked credential) and shows "Credential verified."; Complete shows the "Setup complete" terminal screen with a Sign in link to /.
4. Re-navigate to /setup after completion and confirm the "Already Locked" screen renders (API 423).
5. Capture screenshots of the wizard, a validation-success row, and the terminal screen into the phase dir for the SUMMARY.
Only escalate to the human for steps playwright-cli cannot perform (e.g. a live Authelia/Fastmail round-trip if no mock is wired) — note any such steps explicitly.
Type "approved" or describe the issues observed
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| operator browser → /api/setup/* | the wizard is the unauthenticated client of the pre-auth API; it submits non-secret config + the Fastmail app password |
| SetupPage render → DOM | operator-supplied copy/config values rendered; XSS risk if not plain-text |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-13 | Information Disclosure | wizard never displays/handles secrets | mitigate | D-05 — no generate-secrets step; the wizard never receives SESSION_SECRET/encryption key/VAPID private key; only the non-secret VAPID public key is an input |
| T-12-14 | Tampering (XSS) | SetupPage rendering operator input | mitigate | No dangerouslySetInnerHTML (acceptance-checked); all copy + config values rendered as plain-text JSX children (UI-SPEC security contract) |
| T-12-15 | Information Disclosure | app password in the Credential step | mitigate | type="password" input (UI-SPEC); reuses CredentialSheet idiom; server-side noEchoHook (Plan 02) ensures the value is never echoed back |
| T-12-SC | Tampering | npm/pip/cargo installs | accept | Zero new packages — lucide-react/react-query/react-router already installed (RESEARCH §Standard Stack) |
- `cd apps/pwa && pnpm typecheck && pnpm build` green
- `pnpm --filter @familysync/pwa test -- App` green (both setupComplete branches)
- UI-SPEC revised: no generate-secrets step, config-collect inputs present
- playwright-cli desktop smoke: redirect→wizard→config→validate→credential→complete + locked screen
- SETUP-01: fresh instance redirects to /setup; guided multi-step wizard renders standalone
- SETUP-02: each input validates (DB/OIDC/VAPID/CalDAV) before the step completes
- D-05 honored: no in-wizard secret generation
- Terminal + Already-Locked screens behave per UI-SPEC; playwright-cli smoke passes