Drop Step 2 "Generate Secrets" (secrets move to pre-boot helper script per D-05), rework Step 2 to collect non-secret config via form fields (app URL, OIDC issuer, client_id, VAPID public key) written to app_config per D-02, and reduce wizard from 5 steps to 4. All design tokens, surfaces, and a11y contracts unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
532 lines
25 KiB
Markdown
532 lines
25 KiB
Markdown
---
|
||
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 `<BrowserRouter>` 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: `<h1>` for page title, `<h2>` for step heading
|
||
- All inputs: explicit `<label htmlFor>` association (same CredentialSheet pattern)
|
||
- Disabled buttons: `disabled` attribute (not just pointer-events: none)
|
||
- Validation state row: `role="status"` with `aria-live="polite"` so screen readers announce
|
||
results without focus movement
|
||
- Escape key: no sheet to close on this page; Escape has no effect in wizard
|
||
- Focus management: on step advance, focus moves to the new step heading (tabIndex={-1} pattern
|
||
from CredentialSheet heading)
|
||
- Minimum touch targets: 44px on all interactive elements (`minHeight: 44px`, `minWidth: 44px`)
|
||
- Focus ring: `var(--color-focus-ring)` (#4a90d9), 2px outline, 2px offset on all focusable
|
||
elements (same project convention)
|
||
|
||
---
|
||
|
||
## Responsive Behavior
|
||
|
||
The wizard is desktop-first (operator is typically on a desktop browser when standing up
|
||
infrastructure), but must be usable on a phone if needed.
|
||
|
||
- Desktop (≥768px): card centered at maxWidth 540px; step indicator spans full card width
|
||
- Phone (<768px): card fills viewport minus 24px horizontal padding;
|
||
step indicator uses short labels (1, 2, 3, 4) or icon-only to avoid overflow;
|
||
no bottom tab bar (not rendered at all on wizard page)
|
||
- No BottomTabBar, no AppNav on this page at any breakpoint
|
||
|
||
---
|
||
|
||
## Security Display Rules
|
||
|
||
These are hard UI rules, not implementation notes:
|
||
|
||
- The wizard never displays generated secret values. Secrets (SESSION_SECRET,
|
||
APP_PASSWORD_ENCRYPTION_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY) are generated pre-boot by
|
||
the `npm run generate-secrets` helper and pasted into Docker env by the operator. The wizard
|
||
only collects the VAPID public key (non-secret) as a form field.
|
||
- App password in Step 3: `type="password"` — never visible
|
||
- No `dangerouslySetInnerHTML` anywhere on this page (T-05-24 project convention)
|
||
|
||
---
|
||
|
||
## Registry Safety
|
||
|
||
| Registry | Blocks Used | Safety Gate |
|
||
|----------|-------------|-------------|
|
||
| shadcn official | none | not applicable |
|
||
| third-party | none | not applicable |
|
||
|
||
No third-party registries. All components are hand-rolled following existing project convention.
|
||
lucide-react icons are already installed (`KeyRound`, `ShieldCheck`, `Loader2`, `CheckCircle`,
|
||
`AlertCircle`, `Copy`, `Check` are all available or trivially added from the existing
|
||
lucide-react dependency).
|
||
|
||
---
|
||
|
||
## Pre-Population Sources
|
||
|
||
| Decision | Source | Value |
|
||
|----------|--------|-------|
|
||
| Spacing scale | apps/pwa/src/styles/tokens.css | --space-1 through --space-12 |
|
||
| Typography scale | apps/pwa/src/styles/tokens.css | 4 sizes (13/15/18/24px), 2 weights (400/600) |
|
||
| Color palette | apps/pwa/src/styles/tokens.css | All hex values |
|
||
| Component library | apps/pwa (convention) | Hand-rolled inline styles, no shadcn |
|
||
| Icon library | apps/pwa imports | lucide-react |
|
||
| Button style | CredentialSheet.tsx / AdminPage.tsx | Filled accent for primary, ghost for secondary |
|
||
| Input style | CredentialSheet.tsx | 12px/16px padding, 4px border-radius, destructive border on error |
|
||
| Section label style | AdminPage.tsx | 13px/600/uppercase/0.06em letter-spacing |
|
||
| Card padding | AdminPage.tsx | var(--space-12) top/bottom, var(--space-6) horizontal |
|
||
| Credential copy | CredentialSheet.tsx | Same field layout, same validation feedback pattern |
|
||
| Step 3 fields | CredentialSheet.tsx | Exact field structure, labels, helper text, link |
|
||
| Step count (4 not 5) | CONTEXT.md D-04/D-05 | Step 2 "Generate Secrets" dropped — generation pre-boot |
|
||
| Step 2 input fields | CONTEXT.md D-02 | Non-secret config collected in wizard, written to app_config |
|
||
| No secrets in wizard | CONTEXT.md D-01/D-05 | Kernel secrets (DB, SESSION, ENCRYPTION_KEY, VAPID_PRIVATE) stay in env |
|
||
| Step labels | Claude's Discretion (CONTEXT.md) | Welcome / Instance / Calendar / Complete |
|
||
|
||
---
|
||
|
||
## Checker Sign-Off
|
||
|
||
- [ ] Dimension 1 Copywriting: PASS
|
||
- [ ] Dimension 2 Visuals: PASS
|
||
- [ ] Dimension 3 Color: PASS
|
||
- [ ] Dimension 4 Typography: PASS
|
||
- [ ] Dimension 5 Spacing: PASS
|
||
- [ ] Dimension 6 Registry Safety: PASS
|
||
|
||
**Approval:** pending
|