my-sea voice: guard before NVM disconnects voice + harden mute (honor pre-stream mute) — TDD

Phase 4 of the my-sea voice batch (user-spec 2026-05-29).

── Voice-disconnect guard (item 6, the achievable slice) ──
Every my-sea navigation is a full page reload, which tears the WebRTC mesh
down — so voice can't literally persist across a reload without an SPA-style
no-reload nav (a separate, larger refactor, deferred). What ships now: the
gear-menu NVM warns before dropping the call.

- _my_sea_gear.html: the NVM routes through an inline, dependency-free
  `mySeaGuardedNav(event, url)`. When voice is LIVE (VoiceRoom.localStream) AND
  the target leaves my_sea (`url` has no `/my-sea`), it pops the shared guard
  portal — "Leave the Sea? You'll disconnect from voice." — before navigating;
  confirm proceeds, dismiss stays. NVMs that stay within my_sea, or any nav
  with no live mic, go straight through. Covers all NVMs (owner my_sea, the
  gatekeeper, the spectator) since they all include this partial.

── Bug B: desktop mute (mute robustness) ──
- voice-mesh.js: extracted `_applyMute()` and call it from join (post-
  getUserMedia) as well as toggleMute. On desktop the first join pops a mic-
  permission prompt; a mute toggled while that prompt is open used to be lost
  because the stream didn't exist yet — re-applying after it resolves makes the
  mute stick. Teardown resets `muted=false` so a rejoin starts clean.
- voice-glow.js: setVoiceState now syncs the voice btn's own flags
  (.in-call / .muted / dataset.inCall) to the mesh truth, so a rejoin starts
  clean and the glow's DOM fallback can't get stuck "live".

Tests: +5 VoiceMeshSpec mute specs (incl. the pre-stream-mute Bug-B case);
+2 NVM-guard FTs (warn when live / pass through when not); 3 gear-NVM ITs
updated for the mySeaGuardedNav markup. 286 gameboard ITs + 433 Jasmine specs
green.

Note: true cross-view voice persistence (no-reload within-my_sea nav) + the
desktop-mute live confirmation remain to verify on staging w. Redis up.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-29 21:22:21 -04:00
parent b021d8017c
commit 6799749ede
7 changed files with 216 additions and 11 deletions

View File

@@ -1040,7 +1040,8 @@ class MySeaViewTest(TestCase):
def test_gear_nvm_navs_to_gameboard_on_landing_phase(self):
"""Landing-phase NVM = "back out of my-sea entirely" → /gameboard/."""
response = self.client.get(reverse("my_sea"))
self.assertIn("location.href='/gameboard/'", response.content.decode())
self.assertIn(
"mySeaGuardedNav(event, '/gameboard/')", response.content.decode())
def test_sea_btn_is_inactive_on_landing_phase(self):
"""Sea sub-btn flips ACTIVE only when the user is in the picker
@@ -1120,7 +1121,7 @@ class MySeaViewTest(TestCase):
)
response = self.client.get(reverse("my_sea"))
self.assertIn(
"location.href='/gameboard/my-sea/?phase=landing'",
"mySeaGuardedNav(event, '/gameboard/my-sea/?phase=landing')",
response.content.decode(),
)
@@ -2473,8 +2474,10 @@ class MySeaGateViewTest(TestCase):
2026-05-26 so the gatekeeper exit lands one step back."""
response = self.client.get(reverse("my_sea_gate"))
body = response.content.decode()
self.assertIn("location.href='/gameboard/my-sea/'", body)
self.assertNotIn("location.href='/gameboard/'", body)
# NVM routes through mySeaGuardedNav (voice-disconnect guard, 2026-05-29)
# but still carries the same target URL.
self.assertIn("mySeaGuardedNav(event, '/gameboard/my-sea/')", body)
self.assertNotIn("mySeaGuardedNav(event, '/gameboard/')", body)
def test_gate_view_renders_burger_btn_and_fan(self):
response = self.client.get(reverse("my_sea_gate"))