web/chat.html: add ?loopback=1 dev loopback via BroadcastChannel
without zebrad running, the page is TX-only: tabs see their own
outbound frames but nothing crosses. for UX validation while the
introspector daemon doesn't exist yet, gate a BroadcastChannel
fallback on the ?loopback=1 query param.
behavior:
* with ?loopback=1: after each txFrame completes its modulation,
the raw frame bytes are postMessage'd on a same-origin
BroadcastChannel('zebra-report-loopback'). other tabs of the
same browser subscribe & feed received bytes into onRxFrame
exactly as zebrad would. BroadcastChannel suppresses echo to
sender by spec, so the originating tab does not see itself.
* without ?loopback=1: behavior unchanged. real PA transport
stays the production path.
a yellow banner at the top of the page warns when loopback is
active: "frames cross via BroadcastChannel between same-browser
tabs, NOT via PulseAudio". no risk of mistaking dev mode for
production.
This commit is contained in:
parent
d8607cb000
commit
5d76d713a6
1 changed files with 35 additions and 1 deletions
|
|
@ -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.');
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue