---
phase: 8
slug: ipc-contract-reshape
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-05
---

# Phase 8 — UI Design Contract

> Visual and interaction contract for the Phase 8 client UI surfaces (driver-health pane + rewired settings controls). Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Important:** This is a Dear ImGui (immediate-mode) Win32 + D3D11 desktop client, not a web app. shadcn does not apply. Tokens below are expressed as ImGui constants and ImVec4 RGBA values used at draw time.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none (Dear ImGui immediate mode) |
| Preset | not applicable |
| Component library | Dear ImGui (existing widgets: `Text`, `Button`, `Combo`, `SliderInt`, `ProgressBar`, `Separator`, `Spacing`) |
| Icon library | none — emoji/text glyphs only (no font atlas additions in P8) |
| Font | Default ImGui font (ProggyClean baked atlas) — unchanged from v1.5 |
| Theme | `ImGui::StyleColorsDark()` baseline (unchanged from v1.5; see `apps/micmap/main.cpp:914`) |

**Layout shell:** Single full-window panel (`ImGui::Begin("MicMap", ...)` with `NoTitleBar | NoResize | NoMove | NoCollapse`) sized to display. Sections separated by `ImGui::Spacing()` + `ImGui::Separator()`. Phase 8 keeps the existing top-down section order and inserts a new **Driver Health** section above the existing **Audio Device** block.

**Section order (post-P8):**
1. Status (existing — extended)
2. **Driver Health** (NEW — HEALTH-01..07)
3. Audio Device (existing — device picker rewired to `GET /devices`)
4. Settings (existing — controls rewired to `PUT /settings`)
5. Training (existing — unchanged in P8; P9 reshapes)
6. Audio Levels (existing — `currentLevelDb` source switches to `/telemetry/level` poll)
7. Detection State Box (existing — content sourced from `/state.detection_state`)

---

## Spacing Scale

ImGui spacing tokens declared for P8 (used as constants in pixel-space ImVec2 arguments):

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Inline gaps inside a row (e.g., indicator dot to label) |
| sm | 8px | `ImGui::Spacing()` between related elements within a section |
| md | 16px | Default section-internal padding (button height padding, control row gaps) |
| lg | 24px | Between major sections (used after `Separator()` before next `Text(heading)`) |
| xl | 32px | Reserved — not used in P8 |

**Control sizing:**
| Element | Size | Notes |
|---------|------|-------|
| Standard button | `ImVec2(120, 30)` | Matches existing v1.5 "Stop Training" / "Train Pattern" buttons |
| Compact button (e.g., "Clear error") | `ImVec2(80, 24)` | New for HEALTH-05 |
| Slider/Combo width | `-1` (full width via `SetNextItemWidth(-1)`) | Matches v1.5 pattern |
| Detection state box | `ImVec2(-1, 50)` | Existing — unchanged |
| Level meter (RMS/dBFS) | `ImVec2(-1, 18)` | Existing `ProgressBar` height; unchanged |
| Indicator dot | inline text glyph (no custom size) | Use `●` (U+25CF) colored via `TextColored` |

Exceptions: none. All values are multiples of 4.

---

## Typography

ImGui has a single baked font in this project. "Sizes" here are role labels — visual hierarchy comes from layout (Separator + Spacing) and color, not from font size. Phase 8 declares 2 weights via emphasis usage, not 3-4 sizes.

| Role | Size | Weight | Line Height | ImGui Construct |
|------|------|--------|-------------|------------------|
| Body | 13px (default ProggyClean) | regular | ~1.0 (font default) | `ImGui::Text(...)` |
| Section heading | 13px (same atlas) | regular, used as heading by position | ~1.0 | `ImGui::Text("Driver Health")` followed by `ImGui::Separator()` |
| Emphasis (state/error) | 13px | regular | ~1.0 | `ImGui::TextColored(color, ...)` — color *is* the emphasis |
| Disabled / hint | 13px | regular, alpha 0.5 | ~1.0 | `ImGui::TextDisabled(...)` |

