diff --git a/web/chat.html b/web/chat.html index f368240..a6e9313 100644 --- a/web/chat.html +++ b/web/chat.html @@ -334,6 +334,10 @@ const WS_URL = 'ws://127.0.0.1:7777'; const PBKDF2_SALT = new TextEncoder().encode('zebra-report-v1'); const PBKDF2_ITER = 600000; +/* dev loopback: bypass PulseAudio, ride BroadcastChannel between tabs. + * gated on ?loopback=1. real PA transport stays the production path. */ +const LOOPBACK = new URLSearchParams(location.search).has('loopback'); + /* ============================================================== * * tiny utils * * ============================================================== */ @@ -630,6 +634,14 @@ async function txFrame(bytes, baud) { while (txQueue.length) { const job = txQueue.shift(); await _txOne(job.bytes, job.baud); + if (loopChan) { + try { + const ab = job.bytes.buffer.slice( + job.bytes.byteOffset, + job.bytes.byteOffset + job.bytes.byteLength); + loopChan.postMessage(ab); + } catch (_) {} + } } } finally { txBusy = false; @@ -705,10 +717,19 @@ function recomputeGroupBaud() { } /* ============================================================== * - * introspector bridge (RX path) * + * introspector bridge (RX path) + dev loopback * * ============================================================== */ let ws = null; let wsReconnectTimer = null; +let loopChan = null; +if (LOOPBACK) { + loopChan = new BroadcastChannel('zebra-report-loopback'); + loopChan.onmessage = (ev) => { + const buf = ev.data instanceof ArrayBuffer + ? new Uint8Array(ev.data) : new Uint8Array(ev.data); + onRxFrame(buf); + }; +} const dotRx = $('dot-rx'); const rxStatus = $('rx-status'); @@ -1053,6 +1074,19 @@ setInterval(updateSendButton, 1000); /* try to find the introspector; quiet failure */ rxConnect(false); +if (LOOPBACK) { + const banner = document.createElement('div'); + banner.style.cssText = + 'background:#fc0;color:#000;padding:0.6rem 1rem;text-align:center;' + + 'font-size:0.8rem;font-weight:bold;border:1px solid #000;' + + 'margin-bottom:1.2rem;letter-spacing:0.03em'; + banner.textContent = + 'dev loopback active — frames cross via BroadcastChannel between ' + + 'same-browser tabs, NOT via PulseAudio. drop ?loopback=1 from the URL for production.'; + document.body.insertBefore(banner, document.body.firstChild); + logLine('sys', 'loopback mode: frames will echo to other tabs of this same browser.'); +} + logLine('sys', 'ready — start the carrier, derive a room key, then send.'); logLine('sys', 'no chat content ever enters an IP packet. wireshark blind.');