- `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.
257 lines
7.8 KiB
Markdown
257 lines
7.8 KiB
Markdown
# 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 B’s WebSocket for that room’s 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 don’t (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` = callee’s **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; Wi‑Fi 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).
|