Files
familysync/.planning/phases/12-initial-setup-wizard/12-04-PLAN.md
T

18 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
12-initial-setup-wizard 04 execute 3
12-02
.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
false
SETUP-01
SETUP-02
truths artifacts key_links
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)
path provides contains
.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md Revised Wizard-Steps + Interaction-Contract (Step 2 dropped, Steps 3/4 collect config) config
path provides min_lines
apps/pwa/src/routes/SetupPage.tsx The standalone multi-step wizard component 80
path provides contains
apps/pwa/src/App.tsx setup-status gate + /setup route setup
from to via pattern
apps/pwa/src/App.tsx /api/setup/status setupQuery on load → redirect to /setup when unconfigured setup/status|setupStatus
from to via pattern
apps/pwa/src/routes/SetupPage.tsx /api/setup/* (config, validate, credential, complete) TanStack Query mutations 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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_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

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

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

<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>
Create `.planning/phases/12-initial-setup-wizard/12-04-SUMMARY.md` when done