diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 2409710..2da6459 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -82,15 +82,30 @@ unmute.addEventListener('click', () => { setMuted(false); renderControls(); }); renderControls(); } - function show(title, body) { - if (!supported || Notification.permission !== 'granted' || isMuted()) return; + function show(title, body, tag) { + if (!supported) { + console.debug('notify: Notification API not supported'); + return; + } + if (Notification.permission !== 'granted') { + console.debug('notify: permission not granted', Notification.permission); + return; + } + if (isMuted()) { + console.debug('notify: muted'); + return; + } try { + // Per-event tag so distinct messages stack instead of + // collapsing into one slot. Caller passes a unique tag per + // notification kind/id; we don't fall back to 'hyperhive' + // because that one tag would replace itself on every fire. const n = new Notification(title, { body, - tag: 'hyperhive', // collapse rapid bursts - icon: '/static/dashboard.css', // any same-origin asset works as a favicon stand-in + tag: tag || ('hyperhive:' + Date.now()), }); n.onclick = () => { window.focus(); n.close(); }; + console.debug('notify: shown', title, 'tag=', tag); } catch (err) { console.warn('notification show failed', err); } @@ -124,12 +139,14 @@ if (seenApprovals.has(a.id)) continue; seenApprovals.add(a.id); const verb = a.kind === 'spawn' ? 'spawn approval' : 'config commit'; - NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`); + NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`, + 'hyperhive:approval:' + a.id); } for (const q of questions) { if (seenQuestions.has(q.id)) continue; seenQuestions.add(q.id); - NOTIF.show('◆ manager asks', q.question.slice(0, 120)); + NOTIF.show('◆ manager asks', q.question.slice(0, 120), + 'hyperhive:question:' + q.id); } // operator_inbox: only notify on truly new ids — sse already // handles single-message notifications, but if the operator @@ -658,7 +675,13 @@ // the OS notification center. if (m.kind === 'sent' && m.to === 'operator') { refreshState(); - NOTIF.show('◆ ' + m.from + ' → operator', String(m.body || '').slice(0, 200)); + NOTIF.show( + '◆ ' + m.from + ' → operator', + String(m.body || '').slice(0, 200), + // Unique-per-arrival tag so a burst stacks instead of + // overwriting itself in the OS notification center. + 'hyperhive:msg:' + m.at + ':' + Math.random().toString(36).slice(2, 6), + ); } const row = document.createElement('div'); row.className = 'msgrow ' + m.kind;