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 viaprefers-color-scheme, a visible 2 px focus ring, and a header with mark + wordmark + nav (Home, Devices, Settings), all perbrand/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/protocolroute table, bearer from the session,ErrorBody→ApiError, and a global 401 → clear the session and go to/login.src/session/store.ts: IndexedDBpneumo/sessionwith{ deviceId, token, mk }and a non-extractablecontentKey(§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
/apiand/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 0mise 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