docs(12): revise UI-SPEC for collect-and-write wizard model

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>
This commit is contained in:
Lucas Berger
2026-06-15 13:09:25 -04:00
co-authored by Claude Sonnet 4.6
parent 513fc887e4
commit 0f3c3784e6
@@ -5,12 +5,18 @@ status: draft
shadcn_initialized: false shadcn_initialized: false
preset: none preset: none
created: 2026-06-14 created: 2026-06-14
updated: 2026-06-15
--- ---
# Phase 12 — UI Design Contract: Initial Setup Wizard # Phase 12 — UI Design Contract: Initial Setup Wizard
> Visual and interaction contract for the first-run setup wizard. > Visual and interaction contract for the first-run setup wizard.
> Generated by gsd-ui-researcher. Verified by gsd-ui-checker. > 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 34 are reworked to collect config via input fields, not just validate
> env. All other design tokens, surfaces, and a11y contracts are unchanged.
--- ---
@@ -60,10 +66,8 @@ Uses the existing 4px-based scale. No new tokens. Values from `tokens.css`:
Exceptions: Exceptions:
- Wizard card max-width: 540px (slightly wider than CredentialSheet 480px to accommodate - Wizard card max-width: 540px (slightly wider than CredentialSheet 480px to accommodate
multi-field steps and generated-secret blocks). multi-field steps).
- Step indicator touch targets: 44px minimum (accessibility). - 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`.
--- ---
@@ -80,11 +84,10 @@ All values from `tokens.css`. No new sizes or weights.
Usage in this phase: Usage in this phase:
- Wizard page title ("FamilySync Setup"): Display (24px/600/1.2) - Wizard page title ("FamilySync Setup"): Display (24px/600/1.2)
- Step heading (e.g. "Generate Secrets"): Heading (18px/600/1.25) - Step heading (e.g. "OIDC & App URL"): Heading (18px/600/1.25)
- Step description / helper text: Body (15px/400/1.5) - Step description / helper text: Body (15px/400/1.5)
- Field labels, step counter, status badges: Label (13px/400/1.4) — labels use weight 600 - 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 4"): Label (13px/600) with
- Section labels (uppercase caps, e.g. "STEP 2 OF 5"): Label (13px/600) with
`text-transform: uppercase; letter-spacing: 0.06em` (AdminPage `sectionLabelStyle` pattern) `text-transform: uppercase; letter-spacing: 0.06em` (AdminPage `sectionLabelStyle` pattern)
--- ---
@@ -96,8 +99,8 @@ All values from `tokens.css`. No new hex values.
| Role | Value | Variable | Usage | | Role | Value | Variable | Usage |
|------|-------|----------|-------| |------|-------|----------|-------|
| Dominant (60%) | #ffffff | var(--color-surface) | Page background, card background | | 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 | | 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, copy button, links | | 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) | | Destructive | #dc2626 | var(--color-destructive) | Validation failure border + helper text (same CredentialSheet pattern) |
Accent reserved for: Accent reserved for:
@@ -105,7 +108,6 @@ Accent reserved for:
- Active wizard step indicator (filled circle) - Active wizard step indicator (filled circle)
- Inline spinner (`Loader2`) during async validation - Inline spinner (`Loader2`) during async validation
- Hyperlinks (e.g. "Get an app password") - Hyperlinks (e.g. "Get an app password")
- Copy-to-clipboard button icon
- Focus ring (`var(--color-focus-ring): #4a90d9`) - Focus ring (`var(--color-focus-ring): #4a90d9`)
Additional semantic colors (not new — already in tokens.css): Additional semantic colors (not new — already in tokens.css):
@@ -120,13 +122,11 @@ Additional semantic colors (not new — already in tokens.css):
## Surface Architecture ## Surface Architecture
The wizard is a **standalone full-page route** (`/setup`) mounted in a separate React root or 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 / an App-level gate (see Routing section). It renders none of the AppNav / BottomTabBar /
SetupBanner chrome. SetupBanner chrome.
### Surface 1 — Wizard Page Shell ### Surface 1 — Wizard Page Shell
The wizard page itself.
- Background: `var(--color-surface)` (#ffffff) - Background: `var(--color-surface)` (#ffffff)
- Layout: vertically centered column, `min-height: 100dvh` - Layout: vertically centered column, `min-height: 100dvh`
- Content column: `maxWidth: 540px`, `margin: 0 auto`, - Content column: `maxWidth: 540px`, `margin: 0 auto`,
@@ -140,7 +140,7 @@ The wizard page itself.
Linear step tracker shown above the active step card. Linear step tracker shown above the active step card.
- Horizontal row of N step circles connected by lines - Horizontal row of 4 step circles connected by lines
- Completed step: filled circle `var(--color-member-0)` with white `Check` icon (16px) - 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) - 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 - Upcoming step: circle with `var(--color-border)` 2px border, `var(--color-text-muted)` step
@@ -152,12 +152,11 @@ Linear step tracker shown above the active step card.
- Circle size: 28px diameter; connector height: 1px; minimum row height: 44px touch target - Circle size: 28px diameter; connector height: 1px; minimum row height: 44px touch target
achieved by centering in a 44px tall row achieved by centering in a 44px tall row
Step labels (5 steps total): Step labels (4 steps total):
1. Welcome 1. Welcome
2. Secrets 2. Instance
3. Database 3. Calendar
4. OIDC 4. Complete
5. Calendar
### Surface 3 — Step Card ### Surface 3 — Step Card
@@ -174,27 +173,23 @@ The active step's input/content area. One card rendered at a time.
`marginBottom: var(--space-6)` (24px) `marginBottom: var(--space-6)` (24px)
- Field group spacing: `var(--space-4)` (16px) between fields - Field group spacing: `var(--space-4)` (16px) between fields
### Surface 4 — Generated-Secret Block ### Surface 4 — Input Field
Used in Step 2 (Secrets) for values the operator must copy into env. Standard text input used across steps 23 to collect config.
- Background: `var(--color-surface-dim)` (#f7f7f8) - Width: 100%, `box-sizing: border-box`
- Border: `1px solid var(--color-border-subtle)` (#eceef2) - Padding: `var(--space-3, 12px) var(--space-4, 16px)` (matches CredentialSheet pattern)
- Border-radius: 4px (`var(--space-1)`) - Border: `1px solid var(--color-border)` default; `1px solid var(--color-destructive)` on
- Padding: `var(--space-3) var(--space-4)` (12px 16px) validation error
- Secret value: monospace, 13px/400, `var(--color-text-primary)`, word-break: break-all - Border-radius: `var(--space-1, 4px)` (4px)
(VAPID keys are long strings) - Font: 15px/400, `var(--color-text-primary)`, `var(--font-family-base)`
- Label above block: Label (13px/600), `var(--color-text-primary)` - Background: `var(--color-surface)`
- Copy button: icon-only (`Copy` icon 16px, `var(--color-member-0)`), positioned top-right - Label above: 13px/600, `var(--color-text-primary)`, `marginBottom: var(--space-1)` (4px)
inside the block, `minWidth: 44px`, `minHeight: 36px` (acceptable — paired with large block) - Helper text below: 13px/400, `var(--color-text-secondary)`
- 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 ### Surface 5 — Validation State Row
Shown after the operator submits a validation step (DB / OIDC / CalDAV). 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`) + - 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 Body (15px/400) status text in `var(--color-text-secondary)` — inline row
@@ -203,6 +198,7 @@ Shown after the operator submits a validation step (DB / OIDC / CalDAV).
- Failure: `AlertCircle` icon (16px, `var(--color-destructive)`) + error text Body (15px/400) in - Failure: `AlertCircle` icon (16px, `var(--color-destructive)`) + error text Body (15px/400) in
`var(--color-destructive)` — same pattern as CredentialSheet `FAILURE_TEXT` `var(--color-destructive)` — same pattern as CredentialSheet `FAILURE_TEXT`
- Layout: `display: flex; alignItems: center; gap: var(--space-2, 8px)` (CredentialSheet pattern) - Layout: `display: flex; alignItems: center; gap: var(--space-2, 8px)` (CredentialSheet pattern)
- Container: `role="status"` with `aria-live="polite"`
### Surface 6 — Action Row ### Surface 6 — Action Row
@@ -218,11 +214,11 @@ Bottom of each step card.
`borderRadius: var(--space-1)` (4px), `transition: background 0.15s ease` `borderRadius: var(--space-1)` (4px), `transition: background 0.15s ease`
— disabled state: `background: var(--color-border)` (#e2e4e9), `cursor: default` — disabled state: `background: var(--color-border)` (#e2e4e9), `cursor: default`
(AdminPage / CredentialSheet pattern) (AdminPage / CredentialSheet pattern)
- "Continue" label on steps 14; "Complete Setup" label on step 5 - "Continue" label on steps 13; "Complete Setup" label on step 4
### Surface 7 — Terminal "Setup Complete" Screen ### Surface 7 — Terminal "Setup Complete" Screen
Replaces the wizard card after step 5 completes successfully. Replaces the wizard card after step 4 completes successfully.
- Icon: `ShieldCheck` (48px, `var(--color-member-0)`) centered - Icon: `ShieldCheck` (48px, `var(--color-member-0)`) centered
- Heading: "Setup complete" — Display (24px/600), centered, `marginTop: var(--space-4)`, - Heading: "Setup complete" — Display (24px/600), centered, `marginTop: var(--space-4)`,
@@ -249,78 +245,91 @@ Shown when the operator navigates to `/setup` after `setup_complete = true` (423
## Wizard Steps — Detailed Interaction Contract ## 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 ### Step 1: Welcome
Purpose: orient the operator; no inputs; no validation. Purpose: orient the operator; no inputs; no validation.
- Heading: "Welcome to FamilySync Setup" - Heading: "Welcome to FamilySync Setup"
- Description: "This wizard will guide you through configuring your self-hosted instance. - Description: "This wizard will guide you through configuring your self-hosted instance. Before
You'll need: your OIDC client credentials (Authelia), a Fastmail account with an app password, continuing, run `npm run generate-secrets` from the repo to generate your instance secrets and
and a copy of your `docker-compose.yml` to paste generated secrets into. This takes about add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia)
5 minutes." 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. - No input fields.
- Continue button: always enabled. - Continue button: always enabled.
### Step 2: Generate Secrets ### Step 2: Instance Configuration
Purpose: display generated session secret, encryption key, and VAPID keypair; operator copies Purpose: collect non-secret runtime config that the wizard writes to `app_config`. No secrets
each into env. are collected here. Validates DB connectivity and OIDC discovery.
- Heading: "Generated Secrets" - Heading: "Instance Configuration"
- Description: "These values are generated once and displayed now. Copy each into your - Description: "Enter your instance's connection details. These are written to the database —
`docker-compose.yml` environment block before continuing. They will never be shown again and not your environment file."
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 **Fields (collected and written to `app_config`):**
Purpose: verify the DB connection configured in env is reachable. 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`
- Heading: "Database Connection" 2. **OIDC Issuer**
- Description: "Verify that the app can reach the MariaDB database configured in your - Label: "OIDC issuer URL"
environment. No changes are made — this is a read-only connectivity check." - Type: `text`, placeholder: `https://auth.example.com`
- No operator input fields (DB creds come from env, not from this UI). - Helper: "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration`
- "Test Connection" button (primary filled, full-width on this step — replace normal action row): from this URL."
triggers `POST /api/setup/validate/db` - `app_config` key: `oidc_issuer`
- Validation state row (Surface 5) shown below the description during/after the test:
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…" - Pending: "Testing database connection…"
- Success: "Database connection verified." - Success: "Database connection verified."
- Failure: "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your - Failure: "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your
`docker-compose.yml` and try again." Docker environment 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 2. **OIDC discovery**`POST /api/setup/validate/oidc` (runs after DB success)
- Validation state row (Surface 5):
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…" - Pending: "Checking OIDC discovery…"
- Success: "OIDC discovery resolved." - Success: "OIDC discovery resolved."
- Failure: "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia - Failure: "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from
is reachable." the server."
- VAPID: label "VAPID keypair", `POST /api/setup/validate/vapid`
- Pending: "Checking VAPID keypair…" - Action button label on this step: "Save & Validate" (primary filled, full-width on this
- Success: "VAPID keypair is valid." step — replace the normal right-aligned action row with a full-width button above the validation
- Failure: "VAPID private key could not be verified. Ensure you copied both keys from Step 2 state rows, then normal Continue/Back row appears below once both pass)
into your environment and restarted the container." - Continue appears (enabled) only when both validation rows show success and config has been saved
- "Validate" button triggers both checks in sequence. (`POST /api/setup/config` call completes before validation begins).
- Continue appears (and is enabled) only when both rows show success.
- Back is available. - Back is available.
### Step 5: Calendar Credential ### Step 3: Calendar Credential
Purpose: set the first member's Fastmail app password; validate against CalDAV PROPFIND. Purpose: set the first member's Fastmail app password; validate against CalDAV PROPFIND.
Reuses the CredentialSheet interaction pattern (same fields, same validation feedback, Reuses the CredentialSheet interaction pattern (same fields, same validation feedback,
@@ -344,10 +353,16 @@ same copy).
- Failure: "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & - Failure: "Invalid password — CalDAV validation failed. Check the scope is 'Calendars &
Contacts (CalDAV)' and try again." (in `var(--color-destructive)`) Contacts (CalDAV)' and try again." (in `var(--color-destructive)`)
- Continue button label on this step: "Complete Setup" - Continue button label on this step: "Complete Setup"
- On continue: `POST /api/setup/complete` — promotes operator to admin, sets - On continue: `POST /api/setup/complete` — provisions the pre-OIDC local user + credential,
`app_config.setup_complete`, redirects to Surface 7 (Terminal Screen) flips `app_config.setup_complete`, redirects to Surface 7 (Terminal Screen)
- Back is available. - 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 ## Routing & App-Level Gate
@@ -374,8 +389,9 @@ contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeni
|---------|------| |---------|------|
| Page title | "FamilySync Setup" | | Page title | "FamilySync Setup" |
| Page subtitle | "Let's get your instance ready." | | Page subtitle | "Let's get your instance ready." |
| Primary CTA (steps 14) | "Continue" | | Primary CTA (steps 12) | "Continue" |
| Primary CTA (step 5) | "Complete Setup" | | Step 2 action button | "Save & Validate" |
| Primary CTA (step 3) | "Complete Setup" |
| Secondary action | "Back" | | Secondary action | "Back" |
| Terminal heading | "Setup complete" | | Terminal heading | "Setup complete" |
| Terminal body | "Your FamilySync instance is ready. Sign in to continue." | | Terminal body | "Your FamilySync instance is ready. Sign in to continue." |
@@ -384,36 +400,40 @@ contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeni
| Locked body | "This instance has already been configured. Sign in to continue." | | Locked body | "This instance has already been configured. Sign in to continue." |
| Locked link | "Sign in" | | Locked link | "Sign in" |
| Step 1 heading | "Welcome to FamilySync Setup" | | 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 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 2 heading | "Generated Secrets" | | Step 1 pre-start label | "Before you start" |
| 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 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 acknowledgement | "I have copied this value into my `.env` file." | | Step 2 heading | "Instance Configuration" |
| Step 3 heading | "Database Connection" | | Step 2 description | "Enter your instance's connection details. These are written to the database — not your environment file." |
| 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 2 field: App URL label | "App URL" |
| Step 3 CTA | "Test Connection" | | Step 2 field: App URL placeholder | "https://familysync.example.com" |
| Step 3 pending | "Testing database connection…" | | Step 2 field: App URL helper | "The public URL where FamilySync is reachable." |
| Step 3 success | "Database connection verified." | | Step 2 field: OIDC issuer label | "OIDC issuer URL" |
| 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 2 field: OIDC issuer placeholder | "https://auth.example.com" |
| Step 4 heading | "OIDC & Push" | | Step 2 field: OIDC issuer helper | "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration` from this URL." |
| 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 2 field: OIDC client ID label | "OIDC client ID" |
| Step 4 CTA | "Validate" | | Step 2 field: OIDC client ID placeholder | "familysync" |
| Step 4 OIDC pending | "Checking OIDC discovery…" | | Step 2 field: OIDC client ID helper | "The client ID registered in Authelia for this application." |
| Step 4 OIDC success | "OIDC discovery resolved." | | Step 2 field: VAPID public key label | "VAPID public key" |
| Step 4 OIDC failure | "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia is reachable." | | Step 2 field: VAPID public key placeholder | "BH…" |
| Step 4 VAPID pending | "Checking VAPID keypair…" | | Step 2 field: VAPID public key helper | "Paste the `VAPID_PUBLIC_KEY` value from `npm run generate-secrets`." |
| Step 4 VAPID success | "VAPID keypair is valid." | | Step 2 DB pending | "Testing database connection…" |
| 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 2 DB success | "Database connection verified." |
| Step 5 heading | "Fastmail Credential" | | 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 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 2 OIDC pending | "Checking OIDC discovery…" |
| Step 5 helper text | "Enter the Fastmail app password scoped to Calendars/CalDAV." | | Step 2 OIDC success | "OIDC discovery resolved." |
| Step 5 helper link text | "Get an app password" | | Step 2 OIDC failure | "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server." |
| Step 5 helper link suffix | " — choose the 'Calendars & Contacts (CalDAV)' scope." | | Step 3 heading | "Fastmail Credential" |
| Step 5 pending | "Validating against CalDAV…" | | 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 5 success | "Credential verified." | | Step 3 helper text | "Enter the Fastmail app password scoped to Calendars/CalDAV." |
| Step 5 failure | "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." | | Step 3 helper link text | "Get an app password" |
| Empty state (none for wizard — every step has explicit content) | N/A | | 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) | | 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. | | Destructive actions | None — wizard has no destructive actions. "Complete Setup" is irreversible in effect but not destructive; no confirmation dialog required. |
--- ---
@@ -426,7 +446,6 @@ contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeni
- Heading hierarchy: `<h1>` for page title, `<h2>` for step heading - Heading hierarchy: `<h1>` for page title, `<h2>` for step heading
- All inputs: explicit `<label htmlFor>` association (same CredentialSheet pattern) - All inputs: explicit `<label htmlFor>` association (same CredentialSheet pattern)
- Disabled buttons: `disabled` attribute (not just pointer-events: none) - Disabled buttons: `disabled` attribute (not just pointer-events: none)
- Copy button: `aria-label="Copy {field name}"`, swaps to `aria-label="Copied"` for 2 seconds
- Validation state row: `role="status"` with `aria-live="polite"` so screen readers announce - Validation state row: `role="status"` with `aria-live="polite"` so screen readers announce
results without focus movement results without focus movement
- Escape key: no sheet to close on this page; Escape has no effect in wizard - Escape key: no sheet to close on this page; Escape has no effect in wizard
@@ -435,7 +454,6 @@ contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeni
- Minimum touch targets: 44px on all interactive elements (`minHeight: 44px`, `minWidth: 44px`) - 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 - Focus ring: `var(--color-focus-ring)` (#4a90d9), 2px outline, 2px offset on all focusable
elements (same project convention) elements (same project convention)
- Secret textarea (if used instead of div): `readonly`, `aria-label="{field name} value"`
--- ---
@@ -446,7 +464,7 @@ infrastructure), but must be usable on a phone if needed.
- Desktop (≥768px): card centered at maxWidth 540px; step indicator spans full card width - Desktop (≥768px): card centered at maxWidth 540px; step indicator spans full card width
- Phone (<768px): card fills viewport minus 24px horizontal padding; - Phone (<768px): card fills viewport minus 24px horizontal padding;
step indicator uses short labels (1, 2, 3, 4, 5) or icon-only to avoid overflow; 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 bottom tab bar (not rendered at all on wizard page)
- No BottomTabBar, no AppNav on this page at any breakpoint - No BottomTabBar, no AppNav on this page at any breakpoint
@@ -456,13 +474,12 @@ infrastructure), but must be usable on a phone if needed.
These are hard UI rules, not implementation notes: These are hard UI rules, not implementation notes:
- Secret values in Surface 4 blocks: displayed in a readonly `<textarea>` or `<div>` with - The wizard never displays generated secret values. Secrets (SESSION_SECRET,
`userSelect: all` for easy selection — never in a `type="password"` input (they must be APP_PASSWORD_ENCRYPTION_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY) are generated pre-boot by
visible to copy) the `npm run generate-secrets` helper and pasted into Docker env by the operator. The wizard
- App password in Step 5: `type="password"` — never visible 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) - No `dangerouslySetInnerHTML` anywhere on this page (T-05-24 project convention)
- Acknowledgement checkboxes enforce operator intent before Continue is enabled; this is a UX
gate only (not a security boundary — the secrets are already displayed)
--- ---
@@ -493,9 +510,12 @@ lucide-react dependency).
| Input style | CredentialSheet.tsx | 12px/16px padding, 4px border-radius, destructive border on error | | 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 | | 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 | | Card padding | AdminPage.tsx | var(--space-12) top/bottom, var(--space-6) horizontal |
| Bottom sheet pattern | CredentialSheet.tsx | 12px 12px 0 0 radius, zIndex 301, backdrop 300 |
| Credential copy | CredentialSheet.tsx | Same field layout, same validation feedback pattern | | Credential copy | CredentialSheet.tsx | Same field layout, same validation feedback pattern |
| Step 5 fields | CredentialSheet.tsx | Exact field structure, labels, helper text, link | | 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 |
--- ---