diff --git a/docs/architecture/ux/README.md b/docs/architecture/ux/README.md index 6d464252..07c6196f 100644 --- a/docs/architecture/ux/README.md +++ b/docs/architecture/ux/README.md @@ -92,9 +92,12 @@ source of truth in `ui.store.connectionStatus` > `SidebarArea` never passed it a `ws`; it now gates on the store and receives > the `ws` send path). The one-shot connected-overlay wiring in `main.ts` stays > on `ws.onStateChange` deliberately — it needs the exact internal transition. -> **Remaining gap:** the table's voice column. Voice controls are not yet -> frozen during a WS reconnect — LiveKit reconnection retries underneath, but -> join/leave controls stay enabled and would send over the down socket. +> The voice column is now wired too: the VoiceWidget freezes its controls +> (disabled + a "Reconnecting…" / "Not connected" reason) while +> `connectionStatus !== "connected"`, and the join/leave callbacks +> (`VoiceCallbacks.ts`) refuse to fire `voice_join`/`voice_leave` over a down +> socket. LiveKit's own reconnection keeps retrying underneath — only the UI is +> gated, never LiveKit's machinery. | Status | Composer / send | Voice controls | Presence picker | Reconnect banner | |--------|-----------------|----------------|-----------------|------------------| diff --git a/docs/architecture/ux/voice-and-e2ee.md b/docs/architecture/ux/voice-and-e2ee.md index 88445f9a..3bb7fc5d 100644 --- a/docs/architecture/ux/voice-and-e2ee.md +++ b/docs/architecture/ux/voice-and-e2ee.md @@ -24,14 +24,21 @@ Plus the user-facing booleans in `voice.store` (`localMuted`, `localDeafened`, roster (`voiceUsers` with per-user `speaking/muted/deafened/camera/screenshare`). **Target:** expose the voice session as one observable `voiceStatus` the widgets -read — `idle | joining | securing | connected | reconnecting | failed` — rather -than inferring it from `isVoiceConnected()` alone. +read — `idle | joining | securing | connected | reconnecting` — rather than +inferring it from `isVoiceConnected()` alone. -> **⚠ Current gap.** The voice session FSM is internal; the only UI-observable -> connection signal is `isVoiceConnected()` (`livekitSession.ts:1713`, true only -> in `connected`). There is **no** store-backed `joining`/`securing`/`reconnecting` -> indicator, so the UI can't distinguish "connecting to the room" from "securing -> the encryption" from "reconnecting". Target adds an explicit status field. +> **✓ Implemented (2026-07).** `voice.store.voiceStatus` +> (`idle | joining | securing | connected | reconnecting`) is now the observable +> voice-session status. `livekitSession.ts` is the single writer: `joining` at the +> start of `connectAndSetup`, `securing` when the ECDH key exchange begins, +> `connected` on the atomic `connected` transition (both the initial join and a +> successful auto-reconnect), `reconnecting` when the room drops and the reconnect +> loop forms its state, and `idle` on `leaveVoice`. `joinVoiceChannel` seeds +> `joining` optimistically on click so the widget reacts before the `voice_token` +> round-trip. The VoiceWidget reads it to distinguish "connecting to the room" +> from "securing the encryption" from "reconnecting". `failed` is not a persisted +> status: an E2EE-timeout / connection error auto-leaves to `idle` and surfaces a +> toast via `onErrorCallback` (§2). --- @@ -66,13 +73,17 @@ stateDiagram-v2 - Leaving is immediate and local (`leaveVoice`): tear down tracks, clear E2EE state, reset camera/screenshare, `idle`. -> **⚠ Current gap — E2EE has no visible indicator.** Key exchange produces only -> log lines; the sole user-facing effects are (a) the join *blocking* while the -> key is fetched and (b) an `"e2ee_timeout"` error string on failure -> (`livekitSession.ts:893`). There is no "securing" state and no persistent -> "secured 🔒" affirmation once connected. Target: a `voiceStatus: "securing"` -> phase + a secured indicator on the connected widget, so users can *see* the -> call is end-to-end encrypted (and see when it isn't yet). +> **✓ Implemented (2026-07).** The VoiceWidget header now renders the E2EE phase +> from `voiceStatus`: a "Securing…" label (amber) while the key exchange runs and +> a persistent "🔒 Secured" badge once the room key is ready and the room is +> connected — replacing the log-line-only feedback. `joining` shows "Connecting…" +> and `reconnecting` shows "Reconnecting voice…", neither showing the secured +> badge. An E2EE-timeout still surfaces its `"e2ee_timeout"` toast and auto-leaves +> (`livekitSession.ts` `connectAndSetup`). **Code vs. diagram note:** the client +> actually runs the ECDH key exchange *before* `room.connect()`, so `securing` +> spans the key wait and the media connect; the state diagram below draws them in +> the reverse order for readability. The distinction users see is unchanged: +> non-key-holders sit in `securing` until a room key arrives. ---