diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 297925f..3ae6ece 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -5877,6 +5877,41 @@ async function dumpTelemetry(){
});
} catch(e){ parts.push('stats.err=' + e.message); }
}
+ /* mesh receiver stats — one line per peer's mesh PC inbound audio.
+ * Lets us tell whether the worklet is being fed by a live mesh
+ * decoder vs an orphaned one. For ticket 0001 we needed to answer:
+ * when handleRemoteSfuTrack hands off to mesh and the SFU receiver
+ * reports level=0+jbuf=? (expected — orphaned), is the MESH receiver
+ * actually decoding (level>0+jbuf>0) or also silent? fox 2026-06-07.
+ *
+ * Each peer's PC has one audio inbound-rtp; tag with the short uuid
+ * so a multi-peer room is readable. Failure on one peer's getStats
+ * doesn't stop the others. */
+ for (const [uuid, pc] of peers){
+ if (!pc || typeof pc.getStats !== 'function') continue;
+ if (pc.connectionState !== 'connected') continue; /* skip non-live PCs */
+ try {
+ const stats = await pc.getStats(null);
+ const nowMs = Date.now();
+ stats.forEach(r => {
+ if (r.type !== 'inbound-rtp' || r.kind !== 'audio') return;
+ const lp = r.lastPacketReceivedTimestamp
+ ? ((nowMs - r.lastPacketReceivedTimestamp) / 1000).toFixed(1) + 's'
+ : '?';
+ const jbuf = (r.jitterBufferEmittedCount > 0)
+ ? (r.jitterBufferDelay / r.jitterBufferEmittedCount).toFixed(2) + 's'
+ : '?';
+ parts.push('mesh.recv u=' + uuid.slice(0,4) +
+ ' pkt=' + (r.packetsReceived|0) +
+ ' lost=' + (r.packetsLost|0) +
+ ' bytes=' + (r.bytesReceived|0) +
+ ' jitter=' + (r.jitter || 0).toFixed(4) +
+ ' level=' + (r.audioLevel || 0).toFixed(3) +
+ ' jbuf=' + jbuf +
+ ' lp=' + lp);
+ });
+ } catch(_){}
+ }
/* publisher-side stats — what we are actually emitting on the wire.
* One line per kind so screen-audio shows up independently of
* screen-video (earlier collapse hid scr.send.aud entirely under
@@ -8148,8 +8183,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');