- server: SET_PEER_ROLE now uses the same per-room 500ms debounce as
SET_CONTROL_MODE (M-4), preventing a buggy/malicious host from
thrashing the room's UI with rapid promote/demote bursts.
- server: PEER_STATUS relay treats explicit null as 'clear' for
tabTitle/mediaTitle/currentTime, so the tab-close heartbeat actually
zeroes out stale state on other peers instead of being silently
preserved by the clamp fallback.
- background: _persistLastSeq is now trailing-debounced (500ms),
cutting storage.session IPC writes from one-per-relayed-event to
one-per-quiet-window in active rooms.
- locales: add missing TOAST_ID_REGENERATED to all 14 non-English
locales (was blocking locale coverage test on main).
- test: H-1 force-sync demote test now waits out the debounce window
to reflect real-world UI timing (a host cannot promote, run a
force-sync, and demote inside 500ms).
Audit fixes (each verified against the actual code path):
H-1 (server): track force-sync initiator on PREPARE; let the demoted
initiator's EXECUTE through the host-only gate so mid-sync demotion no
longer strands the whole room paused. Clear on EXECUTE/peer-leave.
M-1 (background): episode-lobby gate now uses !amController() for parity
with CONTENT_EVENT and server gates — co-hosts can drive the room and
initiate lobbies, not just the owner.
M-2/M-3 (popup/content/background): forceSyncReset respects hcmGuestLocked;
desynced guest skips EPISODE_LOBBY so they don't get frozen in pause after
lobby completion, and checkEpisodeLobbyCompletion excludes desynced peers
from the required count so they don't block the lobby.
M-4 (background): getHostSyncTarget clamps extrapolation to 2x heartbeat
interval so a stale host heartbeat can't snap the guest tens of seconds
past the host's real position.
L-1..L-4 (server/content/background/popup): clarify dedup comment re:
network-blip window, enforce desync invariant on SW-restore, add
forceSyncBtn guest-locked backstop, refresh badge text in place.
Backward compatibility (verified by BC-1..BC-4 regression tests):
- Old client ↔ new server: server adds fields only, never requires; old
heartbeats stripped of desynced; host-only enforced server-side even
when the client has no awareness.
- New client ↔ old server: empty capabilities → host-control UI hidden,
all gates default to everyone, behavior byte-identical to pre-HCM.
- Mixed rooms: every pre-HCM event type relays cleanly in both directions.
Generalizes host-only from a single host to a controller SET. The owner (creator)
can promote/demote peers; in host-only mode the relay gate allows any controller,
not just the owner. Single-host is now the special case controllers={owner}, so
existing behavior/tests are unchanged.
- shared: CAPABILITIES.CO_HOST enabled; new SET_PEER_ROLE event; CONTROL_MODE
payload carries controllers.
- server: room.controllers Set (always incl. owner), advertised in ROOM_DATA;
gate checks controllers.has(peerId); SET_PEER_ROLE handler (owner-only, target
must be present, MAX_CONTROLLERS=10 cap, re-sync on reject); controlModePayload
helper for consistent emits; owner-leave resets controllers + falls back to
everyone, co-host-leave broadcasts the updated list (dedup/reconnect guarded).
- WS test: promote→drive, plain guest still gated, non-owner can't promote,
demote→re-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move KNOWN_LIMITATIONS.md → docs/KNOWN_LIMITATIONS.md (+ fix SECURITY.md link).
- WS test: in default 'everyone' mode a non-host guest can still drive
play/pause/seek/force-sync/episode-lobby — proves host-control OFF == unchanged
behavior.
- docs/host-control-mode-COHOST-PLAN.md: plan for multi-controller (owner grants
drive rights to several co-hosts), built on the capabilities hook; covers the
gate generalization, roles, backwards-compat, edge cases, and a separate
large-room (510-peer) scaling track.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Findings recovered from the multi-agent audit (verification phase was cut off by a
usage limit; verified inline against the code):
HIGH:
- Host was demoted on peerId dedup (fast reconnect / second tab): the dedup path
removes the old socket and the kicked socket's 'disconnect' both ran the
host-leave fallback before the same peerId re-joined → room silently unlocked on
every host network blip. Now skip the fallback while a join for that peerId is in
flight (peerJoinLocks). Regression test added. (A long real disconnect still
falls back — that's the deferred host-grace EC-10.)
- Episode auto-advance froze a gated guest: in host-only the guest's EPISODE_LOBBY
is dropped server-side, but the guest still self-paused (PAUSE_FOR_LOBBY) waiting
for readies that never came → 60s freeze. A host-only guest now skips creating a
lobby (the host drives episode sync).
LOW / cleanup:
- GET_HCM_STRINGS could return a raw key name ("HCM_DIALOG_TITLE") if the locale
dictionary failed to load (getMessage returns the key on miss) → omit it so
content keeps its English fallback.
- hcmReset now also clears the snap-back cooldown + buffering grace, so a stale
cooldown can't swallow the first snap-back after a room/host change.
- Popup play/pause labels reset on lock too (not just unlock), so a button can't
freeze on "Playing…" when host-only activates mid-click.
- Fix a stale comment (heartbeat now carries 'desynced').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the implicit "hostPeerId present ⇒ feature supported" heuristic with an
explicit capabilities list the relay advertises in ROOM_DATA. Cleaner, self-
documenting, and the extensible hook for the planned co-host feature (owner
promotes guests to extra controllers) — a future 'co-host' capability + events
slot in without a protocol bump.
- shared: CAPABILITIES { HOST_CONTROL }.
- server: SERVER_CAPABILITIES advertised in ROOM_DATA.
- background: track serverCapabilities (empty against older relay), serverSupports(),
thread hostControlSupported through GET_STATUS / GET_CONTROL_MODE / CONTROL_MODE.
- popup: gate the host-control card on the explicit capability instead of hostPeerId.
Backwards-compatible both ways: old relay omits the field → feature stays hidden
(no errors); old client ignores the field. WS test asserts ROOM_DATA advertises
the capability. Adds docs/host-control-mode-TESTING.md (beta-server setup, wss
caveat, two-new-clients note, verification checklist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bis auf peerId-Auth (akzeptiert) alle Audit-Findings behoben:
- Snap-back spielt nicht mehr blind ab, wenn Host-Status unbekannt
- Teardown-Pfade broadcasten jetzt CONTROL_MODE → kein verwaistes Badge/Dialog
- Dialog-Timer bei Ersetzung sauber gecleart
- Server re-synchronisiert Sender bei nicht-Host-Reject
- Popup-Toggle revertiert bei Server-Ablehnung
- Desync-Zweig sendet keine Phantom-ACKs mehr an Host
- Toggle-Debouncing serverseitig (500ms pro Raum)
- Button-Text auf Unlock zurückgesetzt
- Live-Erkennung defensiver, Resync-Retry bei fehlendem Target, Badge-Retry
Neu: Host sieht 'Solo'-Badge für desyncte Gäste via PEER_STATUS-Heartbeat
(16 Sprachen, neuer i18n-Key BADGE_DESYNCED/TOOLTIP_PEER_DESYNCED)
Tests: H-5-Reject-Unicast, M-4-Debounce, Desync-Heartbeat-Relay
Step 2 of host control mode (server-side, fully testable without UI):
- Room now tracks hostPeerId (= first joiner) and controlMode ('everyone' default).
- ROOM_DATA carries hostPeerId + controlMode so clients know their role on join.
- SET_CONTROL_MODE handler: host-only authority check, broadcasts CONTROL_MODE.
- Relay gate: in host-only mode, drop room-moving events (play/pause/seek/
force-sync/episode-lobby) from non-host guests — robust chokepoint that kills
spam regardless of client. Heartbeats/ACKs/episode-ready still pass.
- removePeerFromRoom: if the host leaves, fall back to 'everyone' and reassign
host to the earliest remaining peer (v1: immediate, no grace period).
Extends the WS integration test suite to cover all of the above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tests: connect, room create/join, password auth, play/pause/seek relay,
force_sync, event_ack, episode_lobby, peer leave, ping/pong, room list,
protocol version check, deduplication, health HTTP endpoint.
All 13 tests pass as part of npm run verify.