diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 5627321..a60c6f5 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -415,6 +415,7 @@ At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx` | 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 | | 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 6/6 | Complete | 2026-06-13 | | 17. UI Optimization & Polish | v1.1 | 0/? | Not started | - | +| 18. Auto Timezone Detection | v1.1 | 4/4 | Complete | 2026-06-14 | ## Backlog @@ -633,11 +634,23 @@ Plans: ### Phase 18: Auto timezone detection and ability to change timezone -**Goal:** [To be planned] -**Requirements**: TBD -**Depends on:** Phase 17 -**Plans:** 0 plans +**Goal:** Make the household timezone an explicit, stored, user-changeable setting — auto-detected from the browser at first run, changeable from the role-gated /admin Settings — and route the server-side all-day "9 AM local" reminder computation through it (replacing the implicit `process.env.TZ` fallback), without touching the already-correct browser-local display/timed-write path. +**Requirements**: TBD (decision contract D-01..D-07 from 18-CONTEXT.md) +**Depends on:** Phase 10 (admin role + `/admin` Settings + `app_config`); Phase 11 (all-day reminder computation this rewires). Independent of Phase 17. Phase 12 (setup wizard) not required — seeding is self-contained. +**Plans:** 4/4 plans complete Plans: +**Wave 1** -- [ ] TBD (run /gsd-plan-phase 18 to break down) +- [x] 18-01-PLAN.md — TDD: getHouseholdTimezone(db) accessor + isValidIanaTimezone (D-05/D-06) + +**Wave 2** *(blocked on Wave 1 completion)* + +- [x] 18-02-PLAN.md — TDD: admin GET/PUT/seed timezone endpoints on adminRouter, requireAdmin + IANA validation + no-overwrite seed (D-01/D-02/D-03/D-04) +- [x] 18-03-PLAN.md — TDD: route all-day reminder TZ at reminderScheduler:247 + outboxWorker:501,607 through the accessor (D-05/D-06/D-07) + +**Wave 3** *(blocked on Wave 2 completion)* + +- [x] 18-04-PLAN.md — PWA Timezone section in /admin Settings (searchable IANA picker + detected-zone seed) + client fns (D-02/D-04) + +**UI hint**: yes diff --git a/.planning/STATE.md b/.planning/STATE.md index a247483..61aa991 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,15 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish -status: executing -stopped_at: Completed 11-04-PLAN.md -last_updated: "2026-06-14T12:31:06.011Z" -last_activity: 2026-06-14 +status: "Phase 18 shipped — PR #21" +stopped_at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next +last_updated: "2026-06-15T13:15:22.076Z" +last_activity: 2026-06-15 progress: total_phases: 23 completed_phases: 9 - total_plans: 32 - completed_plans: 32 + total_plans: 37 + completed_plans: 36 percent: 39 --- @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-06-10) **Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store -**Current focus:** Phase 11 — per-event-reminders +**Current focus:** Phase 18 — auto-timezone-detection-and-ability-to-change-timezone ## Current Position -Phase: 13 -Plan: Not started -Status: Ready to execute -Last activity: 2026-06-14 +Phase: 18 — COMPLETE +Plan: 4 of 4 +Status: Phase 18 shipped — PR #21 +Last activity: 2026-06-15 ### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action) @@ -107,6 +107,10 @@ _Updated after each plan completion_ | Phase 10-admin-role-settings P03 | 720 | 3 tasks | 6 files | | Phase 10-admin-role-settings P04 | 1315 | 3 tasks | 8 files | | Phase 11-per-event-reminders P11-04 | 60 | 3 tasks | 4 files | +| Phase 18-auto-timezone-detection-and-ability-to-change-timezone P01 | 2 | 2 tasks | 2 files | +| Phase 18 P02 | 3 | 2 tasks | 2 files | +| Phase 18 P03 | 28 | 2 tasks | 4 files | +| Phase 18 P04 | 15 | 3 tasks | 3 files | ## Accumulated Context @@ -184,6 +188,7 @@ Recent decisions affecting current work: - [Phase ?]: D-CLIENT-TYPES: reminderLeadMinutes required on CalendarOccurrence, optional on CreateEventPayload (absent=no-change D-08) - [Phase ?]: D-PAYLOAD-ABSENT: __custom__ unchanged → field omitted from payload; server hasOwnProperty check preserves original VALARM (D-08) - [Phase ?]: D-NULL-FALLBACK: occurrence.reminderLeadMinutes===null mapped to None; occurrence cannot distinguish absolute/multi-VALARM from no-reminder; rely on server-side preserve (absent payload) +- [Phase ?]: D-05/18-03: three all-day broker sites now route through getHouseholdTimezone(db) ### Roadmap Evolution @@ -252,8 +257,8 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-14T10:57:47.923Z -Stopped at: Completed 11-04-PLAN.md +Last session: 2026-06-15T02:46:09.800Z +Stopped at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next Resume file: None ## Operator Next Steps diff --git a/.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md b/.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md new file mode 100644 index 0000000..ac2d489 --- /dev/null +++ b/.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md @@ -0,0 +1,511 @@ +--- +phase: 12 +slug: initial-setup-wizard +status: draft +shadcn_initialized: false +preset: none +created: 2026-06-14 +--- + +# Phase 12 — UI Design Contract: Initial Setup Wizard + +> Visual and interaction contract for the first-run setup wizard. +> Generated by gsd-ui-researcher. Verified by gsd-ui-checker. + +--- + +## Context & Audience + +This wizard is **operator-facing, not end-user-facing**. The operator is the person standing up +the self-hosted FamilySync instance on Unraid — technical enough to edit `docker-compose.yml`, +but not necessarily a developer. The wizard renders for an **unauthenticated visitor** (it is the +one screen in the app shown outside the Authelia/OIDC guard). It must be calm, legible, and +unintimidating. + +This is the **only full-page standalone UI** in the app. It does NOT render inside the existing +AppNav + BottomTabBar shell. It owns its entire viewport. + +All design tokens are inherited from the existing system (`apps/pwa/src/styles/tokens.css`). +No new tokens are introduced. + +--- + +## Design System + +| Property | Value | +|----------|-------| +| Tool | none (existing CSS custom properties) | +| Preset | not applicable | +| Component library | none (hand-rolled inline styles, project convention) | +| Icon library | lucide-react (already installed — `KeyRound`, `CheckCircle`, `AlertCircle`, `Loader2`, `Copy`, `Check`, `ShieldCheck`) | +| Font | system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif (var(--font-family-base)) | + +Source: `apps/pwa/src/styles/tokens.css` — pre-populated from existing codebase. + +--- + +## Spacing Scale + +Uses the existing 4px-based scale. No new tokens. Values from `tokens.css`: + +| Token | Value | Usage | +|-------|-------|-------| +| --space-1 | 4px | Icon gaps, label-to-input gap | +| --space-2 | 8px | Compact element spacing, badge gap | +| --space-3 | 12px | Input padding (vertical), row gaps | +| --space-4 | 16px | Default element spacing, card padding, input padding (horizontal) | +| --space-6 | 24px | Sheet/card padding, section gap | +| --space-8 | 32px | Between major sections | +| --space-12 | 48px | Page top/bottom padding (AdminPage pattern) | + +Exceptions: +- Wizard card max-width: 540px (slightly wider than CredentialSheet 480px to accommodate + multi-field steps and generated-secret blocks). +- Step indicator touch targets: 44px minimum (accessibility). +- Copy-to-clipboard button: 36px height is acceptable since it is paired with an adjacent + textarea (which itself is large enough), but the copy button must have `minWidth: 44px`. + +--- + +## Typography + +All values from `tokens.css`. No new sizes or weights. + +| Role | Size | Weight | Line Height | Variable | +|------|------|--------|-------------|----------| +| Body | 15px | 400 | 1.5 | var(--text-body-size) / var(--text-body-weight) / var(--text-body-line-height) | +| Label | 13px | 400 | 1.4 | var(--text-label-size) / var(--text-label-weight) / var(--text-label-line-height) | +| Heading | 18px | 600 | 1.25 | var(--text-heading-size) / var(--text-heading-weight) / var(--text-heading-line-height) | +| Display | 24px | 600 | 1.2 | var(--text-display-size) / var(--text-display-weight) / var(--text-display-line-height) | + +Usage in this phase: +- Wizard page title ("FamilySync Setup"): Display (24px/600/1.2) +- Step heading (e.g. "Generate Secrets"): Heading (18px/600/1.25) +- Step description / helper text: Body (15px/400/1.5) +- Field labels, step counter, status badges: Label (13px/400/1.4) — labels use weight 600 +- Generated secret value (monospace block): 13px/400/1.4 with `font-family: monospace` override +- Section labels (uppercase caps, e.g. "STEP 2 OF 5"): Label (13px/600) with + `text-transform: uppercase; letter-spacing: 0.06em` (AdminPage `sectionLabelStyle` pattern) + +--- + +## Color + +All values from `tokens.css`. No new hex values. + +| Role | Value | Variable | Usage | +|------|-------|----------|-------| +| Dominant (60%) | #ffffff | var(--color-surface) | Page background, card background | +| Secondary (30%) | #f7f7f8 | var(--color-surface-dim) | Step sidebar/tracker background, generated-secret block background, inactive step indicator | +| Accent (10%) | #4a90d9 | var(--color-member-0) | Primary CTA buttons, active step indicator fill, spinner, copy button, links | +| Destructive | #dc2626 | var(--color-destructive) | Validation failure border + helper text (same CredentialSheet pattern) | + +Accent reserved for: +- Primary action buttons ("Continue", "Complete Setup") — filled background +- Active wizard step indicator (filled circle) +- Inline spinner (`Loader2`) during async validation +- Hyperlinks (e.g. "Get an app password") +- Copy-to-clipboard button icon +- Focus ring (`var(--color-focus-ring): #4a90d9`) + +Additional semantic colors (not new — already in tokens.css): +- `var(--color-border)` #e2e4e9 — card border, input border (default), step connector line +- `var(--color-border-subtle)` #eceef2 — section dividers within steps +- `var(--color-text-primary)` #111318 — headings, field values +- `var(--color-text-secondary)` #6b7280 — descriptions, helper text, "Back" button +- `var(--color-text-muted)` #9ca3af — completed step labels, placeholder text, inactive step numbers + +--- + +## Surface Architecture + +The wizard is a **standalone full-page route** (`/setup`) mounted in a separate React root or +an App-level gate (see Interaction Contract). It renders none of the AppNav / BottomTabBar / +SetupBanner chrome. + +### Surface 1 — Wizard Page Shell + +The wizard page itself. + +- Background: `var(--color-surface)` (#ffffff) +- Layout: vertically centered column, `min-height: 100dvh` +- Content column: `maxWidth: 540px`, `margin: 0 auto`, + `padding: var(--space-12) var(--space-6)` (48px top/bottom, 24px sides) +- Page title "FamilySync Setup": Display (24px/600), `color: var(--color-text-primary)`, + `marginBottom: var(--space-2)` (8px) +- Page subtitle "Let's get your instance ready.": Body (15px/400), + `color: var(--color-text-secondary)`, `marginBottom: var(--space-8)` (32px) + +### Surface 2 — Step Indicator + +Linear step tracker shown above the active step card. + +- Horizontal row of N step circles connected by lines +- Completed step: filled circle `var(--color-member-0)` with white `Check` icon (16px) +- Active step: filled circle `var(--color-member-0)` with white step number (13px/600) +- Upcoming step: circle with `var(--color-border)` 2px border, `var(--color-text-muted)` step + number +- Connector line: 1px `var(--color-border)` between circles; completed segment fills to + `var(--color-member-0)` +- Step label below each circle: 13px/400, `var(--color-text-muted)` (upcoming/completed), + `var(--color-text-primary)` (active) +- Circle size: 28px diameter; connector height: 1px; minimum row height: 44px touch target + achieved by centering in a 44px tall row + +Step labels (5 steps total): +1. Welcome +2. Secrets +3. Database +4. OIDC +5. Calendar + +### Surface 3 — Step Card + +The active step's input/content area. One card rendered at a time. + +- Background: `var(--color-surface)` (#ffffff) +- Border: `1px solid var(--color-border)` (#e2e4e9) +- Border-radius: 8px (2× `var(--space-2)`) +- Padding: `var(--space-6)` (24px) all sides +- Box-shadow: `0 1px 4px rgba(0,0,0,0.06)` (subtle lift) +- Step heading: Heading (18px/600), `color: var(--color-text-primary)`, + `marginBottom: var(--space-2)` (8px) +- Step description: Body (15px/400), `color: var(--color-text-secondary)`, `lineHeight: 1.5`, + `marginBottom: var(--space-6)` (24px) +- Field group spacing: `var(--space-4)` (16px) between fields + +### Surface 4 — Generated-Secret Block + +Used in Step 2 (Secrets) for values the operator must copy into env. + +- Background: `var(--color-surface-dim)` (#f7f7f8) +- Border: `1px solid var(--color-border-subtle)` (#eceef2) +- Border-radius: 4px (`var(--space-1)`) +- Padding: `var(--space-3) var(--space-4)` (12px 16px) +- Secret value: monospace, 13px/400, `var(--color-text-primary)`, word-break: break-all + (VAPID keys are long strings) +- Label above block: Label (13px/600), `var(--color-text-primary)` +- Copy button: icon-only (`Copy` icon 16px, `var(--color-member-0)`), positioned top-right + inside the block, `minWidth: 44px`, `minHeight: 36px` (acceptable — paired with large block) +- After copy: icon swaps to `Check` (16px, `var(--color-member-0)`) for 2 seconds, then reverts +- Acknowledgement checkbox below each secret block: standard checkbox input, Label (13px/400), + "I have copied this value into my `.env` file." — the Continue button is disabled until all + checkboxes on the step are checked. + +### Surface 5 — Validation State Row + +Shown after the operator submits a validation step (DB / OIDC / CalDAV). + +- Pending: `Loader2` icon (16px, `var(--color-member-0)`, `animation: spin 1s linear infinite`) + + Body (15px/400) status text in `var(--color-text-secondary)` — inline row +- Success: `CheckCircle` icon (16px, `var(--color-text-secondary)`) + success text Body (15px/400) + in `var(--color-text-secondary)` +- Failure: `AlertCircle` icon (16px, `var(--color-destructive)`) + error text Body (15px/400) in + `var(--color-destructive)` — same pattern as CredentialSheet `FAILURE_TEXT` +- Layout: `display: flex; alignItems: center; gap: var(--space-2, 8px)` (CredentialSheet pattern) + +### Surface 6 — Action Row + +Bottom of each step card. + +- Layout: `display: flex; justifyContent: flex-end; gap: var(--space-3, 12px)` (CredentialSheet + pattern) +- "Back" button: ghost (no background, no border), Label (13px/600), + `color: var(--color-text-secondary)`, `minHeight: 44px`, `padding: 0 var(--space-4)` + — hidden on Step 1 (no back from Welcome) +- "Continue" / "Complete Setup" button: filled, `background: var(--color-member-0)` (#4a90d9), + `color: #ffffff`, Label (13px/600), `minHeight: 44px`, `padding: 0 var(--space-6)`, + `borderRadius: var(--space-1)` (4px), `transition: background 0.15s ease` + — disabled state: `background: var(--color-border)` (#e2e4e9), `cursor: default` + (AdminPage / CredentialSheet pattern) +- "Continue" label on steps 1–4; "Complete Setup" label on step 5 + +### Surface 7 — Terminal "Setup Complete" Screen + +Replaces the wizard card after step 5 completes successfully. + +- Icon: `ShieldCheck` (48px, `var(--color-member-0)`) centered +- Heading: "Setup complete" — Display (24px/600), centered, `marginTop: var(--space-4)`, + `color: var(--color-text-primary)` +- Body: "Your FamilySync instance is ready. Sign in to continue." — Body (15px/400), + `color: var(--color-text-secondary)`, centered, `marginTop: var(--space-2)` +- "Sign in" button: filled accent button (same style as Continue), centered, + `marginTop: var(--space-6)`, navigates to `/` (triggers OIDC redirect) +- No step indicator visible on this screen (step indicator hidden once setup complete) + +### Surface 8 — "Already Locked" Screen + +Shown when the operator navigates to `/setup` after `setup_complete = true` (423 from API). + +- Icon: `ShieldCheck` (48px, `var(--color-text-muted)`) centered +- Heading: "Setup already complete" — Heading (18px/600), centered, + `color: var(--color-text-primary)` +- Body: "This instance has already been configured. Sign in to continue." — Body (15px/400), + `color: var(--color-text-secondary)`, centered +- "Sign in" link: accent-colored text link (no button chrome), 15px/600, + navigates to `/` + +--- + +## Wizard Steps — Detailed Interaction Contract + +### Step 1: Welcome + +Purpose: orient the operator; no inputs; no validation. + +- Heading: "Welcome to FamilySync Setup" +- Description: "This wizard will guide you through configuring your self-hosted instance. + You'll need: your OIDC client credentials (Authelia), a Fastmail account with an app password, + and a copy of your `docker-compose.yml` to paste generated secrets into. This takes about + 5 minutes." +- No input fields. +- Continue button: always enabled. + +### Step 2: Generate Secrets + +Purpose: display generated session secret, encryption key, and VAPID keypair; operator copies +each into env. + +- Heading: "Generated Secrets" +- Description: "These values are generated once and displayed now. Copy each into your + `docker-compose.yml` environment block before continuing. They will never be shown again and + are not stored in the database." +- Four generated-secret blocks (Surface 4), each with acknowledgement checkbox: + 1. `SESSION_SECRET` — label "Session secret", 64-char hex string + 2. `APP_PASSWORD_ENCRYPTION_KEY` — label "Encryption key", 64-char hex string + 3. `VAPID_PUBLIC_KEY` — label "VAPID public key" + 4. `VAPID_PRIVATE_KEY` — label "VAPID private key" +- Continue button disabled until all 4 checkboxes are checked. +- No async validation on this step. Secrets are generated client-side or fetched from + `POST /api/setup/generate` (backend choice — UI treats them as string values to display). + +### Step 3: Database + +Purpose: verify the DB connection configured in env is reachable. + +- Heading: "Database Connection" +- Description: "Verify that the app can reach the MariaDB database configured in your + environment. No changes are made — this is a read-only connectivity check." +- No operator input fields (DB creds come from env, not from this UI). +- "Test Connection" button (primary filled, full-width on this step — replace normal action row): + triggers `POST /api/setup/validate/db` +- Validation state row (Surface 5) shown below the description during/after the test: + - Pending: "Testing database connection…" + - Success: "Database connection verified." + - Failure: "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your + `docker-compose.yml` and try again." +- Continue button appears after success; disabled during pending; hidden on failure (operator + must retry first). +- Back is available. + +### Step 4: OIDC & VAPID + +Purpose: verify OIDC discovery resolves and the VAPID keypair in env is structurally valid. + +- Heading: "OIDC & Push" +- Description: "Verify that the Authelia OIDC issuer is reachable and that the VAPID keypair + you copied in Step 2 is in place." +- Two validation rows, triggered sequentially by "Validate" button: + - OIDC: label "Authelia issuer discovery", `POST /api/setup/validate/oidc` + - Pending: "Checking OIDC discovery…" + - Success: "OIDC discovery resolved." + - Failure: "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia + is reachable." + - VAPID: label "VAPID keypair", `POST /api/setup/validate/vapid` + - Pending: "Checking VAPID keypair…" + - Success: "VAPID keypair is valid." + - Failure: "VAPID private key could not be verified. Ensure you copied both keys from Step 2 + into your environment and restarted the container." +- "Validate" button triggers both checks in sequence. +- Continue appears (and is enabled) only when both rows show success. +- Back is available. + +### Step 5: Calendar Credential + +Purpose: set the first member's Fastmail app password; validate against CalDAV PROPFIND. +Reuses the CredentialSheet interaction pattern (same fields, same validation feedback, +same copy). + +- Heading: "Fastmail Credential" +- Description: "Add the Fastmail app password for the first household member. This credential + is validated against Fastmail CalDAV before saving. The password is never stored in + plain text." +- Fields (same as CredentialSheet): + - Fastmail email (type="email", autoComplete="email", placeholder="user@fastmail.com") + - App password (type="password", autoComplete="new-password") +- Helper text below fields: "Enter the Fastmail app password scoped to Calendars/CalDAV. + [Get an app password](https://app.fastmail.com/settings/security/devicetokens) — choose + the 'Calendars & Contacts (CalDAV)' scope." + - Link: accent-colored, `text-decoration: underline`, opens in new tab + `rel="noopener noreferrer"` +- Validation state row (Surface 5): + - Pending: "Validating against CalDAV…" (Loader2 spinner) + - Success: "Credential verified." — Continue becomes enabled + - Failure: "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & + Contacts (CalDAV)' and try again." (in `var(--color-destructive)`) +- Continue button label on this step: "Complete Setup" +- On continue: `POST /api/setup/complete` — promotes operator to admin, sets + `app_config.setup_complete`, redirects to Surface 7 (Terminal Screen) +- Back is available. + +--- + +## Routing & App-Level Gate + +The wizard lives at route `/setup`. App.tsx must add a gate at startup: + +1. On app load, `GET /api/setup/status` is fetched (before any other /api route, mounts outside + OIDC guard). +2. If response is `{ setupComplete: false }` → redirect entire app to `/setup` (full-page + takeover, no AppNav/BottomTabBar rendered). +3. If response is `{ setupComplete: true }` → normal app boot continues. +4. If `/setup` is navigated directly after setup is complete (API returns 423) → render + Surface 8 ("Already Locked"). + +The `/setup` route does NOT render inside the normal App shell. It replaces it entirely (or +is handled before `` routes — implementation choice for planner, but the design +contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeniedBanner). + +--- + +## Copywriting Contract + +| Element | Copy | +|---------|------| +| Page title | "FamilySync Setup" | +| Page subtitle | "Let's get your instance ready." | +| Primary CTA (steps 1–4) | "Continue" | +| Primary CTA (step 5) | "Complete Setup" | +| Secondary action | "Back" | +| Terminal heading | "Setup complete" | +| Terminal body | "Your FamilySync instance is ready. Sign in to continue." | +| Terminal CTA | "Sign in" | +| Locked heading | "Setup already complete" | +| Locked body | "This instance has already been configured. Sign in to continue." | +| Locked link | "Sign in" | +| Step 1 heading | "Welcome to FamilySync Setup" | +| Step 1 description | "This wizard will guide you through configuring your self-hosted instance. You'll need: your OIDC client credentials (Authelia), a Fastmail account with an app password, and a copy of your `docker-compose.yml` to paste generated secrets into. This takes about 5 minutes." | +| Step 2 heading | "Generated Secrets" | +| Step 2 description | "These values are generated once and displayed now. Copy each into your `docker-compose.yml` environment block before continuing. They will never be shown again and are not stored in the database." | +| Step 2 acknowledgement | "I have copied this value into my `.env` file." | +| Step 3 heading | "Database Connection" | +| Step 3 description | "Verify that the app can reach the MariaDB database configured in your environment. No changes are made — this is a read-only connectivity check." | +| Step 3 CTA | "Test Connection" | +| Step 3 pending | "Testing database connection…" | +| Step 3 success | "Database connection verified." | +| Step 3 failure | "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your `docker-compose.yml` and try again." | +| Step 4 heading | "OIDC & Push" | +| Step 4 description | "Verify that the Authelia OIDC issuer is reachable and that the VAPID keypair you copied in Step 2 is in place." | +| Step 4 CTA | "Validate" | +| Step 4 OIDC pending | "Checking OIDC discovery…" | +| Step 4 OIDC success | "OIDC discovery resolved." | +| Step 4 OIDC failure | "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia is reachable." | +| Step 4 VAPID pending | "Checking VAPID keypair…" | +| Step 4 VAPID success | "VAPID keypair is valid." | +| Step 4 VAPID failure | "VAPID private key could not be verified. Ensure you copied both keys from Step 2 into your environment and restarted the container." | +| Step 5 heading | "Fastmail Credential" | +| Step 5 description | "Add the Fastmail app password for the first household member. This credential is validated against Fastmail CalDAV before saving. The password is never stored in plain text." | +| Step 5 helper text | "Enter the Fastmail app password scoped to Calendars/CalDAV." | +| Step 5 helper link text | "Get an app password" | +| Step 5 helper link suffix | " — choose the 'Calendars & Contacts (CalDAV)' scope." | +| Step 5 pending | "Validating against CalDAV…" | +| Step 5 success | "Credential verified." | +| Step 5 failure | "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." | +| Empty state (none for wizard — every step has explicit content) | N/A | +| Error state (network/unexpected) | "Something went wrong. Please try again." (generic fallback, shown in Surface 5 failure style) | +| Destructive actions | None — wizard has no destructive actions. The "Complete Setup" action is irreversible in effect but not destructive; no confirmation dialog required. | + +--- + +## Accessibility Contract + +- `role="main"` on the wizard content column +- Step indicator: `role="list"` with each step as `role="listitem"`; + active step has `aria-current="step"` +- Step card: `role="group"` with `aria-labelledby` pointing to the step heading `id` +- Heading hierarchy: `

` for page title, `

` for step heading +- All inputs: explicit `