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
+154
View File
@@ -0,0 +1,154 @@
# 03 — Architecture map (for agents)
## Bounded contexts (product client)
See `toju-app/src/app/domains/README.md`. Emergency-relevant domains:
| Domain | Owns |
|--------|------|
| `authentication` | Login/register HTTP, provision secret, per-URL credentials, authorize navigation |
| `server-directory` | Endpoints, health, discovery, invites, room metadata affinity |
| `chat` | Message rules, sync rules, chat UI |
| `direct-message` | DMs, friends, offline queue, PeerDelivery usage |
| `direct-call` | Private call sessions / rings |
| `voice-session` | Join/leave bookkeeping, floating controls, settings storage |
| `voice-connection` | Facade over realtime for mic/camera/playback/VAD |
| `screen-share` | Picker / quality |
| `attachment` | Chunked P2P files |
| `access-control` | Permissions / bans |
**Infrastructure (not a domain):** `infrastructure/realtime/` (WebRTC + signaling), `infrastructure/persistence/`, `infrastructure/mobile/`.
**Global NgRx:** `store/users`, `store/rooms`, `store/messages` — orchestration across domains.
---
## Identity & credentials
```
Home profile (NgRx User)
id / username / displayName / homeSignalServerUrl
├─ AuthTokenStore (legacy per-URL token) metoyou.authTokens
├─ SignalServerCredentialStore metoyou.signalServerCredentials
│ { serverUrl → userId, token, provisioned? }
└─ ProvisionSecretStore (per home user id)
Electron safeStorage | web sessionStorage
```
**Actor resolution for a room:** `SignalServerAuthService.resolveActorUserIdForServer(sourceUrl, homeOderId)` — foreign URL must use provisioned `userId`.
**Identify on socket:** `SignalingTransportHandler.getIdentifyCredentialsForSignalUrl(url)` must resolve token+actor for that URL (store fallback).
**Danger:** `getIdentifyCredentials()` returns **home** credential — used for some localOderId / polite-peer paths → cross-signal initiator bugs (`06`, `09`).
---
## Room ↔ signal affinity
```
Room { id, channels[], sourceUrl, sourceId, ... }
RoomSignalingConnection
ensureCredentialForServerUrl(sourceUrl)
identify(actor for sourceUrl)
join_server / view_server on that WS
ServerSignalingCoordinator
maps serverId ↔ signalUrl ↔ peers
```
Fallback: try other online compatible endpoints on outage; **do not** treat Cloudflare 521/522 as “client incompatible”. Non-federated: peers must converge on same endpoint for that room.
---
## Realtime composition root
`WebRTCService` (`realtime-session.service.ts`) wires:
- `SignalingTransportHandler` → many `SignalingManager` (one WS per URL)
- `PeerConnectionManager` → negotiation, DC, recovery
- `MediaManager` / noise / screen share
- `WebRtcStateController` (signals)
Inbound WS → `SignalingMessageHandler` → users/rooms/voice/chat effects.
---
## Message send path (server channel)
```
Composer → MessagesActions.sendMessage({ id? })
→ local DB + NgRx
→ DC broadcast chat-message (+ message-revision)
→ WS chat_message fallback (room members)
→ account_sync to sibling devices
```
Receive gates: room must be current or saved; channel scoping for text.
---
## Voice join path
```
UI join channel
→ VoiceSessionFacade startSession
→ leave previous exclusive target
→ enableVoice / heartbeat
→ voice_state on WS (+ DC control)
→ MediaManager.syncVoiceRouting (same channel only)
→ VoicePlaybackService for remote same-channel peers
```
Peer PC may already exist from presence mesh; join mainly attaches tracks + announces state.
---
## Call / DM delivery path
```
DirectCallService / DirectMessageService
→ PeerDeliveryService
1) data channel if open
2) signaling forward (targetUserId)
3) offline queue (DM) / silent fail (call if ignored)
```
Inbound call admission: `direct-call-participant-identity.rules.ts` (aliases — implemented).
Outbound routable id pick: **documented in LESSONS as fixed; symbols not in tree** — see `10-code-lies`.
---
## Recovery constants (cheat sheet)
| Constant | Value |
|----------|-------|
| WS reconnect backoff | 1s → 30s |
| Connect timeout | 5s |
| Keepalive interval / ack timeout | 25s / 10s |
| Health probe | 5s |
| Peer disconnect grace | 10s |
| Peer reconnect | 5s × 12 then silent abandon |
| DC recovery grace (closing) | 2.5s |
| Non-initiator give-up | 5s |
| Offer-sent grace | 20s |
| Inventory / full sync limit | 20_000 |
| Sync batch | 200 |
| Sync poll | 10s fast / 15 min slow |
Source: `realtime.constants.ts`, `message-sync.rules.ts`.
---
## Server responsibilities (only when needed)
Default agent scope excludes `server/` unless packet expands. Know this:
- Serializes WS handlers per connection (identify-before-join).
- Relays RTC only when peers share membership (DM types exempt).
- No message persistence.
- `voiceActive` routes offers to owning connection.
Canonical envelopes: `agents-docs/features/signaling.md`.