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
+118
View File
@@ -0,0 +1,118 @@
# 01 — Product overview (user + system)
## What the user thinks this app is
**Toju / MetoYou** is a Discord-like desktop chat app with:
- **Accounts** on a signal (signaling) server
- **Chat-servers** (communities) with text channels and voice channels
- **Live text chat** in those channels
- **Voice / camera / screen share** in voice channels
- **Direct messages** and **private calls**
- **Friends**, profile cards, custom emoji, GIFs, file attachments
- **Multiple signal servers** in the network settings (home + others)
The marketing promise implied by the product design:
> Log in once. Join communities anywhere on the network. Chat and voice “just work.” Switching signal hosts should not feel like logging into a second product.
---
## What the system actually is
| Layer | Role |
|-------|------|
| **Angular client** (`toju-app/`) | All UX, NgRx state, domain logic, WebRTC + WebSocket clients |
| **Electron shell** (`electron/`) | Desktop window, SQLite persistence, IPC (`window.api`), provision-secret safeStorage, screen capture helpers |
| **Signaling server** (`server/`) | Auth tokens, public server directory REST, WebSocket identify/join/presence, RTC offer/answer/ICE relay, narrow chat/DM/voice_state fallbacks, multi-device `account_sync`. **Does not store chat history.** |
| **Web / Capacitor** | Same Angular app; weaker secret storage (sessionStorage); mobile voice/UI constraints |
### Transport split (critical mental model)
| Transport | What the user experiences | What it actually carries |
|-----------|---------------------------|--------------------------|
| **WebSocket to signal server** | “Im online / in this server / someone joined voice” | Identity, room membership, presence, SDP/ICE relay, `chat_message` + DM fallbacks, `voice_state`, `account_sync` |
| **WebRTC media** | Hearing / seeing people | Mic, camera, screen tracks (never through the signal server) |
| **WebRTC ordered data channel** | Messages syncing, files, emoji, many “live” features | Chat events, inventory sync, attachments, avatar/emoji chunks, voice/screen control, plugin bus |
If the **socket** is wrong or unauthenticated → user is invisible; no relay; chat fallback may fail.
If the **data channel** is dead → live text may still limp via `chat_message`, but **history sync, attachments, emoji, many controls** fail.
If **media** is routed wrong or peer never connects → voice UI lies (“In Voice”) with silence.
---
## Core vocabulary (user ↔ engineering)
| User says | Engineering term |
|-----------|------------------|
| “My account / login” | Home session on `homeSignalServerUrl` + local profile in SQLite/IndexedDB |
| “Another network / signal host” | Foreign `ServerEndpoint` URL; needs per-URL credential |
| “A server / community” | Saved **Room** (chat-server) with `sourceUrl` / `sourceId` = which signal hosts it |
| “Text channel / voice channel” | Channels inside a room; text vs voice types |
| “Im in voice” | Local `VoiceSession` + `voice_state` with `isConnected` + mic ownership via `clientInstanceId` |
| “DM / call someone” | Direct-message conversation + optional `direct-call` session |
| “It logged me out” | Often `/login` or `/login?mode=authorize` — may be **foreign authorize**, not true home logout |
---
## Multi-signal identity (the footgun)
1. User registers on **Signal A** → home user id `H`.
2. User later needs **Signal B** → client is supposed to **auto-register/login** with a local **provision secret**, creating actor id `A_B` on B (username may be suffixed).
3. Peers on Signal B see the user as `A_B`, not `H`.
4. WebRTC peer map keys, `targetUserId` on relay, DM conversation ids, and call participant lists may mix `H` and `A_B`.
**Invariant the product claims:** one human, one local profile, many per-server actor credentials — UX never asks for password again except last-resort authorize / real home expiry.
**Invariant signaling claims:** non-federated — peers in the same room must share the **same signal endpoint** to discover each other. Cross-room “same person” is a client-side identity problem.
---
## Feature inventory (what “done product” includes)
Must work for a release-quality emergency fix:
1. Home register / login / logout / session restore
2. Silent foreign provision + Network settings badges
3. Discover / create / join / leave chat-servers (public, password, invite, moderated)
4. Text channels: send, edit, delete, react, typing, unread, sync after late join
5. Voice channels: join/leave, mute/deafen, speaking indicators, multi-device takeover, move between channels
6. Camera + screen share in voice / calls
7. DMs + friends + delivery states
8. Direct / group calls (ring, answer, decline, DND)
9. Attachments over data channel
10. Multi-device `account_sync` for saved rooms / chat batches / friends / avatar / emoji
11. Endpoint health, version compatibility, room signal affinity + fallback
Secondary (do not block P0 voice/auth/chat): plugins, game activity, themes, KLIPY, link previews, experimental media.
---
## Platforms
| Platform | Notes for bugs |
|----------|----------------|
| Electron desktop | Primary; provision secret in safeStorage; SQLite; best screen share |
| Browser | sessionStorage provision secret dies with tab; IndexedDB |
| Capacitor mobile | Auth routing, mic permissions, no reliable screen share; title bar hidden |
---
## Trust boundaries (short)
- REST mutations and WS `identify` require bearer/session token **per signal URL**.
- Actor user ids in request bodies are ignored server-side; token wins.
- Message bodies are **not E2E encrypted** beyond DTLS on WebRTC and TLS on WS.
- Signaling server is **not** the source of truth for chat history.
---
## Where truth lives (for agents)
| Kind of truth | Prefer |
|---------------|--------|
| Wire WebSocket types | `agents-docs/features/signaling.md` + `server/src/websocket/handler.ts`**not** `shared-kernel/signaling-contracts.ts` |
| Auth multi-server | `agents-docs/features/authentication.md` + `emergency-fix/04-*.md` |
| Voice/WebRTC plumbing | `toju-app/.../realtime/README.md` — verify against code (see `10-code-lies`) |
| Domain UX | `toju-app/src/app/domains/<name>/README.md` |
| Known past bugs | `agents-docs/LESSONS.md` — verify symbols still exist (some lessons describe **desired** fixes as if shipped) |