chore: dev-stack switches, shared e2e harness, and desktop shell rules

- `LIVE_RELOAD=false npm run dev` keeps the renderer alive across a machine
  suspend; the reload client otherwise destroys the session under test.
- `dev-peer.sh` plus a separate userdata dir runs a second local peer.
- `tools/voice-probe.js` samples peer state and RTP counters from a live
  window, persisting to localStorage so a renderer reload cannot erase it.
- e2e helpers for voice pairs, peer-role election, and a TURN relay.
- Electron single-instance and dev-client-load decisions move into rules
  files with colocated specs.
This commit is contained in:
2026-08-14 03:19:29 +02:00
parent d71e3a98da
commit e49b3ec112
41 changed files with 35947 additions and 50 deletions
+256
View File
@@ -0,0 +1,256 @@
# 02 — User journeys (how it is supposed to feel)
Each journey: **steps the user takes**, **what should happen**, **failure symptoms**, **where to dig**.
---
## J1 — First-time home login
**User steps**
1. Open app → login/register.
2. Pick (or accept default) signal server.
3. Register or log in with username/password.
4. Land on dashboard / saved servers.
**Supposed to happen**
- Session token stored for that URL (`metoyou.authTokens` + credential store).
- Local user profile scoped in DB; NgRx `currentUser` set with `homeSignalServerUrl`.
- **Provision secret** created and persisted (Electron safeStorage / web sessionStorage).
- Signing public key registered on **home** server when possible.
- WebSocket connects, `identify` with home token + `clientInstanceId`, ready for joins.
**Broken looks like**
- Stuck on login; bounce back after “success”.
- Dashboard with no token → later `SESSION_EXPIRED`.
- Later foreign joins always open authorize (missing secret).
**Code**
- `domains/authentication/` (`AuthenticationService`, login/register UI)
- `store/users/users.effects.ts` (`authenticateUser`, `prepareAuthenticatedUserStorage`, `ensureHomeProvisionSecret`)
- Feature: `agents-docs/features/authentication.md`
---
## J2 — Restart app still logged in
**User steps**
1. Quit Electron fully; reopen.
2. Expect same user without typing password.
**Supposed to happen**
- Load user from local DB + valid home token (credential store or legacy token fallback).
- Identify on home (and later foreign) sockets.
- Opportunistic `ensureProvisioned` for active endpoints **without** login UI.
**Broken looks like**
- Flash of dashboard then `/login`.
- Profile restored but chat/presence dead (“alone”).
- Foreign rooms immediately open `/login?mode=authorize`.
**Code**
- `loadCurrentUser$`, `hasValidPersistedSession`, `migrateHomeCredential`
- Lessons: identify legacy token fallback; persisted user still needs token
---
## J3 — Join a community on the *same* signal server
**User steps**
1. Discover or invite → Join.
2. Open a text channel; send “hello”.
3. Join a voice channel; talk.
**Supposed to happen**
- REST join with bearer; then WS `join_server`.
- Receive `server_users` / `user_joined`; peer mesh forms.
- Text: local add + DC `chat-message` + WS `chat_message` fallback.
- Voice: `voice_state` broadcast; WebRTC offer/answer; same-channel audio routing.
**Broken looks like**
- Joined in UI but not in others member lists (identify/join race).
- Messages only on sender device.
- Voice tile appears, no audio.
---
## J4 — Join a community on a *different* signal server (critical)
**User steps**
1. Already logged into home Signal A.
2. Open invite / browser card whose `sourceUrl` is Signal B.
3. Join and chat/voice.
**Supposed to happen**
1. `ensureCredentialForServerUrl(B)` → silent provision (or reuse credential).
2. REST + WS use **actor id on B**, not home id.
3. No authorize login page.
4. Peers on B see actor display name (may show `#prefix` disambiguation).
5. Voice/chat use Bs WebSocket for that rooms affinity.
**Broken looks like**
- Redirect to `/login?mode=authorize&serverId=…` while user bar still shows logged in.
- Join “succeeds” locally but invisible on B.
- Can see members but WebRTC never connects (initiator/identity mismatch).
- DMs/calls to that person later miss rings or fork conversations.
**Code**
- `SignalServerAuthorizeService`, `SignalServerProvisionerService`, `room-signaling-connection.ts`
- Story: `agents-docs/user-stories/silent-cross-signal-server-auth.md`
- Pack: `04-auth-login-bugs.md`, `09-identity-cross-signal.md`
---
## J5 — Send a text message (server channel)
**User steps**
1. In text channel, type and send.
2. Peer in same room should see it live; late joiner should catch up after connecting.
**Supposed to happen**
- Optimistic local message with stable id (attachments bind to that id).
- Broadcast on data channel; also relay `chat_message` on signaling for peers without DC.
- Edits/deletes primarily P2P (+ `account_sync` to sibling devices).
- On peer connect: inventory ↔ sync-batch (up to 20k recent msgs, chunks of 200).
**Broken looks like**
- Sender sees it; others dont (no presence / no join / DC+fallback both fail).
- Others see live but not history (DC inventory never ran).
- Multi-device: one device has history, another empty (`account_sync` / identify).
- Attachments “Waiting for image…” forever (announce vs message ordering).
**Pack:** `08-messaging-visibility.md`
---
## J6 — Join voice in a channel
**User steps**
1. Click a voice channel.
2. Grant mic if prompted.
3. Hear others; they hear you; speaking indicators; optional camera/screen.
**Supposed to happen**
- Leave any previous voice/call first (exclusive).
- Publish `voice_state` with channel/server ids + `clientInstanceId`.
- Only one device owns mic (others passive; Join = takeover).
- Peer connections already for chat mesh; mic tracks attached only to same-channel peers.
- Playback only for peers in same voice channel.
**Broken looks like**
- “Connecting” forever.
- One-way audio.
- UI shows peers in channel but silent.
- After network blip: forever dead until full app restart.
- Works same-home, fails cross-home (initiator uses home id vs peer actor id).
**Pack:** `06-voice-webrtc-bugs.md`
---
## J7 — Call someone from DM / people card
**User steps**
1. Open DM or people card → Call.
2. Callee hears ring / sees modal (unless DND).
3. Answer → private call UI with optional chat panel.
**Supposed to happen**
- `direct-call` event delivered via PeerDelivery (DC then signaling).
- `targetUserId` = callees **currently connected signal identity**.
- Callee admission checks **all local aliases** (home + every provisioned actor id).
- Caller never sits “In Voice” if ring could not be delivered.
**Broken looks like**
- Caller In Voice; callee silent (outbound route null / wrong id).
- Callee never notified (inbound alias filter — partially fixed).
- Cross-signal: two DM threads; replies land in the “wrong” empty one.
**Pack:** `09-identity-cross-signal.md`
---
## J8 — Network blip / signal server restart
**User steps**
1. In voice + chat; WiFi blips or signal process restarts.
2. Continue without manual reconnect.
**Supposed to happen**
- WS reconnect with backoff; health probe forces fresh socket on instance change.
- `reIdentifyAndRejoin` then room resync.
- Peer disconnect grace 10s; then reconnect loop (~12 × 5s).
- DC close triggers repair; chat fallback covers live text meanwhile.
- Voice presence clears on dead voice-active disconnect server-side.
**Broken looks like**
- Zombie “online” with no events.
- Give-up after ~60s with **no error UI**.
- Docs promise soft DC replace; code tears down full peer (audio drop).
- Identify skipped → alone forever until manual leave/rejoin.
**Pack:** `05-signaling-multi-server.md`, `07-data-channel-drops.md`
---
## J9 — Two devices, same account
**User steps**
1. Desktop in voice; phone/browser also logged in.
2. Second device shows “in voice on another device”; can Takeover.
3. Chat/history appears on both.
**Supposed to happen**
- Distinct `clientInstanceId` per tab (sessionStorage).
- Broadcasts reach sibling connections; `account_sync` for owned state.
- Voice exclusive; takeover yields mic on old owner.
**Broken looks like**
- Tabs evict each other (shared clientInstanceId in localStorage — lesson says use sessionStorage).
- Second device never gets chat batches.
- Both think they own voice / neither transmits.
---
## J10 — Logout
**User steps**
1. Title-bar Logout (desktop) or Settings → Logout (mobile).
**Supposed to happen**
- Disconnect sockets; clear current user id; reset rooms/users/messages; `/login`.
**Broken looks like**
- Stale credentials for foreign URLs linger and confuse next account (verify credential clear scope when fixing auth).