M3: Devices page and phone-pairing QR (ENG-258) #23

Merged demi.dev
targeting main from stoopkidddd/eng-258-m3-devices-page-and-phone-pairing-qr
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-258-m3-devices-page-and-phone-pairing-qr

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

What's in this PR (all under apps/web/, plus the lockfile):

  • devices/: /devices list (platform icon, name, "This device" badge, online state from DeviceDTO.online + device_updated, last-seen relative time), inline rename (PATCH), in-place-confirmed remove (DELETE; removing this device signs out), and a device reducer for device_* frames.
  • "Add a phone" modal: POST /api/pair → buildPairingUri from @pneumo/crypto → QR (qrcode, error correction M), a 10-minute countdown and New code, closes on a matching device_added and highlights the new device. The raw URI never enters the DOM, a link or the clipboard.
  • A browser removed elsewhere lands on /login with "This browser was removed from Pneumo."
  • Settings: this device's name is editable.
  • Reuses the existing device list and the single WebSocket (home/useHome.ts, App.tsx).

Verify (lead, on cba2c93, based on e5f90f2):

  • pnpm install --frozen-lockfile → exit 0
  • mise run check:web → exit 0 (Tests 80 passed (80); build OK)
  • mise run e2e:web → exit 0 (20 passed: rename persists across reload, removal → /login with the message, QR decoded with jsQR and parsed with parsePairingUri (u = origin, k = session MK), real claim closes the modal and shows the device, axe)
  • Screenshots (not committed): devices-list, devices-rename, devices-remove-confirm, devices-pairing, devices-paired, devices-revoked-login, devices-self-removed-login, settings-device-name (light/dark/mobile).

Deviations: none. Settings stays in views/SettingsView.tsx.

🤖 Generated with Claude Code

Commits 0