diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index 3c85355..0a69770 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -817,6 +817,17 @@ function hex(buf){ const u8=buf instanceof Uint8Array?buf:new Uint8Array(buf); r
function shortHex(s){ return s.slice(0,4)+'…'+s.slice(-4); }
const logEl = $('log');
+/* Global error handler — runtime errors used to die silently (no
+ * stack trace anywhere), so a broken button just stopped working
+ * with no signal-server visibility. Now every uncaught exception
+ * AND unhandled-promise-rejection forwards to CLIENT_LOG as kind=err. */
+window.addEventListener('error', (ev) => {
+ try { logLine('err', 'JS error: '+ev.message+' at '+(ev.filename||'?')+':'+(ev.lineno||'?')); } catch(_){}
+});
+window.addEventListener('unhandledrejection', (ev) => {
+ try { logLine('err', 'JS unhandled: '+(ev.reason && (ev.reason.message || ev.reason.toString()) || 'unknown')); } catch(_){}
+});
+
function logLine(kind, msg){
const d=document.createElement('div'); d.className='log-line '+(kind||'');
d.innerHTML=''+new Date().toLocaleTimeString()+' '+msg.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
@@ -4864,6 +4875,7 @@ $('btn-copy-share').addEventListener('click', async () => {
* the same channel cameras/screens use so the room sees the choice */
renderGamesColumn();
$('btn-leave').addEventListener('click', async () => {
+ logLine('', 'leave-button: click fired');
wantConnected = false;
roomMachines.call.send('LEAVE');
/* explicit leave wipes the auto-rejoin state — user said 'out' */
@@ -4932,8 +4944,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');