feat(stage): speakers/listeners + request-to-speak control plane (LC-494) #491

Merged
longjacksonle merged 5 commits from feat/LC-494-stage-control-plane into main 2026-06-29 03:18:35 +02:00

LC-494 - Stage: speakers vs listeners + request-to-speak (control plane)

LC-494 needs an SFU for the actual large-audience audio (the current stack is full-mesh, ~4-6 peers). Per the decision on the ticket, this PR ships the control plane - roles, the live roster, and request-to-speak - designed to drop onto an SFU later. The media transport is the dedicated follow-up LC-512. The panel says so explicitly ("audio is coming soon - needs a media server").

What works now

  • Per-room opt-in: migration 0078 adds rooms.stage_enabled; a switch on the room Manage page (mod-log audited) turns stage mode on for a group room.
  • Roles + roster: ephemeral hub state per room (StageState: participants / speakers / hands). A listener joins, can raise/lower a hand or leave; a speaker can step down; a host (room manager) can approve a raised hand (promote) or remove a speaker (demote).
  • Live + per-viewer: any change broadcasts ChatEvent::StageChanged; each client re-renders the roster panel for itself (host controls + own-state differ per viewer), id-keyed so connections not viewing the room drop the swap. Last-tab disconnect drops the user from every stage.
  • Client: a tiny stage.js bridges the panel's data-lc-stage-* buttons to WS frames over the shared chat socket.

What's deferred (LC-512)

The audio itself. Speakers publishing / listeners subscribing at scale needs an SFU; the control plane here is exactly what grants/revokes publish rights once that lands.

Tests

stage_control: hub roster state machine (join -> raise hand -> promote clears hand -> demote -> leave empties the room) + stage_leave_all affected-rooms + the per-room toggle. just check, just test, just test-saas all green; i18n parity covers new stage-* / room-stage-* keys (en+es); Tailwind rebuilt.

Not operator-visible (no env/config/contract change; off by default).

🤖 Generated with Claude Code

## LC-494 - Stage: speakers vs listeners + request-to-speak (control plane) LC-494 needs an SFU for the actual large-audience audio (the current stack is full-mesh, ~4-6 peers). Per the decision on the ticket, this PR ships the **control plane** - roles, the live roster, and request-to-speak - designed to drop onto an SFU later. The media transport is the dedicated follow-up **LC-512**. The panel says so explicitly ("audio is coming soon - needs a media server"). ### What works now - **Per-room opt-in**: migration `0078` adds `rooms.stage_enabled`; a switch on the room Manage page (mod-log audited) turns stage mode on for a group room. - **Roles + roster**: ephemeral hub state per room (`StageState`: participants / speakers / hands). A listener joins, can **raise/lower a hand** or **leave**; a speaker can **step down**; a **host** (room manager) can **approve** a raised hand (promote) or **remove** a speaker (demote). - **Live + per-viewer**: any change broadcasts `ChatEvent::StageChanged`; each client re-renders the roster panel for itself (host controls + own-state differ per viewer), id-keyed so connections not viewing the room drop the swap. Last-tab disconnect drops the user from every stage. - **Client**: a tiny `stage.js` bridges the panel's `data-lc-stage-*` buttons to WS frames over the shared chat socket. ### What's deferred (LC-512) The audio itself. Speakers publishing / listeners subscribing at scale needs an SFU; the control plane here is exactly what grants/revokes publish rights once that lands. ### Tests `stage_control`: hub roster state machine (join -> raise hand -> promote clears hand -> demote -> leave empties the room) + `stage_leave_all` affected-rooms + the per-room toggle. `just check`, `just test`, `just test-saas` all green; i18n parity covers new `stage-*` / `room-stage-*` keys (en+es); Tailwind rebuilt. Not operator-visible (no env/config/contract change; off by default). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Migration 0078 adds rooms.stage_enabled (opt-in). db get/set_room_stage_enabled. Hub gains an ephemeral StageState per room (participants/speakers/hands) with join/leave/raise/lower/promote/demote + stage_leave_all (returns affected rooms for disconnect cleanup). New ChatEvent::StageChanged broadcasts roster changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
routes::stage builds the StagePanel per viewer (speakers, listeners with raised-hand flag, host controls) from the hub roster + bulk auth labels; is_host reuses room management. The panel carries an explicit "audio coming soon (needs a media server)" note. StageChanged routes to the per-recipient render (render_event returns None for it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClientFrame::Stage* (join/leave/raise/lower/step-down + host promote/demote) mutate the hub and broadcast StageChanged; self-actions gate on access + stage mode, host actions on require_can_manage. The send loop re-renders the panel per recipient; last-tab disconnect drops the user from every stage. stage.js bridges the panel's data-lc-stage-* buttons to WS frames over the shared socket.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_room pre-renders the stage panel for stage-enabled group rooms; room/page.html mounts it above the composer. A manage-page switch (POST /room/{id}/stage, mod-log audited) toggles stage mode. New stage-* and room-stage-* i18n (en+es).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test(stage): hub roster state machine + room toggle (LC-494)
All checks were successful
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / Kingfisher (push) Successful in 7s
check-secrets / Nosey parker (push) Successful in 7s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Successful in 3m52s
Create release / Create release from merged PR (pull_request) Has been skipped
3f6b885c4f
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-06-29 03:16:35 +02:00
longjacksonle deleted branch feat/LC-494-stage-control-plane 2026-06-29 03:18:36 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!491
No description provided.