my-sea voice: persist the call across in-sea reloads via auto-rejoin; pngquant the RWS card back — TDD

Voice-persistence follow-up (user-spec item 6). Every my-sea navigation is a
full page reload that kills the WebSocket + peer connections; true no-reload
nav would need an SPA refactor of the heavily-tested draw IIFEs. Instead we
auto-rejoin: bindVoiceBtn remembers the active room in sessionStorage on join
and silently re-joins it on the next my-sea page if voice is still available
there (mic permission persists for the session, so no prompt). Same user-
visible result (a brief reconnect, not seamless) with no risk to the draw flows.

- burger-btn.js: sessionStorage 'mysea-voice-room' remember/forget helpers +
  window.mySeaVoiceForget; bindVoiceBtn refactored to startCall()/withVoiceRoom()
  and auto-rejoins on bind when the remembered room === the active btn's room.
  A failed join (e.g. INSECURE_CONTEXT) forgets the room so it doesn't retry.
- _my_sea_gear.html: the NVM-disconnect guard confirm + BYE forget the room
  (and leave the mesh) — an explicit leave shouldn't auto-rejoin.
- BurgerSpec: +4 auto-rejoin specs (match / different-sea / inactive / remember
  + forget). 438 Jasmine specs green.

Also (bundled, user's parallel work): pngquant the resaved RWS deck card back
(tarot-rider-waite-smith-back.png) from 733KB truecolor+a to a 264KB 8-bit
palette PNG, matching its companion card faces. Dimensions preserved (the
rotated 401x694).

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 22:08:45 -04:00
parent 2cbc1bf292
commit c4e738ad16
5 changed files with 171 additions and 19 deletions

View File

@@ -120,6 +120,26 @@
// INACTIVE click is left to the delegated fan handler's 2-pulse flash.
// No stopPropagation on active — the delegated handler then closes the
// fan (its existing .active behaviour).
// Voice persistence across my_sea reloads (2026-05-29): we remember the
// active room in sessionStorage so the next my_sea page silently re-joins
// the mesh (mic permission persists for the session → no prompt). True
// no-reload nav would need an SPA refactor of the draw IIFEs; this gets the
// same user-visible result (a brief reconnect, not seamless) with no risk
// to those flows. The flag is cleared only on an EXPLICIT leave (BYE /
// NVM-disconnect guard) or a failed join, never on an in-my_sea reload.
var VOICE_ROOM_KEY = 'mysea-voice-room';
function _rememberVoiceRoom(id) {
try { sessionStorage.setItem(VOICE_ROOM_KEY, id); } catch (e) {}
}
function _forgetVoiceRoom() {
try { sessionStorage.removeItem(VOICE_ROOM_KEY); } catch (e) {}
}
function _rememberedVoiceRoom() {
try { return sessionStorage.getItem(VOICE_ROOM_KEY); } catch (e) { return null; }
}
// BYE + the NVM-disconnect guard call this on an explicit leave.
window.mySeaVoiceForget = _forgetVoiceRoom;
// Surface a join failure to the user instead of failing silently — most
// often the secure-context block (INSECURE_CONTEXT) when the dev server is
// reached over plain HTTP from a phone. Prefers the Brief banner; falls
@@ -127,6 +147,7 @@
function _voiceJoinFailed(vbtn, e) {
vbtn.classList.remove('in-call');
delete vbtn.dataset.inCall; // let the next click retry the join
_forgetVoiceRoom(); // don't auto-rejoin a context that can't talk
var msg = (e && e.code === 'INSECURE_CONTEXT')
? 'Voice needs HTTPS (or localhost) — your browser blocked the mic here.'
: 'Couldnt start voice — mic unavailable or permission denied.';
@@ -143,33 +164,49 @@
function bindVoiceBtn() {
var vbtn = document.getElementById('id_voice_btn');
if (!vbtn) return;
var roomId = vbtn.getAttribute('data-room-id');
// VoiceRoom is lazy-loaded on first use (mesh injected on demand).
function withVoiceRoom(cb) {
if (window.VoiceRoom) { cb(); return; }
var s = document.createElement('script');
s.src = '/static/apps/voice/voice-mesh.js';
s.onload = cb;
document.head.appendChild(s);
}
function startCall() {
if (!window.VoiceRoom || vbtn.dataset.inCall) return;
vbtn.dataset.inCall = '1';
vbtn.classList.add('in-call');
_rememberVoiceRoom(roomId);
var p = window.VoiceRoom.join(roomId);
if (p && typeof p.catch === 'function') {
p.catch(function (e) { _voiceJoinFailed(vbtn, e); });
}
}
vbtn.addEventListener('click', function () {
if (!vbtn.classList.contains('active')) return; // → delegated flash
var roomId = vbtn.getAttribute('data-room-id');
if (!roomId) return;
function act() {
withVoiceRoom(function () {
if (!window.VoiceRoom) return;
if (!vbtn.dataset.inCall) {
vbtn.dataset.inCall = '1';
vbtn.classList.add('in-call');
var p = window.VoiceRoom.join(roomId);
if (p && typeof p.catch === 'function') {
p.catch(function (e) { _voiceJoinFailed(vbtn, e); });
}
startCall();
} else {
var muted = window.VoiceRoom.toggleMute();
vbtn.classList.toggle('muted', muted);
}
}
if (window.VoiceRoom) {
act();
} else {
var s = document.createElement('script');
s.src = '/static/apps/voice/voice-mesh.js';
s.onload = act;
document.head.appendChild(s);
}
});
});
// Auto-rejoin: were we in THIS room before the navigation, and is voice
// still available on this page? Silently re-join so the call survives an
// in-my_sea reload (GATE VIEW / NVM / draw nav).
if (roomId && vbtn.classList.contains('active')
&& _rememberedVoiceRoom() === roomId) {
withVoiceRoom(startCall);
}
}
window.bindVoiceBtn = bindVoiceBtn;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 265 KiB