---
phase: 12-initial-setup-wizard
plan: 05
type: execute
wave: 2
depends_on: ["12-06"]
files_modified:
  - apps/pwa/src/routes/SetupPage.tsx
  - apps/pwa/src/routes/SetupPage.test.tsx
autonomous: true
gap_closure: true
requirements: [SETUP-01]
must_haves:
  truths:
    - "The Instance step intro copy no longer contains the DB-vs-env-file aside"
    - "A read-only, disabled DB-name field renders directly under the App URL field on the Instance step"
    - "Navigating Back from the Calendar step to the Instance step preserves all previously entered field values"
  artifacts:
    - path: "apps/pwa/src/routes/SetupPage.tsx"
      provides: "Instance step copy trimmed; read-only DB-name field; field state lifted so Back preserves values"
      contains: "readOnly"
  key_links:
    - from: "SetupPage Instance step"
      to: "GET /api/setup/status dbName"
      via: "fetchSetupStatus().dbName populates the read-only field"
      pattern: "dbName"
    - from: "SetupPage parent (step owner)"
      to: "Step2Config fields"
      via: "field values lifted to SetupPage (or sessionStorage) and passed as props"
      pattern: "appUrl|oidcIssuer|oidcClientId|vapidPublicKey"
---

<objective>
Close UAT gaps 1, 3 (frontend), and 4 — all on the PWA Instance step (`SetupPage.tsx`).

Gap 1 (cosmetic): the Instance step intro `<p>` contains "These are written to the database — not your environment file." — an implementation aside the user wants dropped.

Gap 3 (minor, frontend half): the "database connection verified" row has no on-screen referent. Add a read-only, greyed-out/disabled field showing the env-derived DB name (from `GET /api/setup/status` `dbName`, added in Plan 06), positioned directly under the App URL field. Keep the existing DB validation row as-is.

Gap 4 (minor): each wizard step holds its field values in its own local `useState` and unmounts on navigation, so going Back from the Calendar step to the Instance step loses all entered config. Lift Instance (and Calendar) field values into `SetupPage` (or persist to sessionStorage) so Back preserves them.

Purpose: First-run operator can navigate Back without re-typing; the DB row makes sense; no confusing implementation copy.
Output: Instance step with trimmed copy, a read-only DB-name field, and persistent field values across Back navigation.
</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-UAT.md
@.planning/phases/12-initial-setup-wizard/12-04-SUMMARY.md

# Files to edit
@apps/pwa/src/routes/SetupPage.tsx
@apps/pwa/src/routes/SetupPage.test.tsx
# Contract this plan consumes (added by Plan 06)
@apps/pwa/src/api/client.ts
</context>

<tasks>

<task type="auto">
  <name>Task 1: Drop the DB-vs-env-file aside + add read-only DB-name field (gaps 1, 3-frontend)</name>
  <files>apps/pwa/src/routes/SetupPage.tsx, apps/pwa/src/routes/SetupPage.test.tsx</files>
  <action>
    Gap 1 — In `Step2Config` (apps/pwa/src/routes/SetupPage.tsx, the intro `<p>` at ~line 547-558), remove the sentence "These are written to the database — not your environment file." Keep the first sentence ("Enter your instance's connection details.") and the surrounding paragraph styling intact.

    Gap 3 (frontend) — Render a read-only, disabled field showing the env-derived DB name directly under the App URL field block (the App URL `<div>` ends ~line 575, just before the OIDC Issuer block):
    - Fetch the DB name from the status endpoint. Import `fetchSetupStatus` from '../api/client.js' (already exported) and read `dbName` from its response (the `dbName?: string | null` field added by Plan 06). Use `useQuery({ queryKey: ['setupStatus'], queryFn: fetchSetupStatus, staleTime: 0, retry: false })` inside Step2Config (or lift the query to SetupPage and pass `dbName` as a prop — executor's choice, but keep it self-contained to the Instance step).
    - Render a labelled input mirroring the existing field markup (reuse `labelStyle`, `inputStyle(false)`, `helperStyle`): label "Database" (or "Database name"), value = the fetched dbName (fallback to an empty string / a "—" placeholder while loading or if null), with `readOnly` AND `disabled` set, a greyed-out appearance (set the input's `background`/`color` to a muted token, e.g. `var(--color-surface-dim)` / `var(--color-text-secondary)`), and `aria-readonly="true"`. Helper text: explains this is configured via the server's Docker environment (DB_HOST/DB_PORT/DB_USER/DB_PASSWORD), not entered here — so the "database connection verified" row below has a referent. NEVER render DB_HOST/DB_USER/DB_PASSWORD — only the name.
    - Do NOT change the existing DB ValidationRow ("Database connection verified.") — keep it as-is per the UAT "missing" note.

    In apps/pwa/src/routes/SetupPage.test.tsx: assert the dropped sentence is no longer present (query the Instance step text and assert "not your environment file" is absent), and assert the read-only DB-name field renders disabled/readOnly with the mocked dbName. Mock `fetchSetupStatus` (or the client module) to return `{ setupComplete: false, dbName: 'familysync' }`.
  </action>
  <verify>
    <automated>cd apps/pwa && pnpm test -- SetupPage 2>&1 | tail -20</automated>
  </verify>
  <done>Instance step intro no longer contains "not your environment file"; a disabled+readOnly DB-name field (value from status dbName) renders under App URL; the existing DB validation row is unchanged; SetupPage.test.tsx GREEN.</done>
</task>

<task type="auto">
  <name>Task 2: Preserve wizard field values across Back navigation (gap 4)</name>
  <files>apps/pwa/src/routes/SetupPage.tsx, apps/pwa/src/routes/SetupPage.test.tsx</files>
  <action>
    Lift the Instance-step field values (appUrl, oidcIssuer, oidcClientId, vapidPublicKey) and the Calendar-step field values (email, plus credential-verified flag if needed for UX) out of the per-step local `useState` so they survive step unmount/remount.

    Recommended approach (state lifted to the SetupPage parent — matches the existing "parent owns `step`" structure):
    - In `SetupPage` (the component owning `useState<WizardStep>`), add state for the Instance fields: `appUrl`, `oidcIssuer`, `oidcClientId`, `vapidPublicKey` (the app password is sensitive — do NOT lift/persist the password value; only the non-secret email may be lifted if convenient, but the password must stay local and cleared on unmount per T-12-15).
    - Pass these values + their setters down to `Step2Config` as props; replace the component-local `useState('')` declarations (~lines 439-442) with the props. Validation/mutation logic stays inside Step2Config.
    - Ensure that when navigating Back from Step 3 → Step 2, the Instance fields are still populated (because the parent now holds them). When navigating Back from Step 2 → Step 1 and forward again, values also persist.

    Alternative (sessionStorage) is acceptable if simpler, but MUST NOT persist the Fastmail app password (T-12-15) — only the non-secret Instance fields. Prefer the lifted-state approach.

    Security: the Fastmail app password (Step 3) is NOT lifted and NOT persisted to sessionStorage — it remains in Step3Credential local state and is cleared on unmount (T-12-15 preserved).

    In apps/pwa/src/routes/SetupPage.test.tsx: add a test that fills the Instance fields, advances to the Calendar step, navigates Back, and asserts the Instance field values are still present (inputs retain their values). Add an assertion that the password field is NOT persisted across navigation (re-mount of Step 3 starts empty).
  </action>
  <verify>
    <automated>cd apps/pwa && pnpm test -- SetupPage 2>&1 | tail -20</automated>
  </verify>
  <done>Filling the Instance step, advancing, then clicking Back restores all four Instance field values; the Fastmail app password is never persisted across navigation; SetupPage.test.tsx GREEN.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| operator input → wizard state | Non-secret config + a sensitive app password are entered here |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-12-15 | Information Disclosure | Step3 app password | mitigate | App password stays in Step3 local state; NOT lifted to parent, NOT written to sessionStorage; cleared on unmount; field remains type="password" |
| T-12-14 | Tampering (XSS) | Instance/DB-name copy | mitigate | All new copy + dbName rendered as plain-text JSX children; no dangerouslySetInnerHTML (grep returns 0) |
| T-12-3DB | Information Disclosure | DB-name field | mitigate | Only the dbName from status is rendered; DB_HOST/DB_USER/DB_PASSWORD never fetched or shown |
</threat_model>

<verification>
- `cd apps/pwa && pnpm test -- SetupPage` GREEN
- `grep -n "not your environment file" apps/pwa/src/routes/SetupPage.tsx` returns nothing
- `grep -c "dangerouslySetInnerHTML" apps/pwa/src/routes/SetupPage.tsx` is 0
- `grep -nE "sessionStorage|localStorage" apps/pwa/src/routes/SetupPage.tsx` — if present, confirm no password/appPassword key is written
- `cd apps/pwa && pnpm typecheck` clean
</verification>

<success_criteria>
- Gap 1 closed: implementation aside removed.
- Gap 3 (frontend) closed: read-only DB-name field gives the DB validation row a referent.
- Gap 4 closed: Back navigation preserves Instance field values; app password never persisted.
</success_criteria>

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