M3: Web app shell — theme, API client, key store, setup and login (ENG-252) #18

Merged demi.dev
targeting main from stoopkidddd/eng-252-m3-web-app-shell-theme-api-client-key-store-setup-and-login
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-252-m3-web-app-shell-theme-api-client-key-store-setup-and-login

Linear: https://linear.app/engineereddev/issue/ENG-252

What's in this PR:

  • Theme: generated tokens.css, self-hosted Chivo 600 / Public Sans 400, base styles, light/dark via prefers-color-scheme, a visible 2 px focus ring, and a header with mark + wordmark + nav (Home, Devices, Settings), all per brand/DESIGN.md.
  • Router and guards from GET /api/health: uninitialized → /setup, signed out → /login, signed in → /.
  • src/api/client.ts: typed calls derived from the @pneumo/protocol route table, bearer from the session, ErrorBody → ApiError, and a global 401 → clear the session and go to /login.
  • src/session/store.ts: IndexedDB pneumo/session with { deviceId, token, mk } and a non-extractable contentKey (§6.9).
  • Setup (§7.2): token from #token=, a missing-token error, passphrase ≥ 12 (NFC) twice with show/hide, UA device name, busy state, then a one-time "can't be recovered" acknowledgment. Login (§7.3) with 401/429 messages. A Settings stub with Sign out.
  • Vite dev proxy for /api and /api/ws. Playwright harness: MinIO + built web + built server on random ports, UPLOAD_MODE=direct, a known setup token; shared fixtures; axe; mise run e2e:web.

Verify (lead, rebased on 555a737):

  • pnpm install --frozen-lockfile → exit 0
  • mise run check:web → exit 0 (Tests 18 passed (18); production build OK)
  • mise run e2e:web → exit 0 (8 passed: setup end to end, second-browser login, reload persistence, sign-out/re-login, wrong passphrase, missing token, rate limit, revocation sign-out, axe on every new view)
  • Screenshots (not committed, apps/web/test-results/screens/): setup, setup-missing-token, login, warning, home, devices, settings, each light and dark, plus mobile setup/login.
  • No pneumo-e2e-* containers left.

Deviations:

  • Passphrase length counts NFC-normalized characters, and confirmation compares normalized strings.
  • Setup saves the session only after the recovery warning is acknowledged.
  • Fresh-instance fixtures use extra isolated random-port servers with unique S3 prefixes.

🤖 Generated with Claude Code

Commits 3

chore: regenerate lockfile
M3: Fix client-side navigation and cover auth failures (ENG-252)
M3: Add branded web setup and login shell (ENG-252)