diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index e5ef772..ddf56de 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -449,7 +449,12 @@ let sfuPubPC = null, sfuPubPeerID = null;
let sfuSubPC = null, sfuSubPeerID = null, sfuSubEvents = null;
async function sfuPublish(){
- if (sfuPubPC || !micStream || !myKeys || !roomID) return;
+ if (sfuPubPC){ logLine('','sfu publish: already publishing'); return; }
+ if (!micStream || !myKeys || !roomID){
+ logLine('err','sfu publish skipped: mic='+(!!micStream)+' keys='+(!!myKeys)+' room='+(!!roomID));
+ return;
+ }
+ logLine('', 'sfu publish: starting (base='+SFU_BASE+')');
const pc = new RTCPeerConnection(rtcConfig);
for (const tr of micStream.getTracks()){ tagTrack(tr); pc.addTrack(tr, micStream); }
setSenderBitrate(pc.getSenders().find(s=>s.track && s.track.kind==='audio'));
@@ -1011,6 +1016,12 @@ async function modInvite(uuid){
const sig = await signBytes(sigAction(roomID, roomEpoch, 'mic-invite', uuid));
send({ type:'mic-invite', to: uuid, epoch: roomEpoch, sig });
}
+/* grant-mic: hand-raised listener doesn't need to accept — server promotes
+ * them directly to speaker. Use modInvite for cold (unsolicited) invites. */
+async function modGrant(uuid){
+ const sig = await signBytes(sigAction(roomID, roomEpoch, 'grant-mic', uuid));
+ send({ type:'grant-mic', to: uuid, epoch: roomEpoch, sig });
+}
async function modPromote(uuid){
const sig = await signBytes(sigAction(roomID, roomEpoch, 'promote', uuid, 'cohost'));
send({ type:'promote', target: uuid, to: 'cohost', epoch: roomEpoch, sig });
@@ -1067,8 +1078,11 @@ function renderRoom(){
if (isMod(myRole) && m.uuid !== myUUID){
if (m.role === 'listener'){
const b = document.createElement('button'); b.className='small invert';
- b.textContent = handraise.has(m.uuid) ? 'give the mic' : 'invite mic';
- b.onclick = () => modInvite(m.uuid).catch(e=>logLine('err','invite: '+e.message));
+ const raised = handraise.has(m.uuid);
+ b.textContent = raised ? 'give the mic' : 'invite mic';
+ b.onclick = raised
+ ? () => modGrant(m.uuid).catch(e=>logLine('err','grant: '+e.message))
+ : () => modInvite(m.uuid).catch(e=>logLine('err','invite: '+e.message));
acts.appendChild(b);
}
if (m.role === 'speaker' && myRole === 'host'){
@@ -1197,8 +1211,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');