zebra-report: sync chat page, add ?loopback=1 dev mode

mirrors web/chat.html from the zebra-report repo. adds a
BroadcastChannel-based loopback gated on ?loopback=1 so two
tabs of a same browser can round-trip frames without zebrad,
useful for UX validation while a real PulseAudio introspector
daemon stays in progress.

yellow warning banner makes dev mode obvious when active.
This commit is contained in:
russell@unturf.com 2026-05-27 15:47:09 -04:00
parent c2b1883e0a
commit 695b6af58f

View file

@ -244,9 +244,8 @@
expects <code>zebrad</code> at <code>ws://127.0.0.1:7777</code>. The bridge
polls PulseAudio sink-input volumes via PA IPC and pushes decoded frames here.
Without it the page transmits but cannot receive. The
<a href="https://git.unturf.com/engineering/unturf/zebra-report"
style="color:#555">recovered <code>rx.c</code></a> handles
decoding; <code>zebrad</code> is a WebSocket wrapper around it.
<a href="https://git.unturf.com/engineering/unturf/zebra-report" style="color:#555">recovered <code>rx.c</code></a> handles
decoding; <code>zebrad</code> is the WebSocket wrapper around it.
</p>
</section>
@ -335,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 *
* ============================================================== */
@ -631,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;
@ -706,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');
@ -1054,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.');