---
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)"
---

<objective>
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.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.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
</context>

## 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`

<tasks>

<task type="execute">
  <name>Task 1: Revise 12-UI-SPEC.md (drop Generate-Secrets; config-collect inputs per D-02/D-04/D-05)</name>
  <files>.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md</files>
  <read_first>
    - .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
  </read_first>
  <action>
    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.
  </action>
  <acceptance_criteria>
    - 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)
  </acceptance_criteria>
  <verify>
    <automated>! 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</automated>
  </verify>
  <done>UI-SPEC steps revised: no generate-secrets step, config-collect inputs for the OIDC/VAPID step, step indicator re-numbered; design system untouched.</done>
</task>

<task type="execute" tdd="true">
  <name>Task 2: Setup API client + SetupPage wizard component</name>
  <files>apps/pwa/src/api/client.ts, apps/pwa/src/routes/SetupPage.tsx</files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    - 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)
  </behavior>
  <action>
    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.
  </action>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <verify>
    <automated>cd apps/pwa && pnpm typecheck && pnpm build</automated>
  </verify>
  <done>Setup client functions added; SetupPage renders the revised 4-step wizard standalone with terminal/locked screens, no dangerouslySetInnerHTML; pwa typecheck + build green.</done>
</task>

<task type="execute">
  <name>Task 3: App.tsx setup-status gate + /setup route + redirect</name>
  <files>apps/pwa/src/App.tsx, apps/pwa/src/App.test.tsx</files>
  <read_first>
    - 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
  </read_first>
  <action>
    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 `<Route path="/setup" element={<SetupPage />} />` 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
    (`<Navigate to="/setup" replace />`); 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.
  </action>
  <acceptance_criteria>
    - 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)
  </acceptance_criteria>
  <verify>
    <automated>cd apps/pwa && pnpm test -- App && pnpm typecheck</automated>
  </verify>
  <done>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.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 4: Verify the /setup wizard flow end-to-end (playwright-cli desktop)</name>
  <action>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).</action>
  <what-built>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.</what-built>
  <how-to-verify>
    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.
  </how-to-verify>
  <resume-signal>Type "approved" or describe the issues observed</resume-signal>
</task>

</tasks>

<threat_model>
## 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) |
</threat_model>

<verification>
- `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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
Create `.planning/phases/12-initial-setup-wizard/12-04-SUMMARY.md` when done
</output>
