--- phase: 12 slug: initial-setup-wizard status: draft shadcn_initialized: false preset: none created: 2026-06-14 updated: 2026-06-15 --- # 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. > > **Revision note (2026-06-15):** CONTEXT.md (D-04/D-05/D-06) supersedes the original > validate-only model. Step 2 "Generate Secrets" is dropped (generation is pre-boot via repo > helper script). Steps 3–4 are reworked to collect config via input fields, not just validate > env. All other design tokens, surfaces, and a11y contracts are unchanged. --- ## 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). - Step indicator touch targets: 44px minimum (accessibility). --- ## 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. "OIDC & App URL"): 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 - Section labels (uppercase caps, e.g. "STEP 2 OF 4"): 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, inactive step indicator | | Accent (10%) | #4a90d9 | var(--color-member-0) | Primary CTA buttons, active step indicator fill, spinner, 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") - 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 Routing section). It renders none of the AppNav / BottomTabBar / SetupBanner chrome. ### Surface 1 — Wizard Page Shell - 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 4 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 (4 steps total): 1. Welcome 2. Instance 3. Calendar 4. Complete ### 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 — Input Field Standard text input used across steps 2–3 to collect config. - Width: 100%, `box-sizing: border-box` - Padding: `var(--space-3, 12px) var(--space-4, 16px)` (matches CredentialSheet pattern) - Border: `1px solid var(--color-border)` default; `1px solid var(--color-destructive)` on validation error - Border-radius: `var(--space-1, 4px)` (4px) - Font: 15px/400, `var(--color-text-primary)`, `var(--font-family-base)` - Background: `var(--color-surface)` - Label above: 13px/600, `var(--color-text-primary)`, `marginBottom: var(--space-1)` (4px) - Helper text below: 13px/400, `var(--color-text-secondary)` ### Surface 5 — Validation State Row Shown after a validation request is triggered (DB connectivity / OIDC discovery / CalDAV PROPFIND). - 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) - Container: `role="status"` with `aria-live="polite"` ### 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–3; "Complete Setup" label on step 4 ### Surface 7 — Terminal "Setup Complete" Screen Replaces the wizard card after step 4 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 > **REVISION (2026-06-15, CONTEXT.md D-04/D-05):** The original 5-step model included a > "Generate Secrets" step (Step 2). This step is **dropped**. Secret generation (SESSION_SECRET, > APP_PASSWORD_ENCRYPTION_KEY, VAPID public/private keys) is done pre-boot via the > `npm run generate-secrets` repo helper script. The wizard never generates, displays, or > requests acknowledgement of secrets. Steps are now 4 total. ### 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. Before continuing, run `npm run generate-secrets` from the repo to generate your instance secrets and add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia) and a Fastmail account with an app password. This takes about 5 minutes." - Informational note block (Surface 3 — inside the card, `background: var(--color-surface-dim)`, `border-radius: 4px`, `padding: var(--space-3) var(--space-4)`, `marginBottom: var(--space-4)`): - Label (13px/600, `var(--color-text-primary)`): "Before you start" - Body: "Run `npm run generate-secrets` and add the output to your Docker environment block. These secrets cannot be recovered if lost." - No input fields. - Continue button: always enabled. ### Step 2: Instance Configuration Purpose: collect non-secret runtime config that the wizard writes to `app_config`. No secrets are collected here. Validates DB connectivity and OIDC discovery. - Heading: "Instance Configuration" - Description: "Enter your instance's connection details. These are written to the database — not your environment file." **Fields (collected and written to `app_config`):** 1. **App URL** - Label: "App URL" - Type: `text`, placeholder: `https://familysync.example.com` - Helper: "The public URL where FamilySync is reachable." - `app_config` key: `app_url` 2. **OIDC Issuer** - Label: "OIDC issuer URL" - Type: `text`, placeholder: `https://auth.example.com` - Helper: "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration` from this URL." - `app_config` key: `oidc_issuer` 3. **OIDC Client ID** - Label: "OIDC client ID" - Type: `text`, placeholder: `familysync` - Helper: "The client ID registered in Authelia for this application." - `app_config` key: `oidc_client_id` 4. **VAPID Public Key** - Label: "VAPID public key" - Type: `text`, placeholder: `BH…` (URL-safe base64, 87 chars) - Helper: "Paste the `VAPID_PUBLIC_KEY` value from `npm run generate-secrets`." - `app_config` key: `vapid_public_key` **Validations (triggered by "Save & Validate" button):** Two sequential checks run after the operator taps the action button: 1. **Database** — `POST /api/setup/validate/db` - Validation state row (Surface 5): - 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 environment and try again." 2. **OIDC discovery** — `POST /api/setup/validate/oidc` (runs after DB success) - Validation state row (Surface 5): - Pending: "Checking OIDC discovery…" - Success: "OIDC discovery resolved." - Failure: "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server." - Action button label on this step: "Save & Validate" (primary filled, full-width on this step — replace the normal right-aligned action row with a full-width button above the validation state rows, then normal Continue/Back row appears below once both pass) - Continue appears (enabled) only when both validation rows show success and config has been saved (`POST /api/setup/config` call completes before validation begins). - Back is available. ### Step 3: 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` — provisions the pre-OIDC local user + credential, flips `app_config.setup_complete`, redirects to Surface 7 (Terminal Screen) - Back is available. ### Step 4 — no longer a step card; becomes the Terminal Screen After `POST /api/setup/complete` succeeds, the wizard card is replaced by Surface 7 (Terminal "Setup Complete" screen). There is no separate "Step 4" card — the terminal screen IS the completion state. --- ## 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–2) | "Continue" | | Step 2 action button | "Save & Validate" | | Primary CTA (step 3) | "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. Before continuing, run `npm run generate-secrets` from the repo to generate your instance secrets and add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia) and a Fastmail account with an app password. This takes about 5 minutes." | | Step 1 pre-start label | "Before you start" | | Step 1 pre-start body | "Run `npm run generate-secrets` and add the output to your Docker environment block. These secrets cannot be recovered if lost." | | Step 2 heading | "Instance Configuration" | | Step 2 description | "Enter your instance's connection details. These are written to the database — not your environment file." | | Step 2 field: App URL label | "App URL" | | Step 2 field: App URL placeholder | "https://familysync.example.com" | | Step 2 field: App URL helper | "The public URL where FamilySync is reachable." | | Step 2 field: OIDC issuer label | "OIDC issuer URL" | | Step 2 field: OIDC issuer placeholder | "https://auth.example.com" | | Step 2 field: OIDC issuer helper | "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration` from this URL." | | Step 2 field: OIDC client ID label | "OIDC client ID" | | Step 2 field: OIDC client ID placeholder | "familysync" | | Step 2 field: OIDC client ID helper | "The client ID registered in Authelia for this application." | | Step 2 field: VAPID public key label | "VAPID public key" | | Step 2 field: VAPID public key placeholder | "BH…" | | Step 2 field: VAPID public key helper | "Paste the `VAPID_PUBLIC_KEY` value from `npm run generate-secrets`." | | Step 2 DB pending | "Testing database connection…" | | Step 2 DB success | "Database connection verified." | | Step 2 DB failure | "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again." | | Step 2 OIDC pending | "Checking OIDC discovery…" | | Step 2 OIDC success | "OIDC discovery resolved." | | Step 2 OIDC failure | "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server." | | Step 3 heading | "Fastmail Credential" | | Step 3 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 3 helper text | "Enter the Fastmail app password scoped to Calendars/CalDAV." | | Step 3 helper link text | "Get an app password" | | Step 3 helper link suffix | " — choose the 'Calendars & Contacts (CalDAV)' scope." | | Step 3 pending | "Validating against CalDAV…" | | Step 3 success | "Credential verified." | | Step 3 failure | "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." | | Empty state | N/A — every step has explicit content | | 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. "Complete Setup" 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 `