**Rationale for not declaring 3-4 sizes:** The v1.5 ImGui layer uses one font size; introducing custom font atlases is out of P8 scope. Visual hierarchy is delivered by `Separator()` + `Spacing()` + color emphasis, which is the established pattern in `apps/micmap/main.cpp`.

---

## Color

ImGui dark theme (`StyleColorsDark`) provides the dominant + secondary surface colors. Phase 8 declares only the foreground colors used inside `TextColored` and `PushStyleColor` calls.

| Role | Value (RGBA float) | Hex equivalent | Usage |
|------|-------------------|---------------|-------|
| Dominant surface (60%) | window bg `(0.06, 0.06, 0.06, 0.94)` | ~#0F0F0F | ImGui dark theme default — main panel background |
| Secondary surface (30%) | frame bg `(0.16, 0.29, 0.48, 0.54)` | ~#294A7B | ImGui dark theme default — combo/slider/button backgrounds |
| Accent green (10%) | `ImVec4(0, 1, 0, 1)` | #00FF00 | Reserved for: driver-loaded indicator (green), SteamVR-running indicator (green), detection-state pill = `triggered`, "Status: Profile trained" |
| Accent orange (warning, 10%) | `ImVec4(1, 0.5f, 0, 1)` | #FF8000 | Reserved for: driver-loaded indicator (red/down), detection-state pill = `cooldown`, "device disappeared" indicator, training in-progress copy |
| Detection-yellow | `ImVec4(1, 0.78f, 0, 1)` | #FFC700 | Reserved for: detection state box "detected but below threshold" (existing v1.5 use; P8 preserves) |
| Detection-green (triggered box) | `ImVec4(0, 0.78f, 0, 1)` | #00C700 | Reserved for: detection state box "triggered" fill (existing v1.5; P8 preserves) |
| Detection-neutral (idle box) | `ImVec4(0.24f, 0.24f, 0.24f, 1)` | #3D3D3D | Reserved for: detection state box "idle/no signal" (existing v1.5; P8 preserves) |
| Destructive | `ImVec4(0.86f, 0.20f, 0.20f, 1)` | #DC3333 | Reserved for: `last_error` text (HEALTH-05) and "Driver not loaded" disabled-state tooltip border |
| Disabled foreground | ImGui style `Text Disabled` `(0.5, 0.5, 0.5, 1)` | #808080 | Settings controls when driver-loaded indicator is red (D-09 gate) |

**Accent reserved for (explicit list):**
- Driver-loaded indicator dot (HEALTH-01) — green when `/health` returns 200, orange when `ECONNREFUSED`
- SteamVR-running indicator dot (HEALTH-02) — green when reachable, orange when unreachable (derived from same poll)
- Detection-state pill text color (HEALTH-03) — green only when state is `triggered`; orange only when state is `cooldown`; default body color when `idle`/`detecting`/`training`
- Detection state box fill (existing v1.5 — preserved)

**60/30/10 audit:**
- Dominant (60%) = window background — covers full panel
- Secondary (30%) = frame backgrounds — covers all interactive widget chrome (combos, sliders, buttons in their non-emphasized state)
- Accent (10%) = green + orange + destructive — limited to indicators, state pill, error text, and the detection state box (which is the single intentional "loud" affordance)

---

## Copywriting Contract

All P8 copywriting is in en-US, sentence case, no terminal periods in inline status strings. Time formatting for `last_trigger_at` follows the relative-timestamp ladder spec'd below.

### Indicators (HEALTH-01..02, HEALTH-07)

| Element | Copy |
|---------|------|
| Driver-loaded indicator (green) | `Driver: Loaded` |
| Driver-loaded indicator (red) | `Driver: Not loaded — install or enable in SteamVR` |
| SteamVR-running indicator (green) | `SteamVR: Running` |
| SteamVR-running indicator (red) | `SteamVR: Not running` |
| Device disappeared (HEALTH-07) | `Audio device unavailable — Re-pick device` (button label: `Re-pick device`) |
| Permission denied (HEALTH-07) | `Mic access blocked — open Windows mic settings` (button label: `Re-pick device`; deep-link copy deferred to P10 FAIL-01) |

