diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index 9607626..25b065d 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -1868,7 +1868,7 @@ function renderCameraTile(pubHex, stream, opts){ return renderVideoTile('camera'
function removeCameraTile(pubHex){ return removeVideoTile('camera', pubHex); }
async function sfuPublish(){
- if (sfuPubPC){ logLine('','sfu publish: already publishing'); return; }
+ if (sfuPubPC) return; /* idempotent — re-entry from signal reconnect is fine */
if (!micStream || !myKeys || !roomID){
logLine('err','sfu publish skipped: mic='+(!!micStream)+' keys='+(!!myKeys)+' room='+(!!roomID));
return;
@@ -2618,9 +2618,23 @@ async function handleSignal(raw){
let m; try { m = JSON.parse(raw); } catch(_){ return; }
switch (m.type){
case 'welcome':
- myUUID = m.your_uuid; myRole = m.role; roomEpoch = m.epoch;
- applyState(m.state);
- roomMachines.call.send('WELCOME', { uuid: myUUID, role: myRole });
+ {
+ /* a second welcome arrives whenever the signal-WS reconnects.
+ * If our uuid hasn't changed, the SFU + mesh are still alive —
+ * just refresh state + role, don't re-run the whole setup
+ * (that's what caused 'sfu publish: already publishing' +
+ * other duplicate-effect noise during reconnect cycles). */
+ const reentry = !!myUUID && myUUID === m.your_uuid;
+ myUUID = m.your_uuid; myRole = m.role; roomEpoch = m.epoch;
+ applyState(m.state);
+ roomMachines.call.send('WELCOME', { uuid: myUUID, role: myRole });
+ if (reentry){
+ logLine('', 'signal re-welcomed — role still '+myRole);
+ flushSfuStreams();
+ renderRoom();
+ break;
+ }
+ }
/* any tile auto-spotlit before welcome (during fast-path subscribe)
* needs to be broadcast now so the room sees our viewing state */
spotlights.set(myUUID, spotlightKey());
@@ -3491,8 +3505,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');