### Detection state pill (HEALTH-03)

Single source: `GET /state.detection_state`. Pill copy is the literal state name in title case:

| State | Copy | Color |
|-------|------|-------|
| `idle` | `Idle` | body default |
| `training` | `Training` | body default |
| `detecting` | `Detecting` | body default |
| `triggered` | `Triggered` | accent green |
| `cooldown` | `Cooldown` | accent orange |

### Last-trigger relative timestamp (HEALTH-04)

Source: `GET /state.last_trigger_at` (UTC ISO-8601 string or null). Client formats to relative:

| Condition | Copy |
|-----------|------|
| `last_trigger_at` is null | `Last trigger: —` |
| < 5 seconds ago | `Last trigger: just now` |
| < 60 seconds ago | `Last trigger: {N} s ago` |
| < 60 minutes ago | `Last trigger: {N} m ago` |
| < 24 hours ago | `Last trigger: {N} h ago` |
| ≥ 24 hours ago | `Last trigger: {N} d ago` |

### Last-error display (HEALTH-05)

| Element | Copy |
|---------|------|
| Heading (when `last_error` non-null) | `Last error` |
| Body | `{driver-supplied error string}` (rendered in destructive color, single line, ellipsize at panel width) |
| Clear button label | `Clear` |
| Heading (when `last_error` null) | section hidden — no copy |

### Settings rewiring (D-09)

| Element | Copy |
|---------|------|
| Slider on-change success | (silent — no toast; UI reflects value) |
| Slider on-change validation failure (HTTP 400) | `Invalid {field}: {reason}` (orange, ephemeral 3 s, below the affected slider) |
| Slider on-change ECONNREFUSED | (no message — controls are disabled per gate; should not be reachable) |
| Settings disabled tooltip (D-09 gate) | `Driver not loaded — settings cannot be changed` |

### Empty / first-run states

| Element | Copy |
|---------|------|
| Detection-state pill before first poll | `—` (em-dash, body default) |
| Level meter before first poll | `Input level: — dB` with empty progress bar |
| Device list before driver replies | `Loading devices…` (combo placeholder, disabled) |

### Destructive confirmations

P8 has **no destructive actions** in the new HEALTH pane (clearing `last_error` is non-destructive — it dismisses a banner; the underlying error condition either still exists and re-fires, or is gone). No confirmation dialogs added in P8.

| Action | Confirmation |
|--------|--------------|
| Clear last error | none — direct `POST /state/clear-error` on click |

P10 owns `POST /button` deletion and any new destructive UX (FAIL cluster). P8 ships none.

### Primary CTA

Phase 8 has no single primary CTA — it's a settings/health surface, not a flow. The most prominent affordance is the existing **Train Pattern** button (unchanged from v1.5; P9 reshapes). For P8 design-quality scoring purposes the "primary CTA" slot is filled by the **Clear** button on `last_error` (HEALTH-05) when an error is present.

---

## Interaction Contracts

### Poll cadences (D-26)

| Endpoint | Visible cadence | Tray-minimized cadence | Client-side timeout |
|----------|----------------|------------------------|---------------------|
| `GET /health` | 1 Hz | 1 Hz | 250 ms |
| `GET /state` | 2 Hz | 0.5 Hz | 250 ms |
| `GET /telemetry/level` | 5 Hz | 0.5 Hz | 250 ms |
| `GET /settings` | on UI open + after PUT 200 | — | 500 ms |
| `GET /devices` | on UI open + on device-disappeared | — | 500 ms |

**Visibility detection:** `IsIconic(hwnd)` (Win32) — when true, drop to tray cadences. Existing tray icon hookup in `apps/micmap/main.cpp` provides the hwnd.

### Driver-loaded gate (D-09)

When the driver-loaded indicator is red (`ECONNREFUSED` on `GET /health`):

1. All Settings section controls (sensitivity slider, threshold, cooldown, min-duration, device picker) render in disabled state.
2. Disabled state = ImGui style `Text Disabled` color + `BeginDisabled()` wrap.
3. Hover tooltip on any disabled control: `Driver not loaded — settings cannot be changed`.
4. Driver Health pane itself remains visible and continues polling (1 Hz).
5. Audio Levels meter shows last-known value frozen with `(stale)` suffix; resumes live updates when driver reconnects.

### Settings PUT round-trip (D-09)

On user input to a settings control (slider release, combo selection, threshold edit committed):

1. Client constructs full `AppConfig` from current in-memory snapshot + the changed field.
2. Client issues `PUT /settings` with full config payload.
3. **HTTP 200:** Optimistically write the new value into client's in-memory `ConfigManager` snapshot. UI shows the new value (no rollback).
4. **HTTP 400:** Roll the control back to its prior value. Show ephemeral 3 s message below the control: `Invalid {field}: {reason}` (orange).
5. **ECONNREFUSED:** Should not be reachable (gate prevents). If observed, roll back silently and let the next `/health` poll flip the gate.

### Level meter rendering (HEALTH-06)

| Property | Value |
|----------|-------|
| Source | `GET /telemetry/level` → `{rms_normalized: float [0,1], dbfs: float}` |
| Widget | `ImGui::ProgressBar(rms_normalized, ImVec2(-1, 18))` |
| Label | `Input level: {dbfs:.1f} dB` above the bar |
| Stale state | If last poll > 1 s old, render bar at last value with overlay text `(stale)` |

### Device picker rewire (D-13)

| Property | Value |
|----------|-------|
| Source | `GET /devices` (driver's WASAPI enumeration), 1 s server-side cache |
| Trigger | UI open (one fetch on panel mount); on `audio_device_state ∈ {missing, permission_denied}` (re-fetch) |
| Selection commit | `PUT /settings` with new `audio_device_id`; round-trip per D-09 |
| Headless fallback | `mic_test.exe` retains client-side WASAPI enumeration (D-13 — not deleted) |

---

## Registry Safety

Not applicable. This phase ships zero new third-party UI components. The client uses Dear ImGui (already vendored, MIT-licensed, in-tree since v1.0) and adds no registries or component imports.

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| Dear ImGui (in-tree, vendored) | existing widgets only — no new widgets | not required (in-tree, no fetch) |
| shadcn official | none | not applicable (no web stack) |
| Third-party | none | not applicable |

---

## Out-of-Scope (Phase 8 UI)

Explicit guardrails for the executor — these belong to P10 or later and must NOT be added in P8:

- **Tray icon state glyphs** (HEALTH-08) — P10. P8 keeps the v1.5 single-icon tray.
- **FAIL-01..05 graceful failure UX** — deep-link copy ("open Windows mic settings"), retry-storm prevention, named-mutex foregrounding — all P10.
- **Custom font atlas / multiple type sizes** — out of P8 scope. Default ProggyClean only.
- **Multi-error aggregation in PUT 400 response** — single-field reporting only (D-14).
- **Rich `last_error` (severity, code, history)** — single string + clear (D-16).
- **In-VR overlay / settings-in-VR** — out of v1.6 entirely (UX-02 deferred).

---

## Pre-Population Sources

| Source | Decisions Used |
|--------|---------------|
| `08-CONTEXT.md` D-09 | Settings gate + optimistic apply + 4xx/ECONNREFUSED handling |
| `08-CONTEXT.md` D-11 | HEALTH-01..07 pane composition |
| `08-CONTEXT.md` D-13 | Device picker source switch + headless fallback |
| `08-CONTEXT.md` D-14 | All-or-nothing validation, single-field 400 envelope copy |
| `08-CONTEXT.md` D-16 | Monotonic `last_error` UX (show + clear) |
| `08-CONTEXT.md` D-26 | Poll cadences (visible + tray) |
| `REQUIREMENTS.md` HEALTH-01..07 | Indicator copy + level meter cadence |
| `REQUIREMENTS.md` IPC-01 | `detection_state` enum values for pill |
| `apps/micmap/main.cpp` (existing) | Section ordering, ImGui style baseline, button sizes (120×30), color conventions for state box (`ImVec4(0,0.78f,0,1)` etc.) |

---

## 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
