feat(cloud): dashboard rows link to detail + desk Web Audio on critical

This commit is contained in:
유창욱 2026-07-12 11:49:07 +09:00
parent 55d99602ce
commit b0d1241da1

View file

@ -55,6 +55,55 @@
return badge; return badge;
} }
// --- desk audio alert: repeating beep while any unacknowledged critical exists ---
// No binary asset (air-gapped friendly): a WebAudio oscillator tone. The browser
// blocks audio until a user gesture, so the context is created on first click.
var audioCtx = null;
var alarmTimer = null;
var beepedCritical = {}; // event_id -> true; each new critical beeps once immediately
function unlockAudio() {
if (!audioCtx) {
var AC = window.AudioContext || window.webkitAudioContext;
if (AC) audioCtx = new AC();
}
document.removeEventListener('click', unlockAudio);
}
function beep() {
if (!audioCtx) return;
try {
var osc = audioCtx.createOscillator();
var gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = 880;
gain.gain.value = 0.2;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.4);
} catch (e) {
// audio unavailable; the visual list still surfaces the critical event
}
}
function updateAlarm(events) {
var hasUnackedCritical = false;
events.forEach(function (ev) {
var acked = ev.acknowledged_by !== null && ev.acknowledged_by !== undefined && ev.acknowledged_by !== '';
if (ev.severity === 'critical' && !acked) {
hasUnackedCritical = true;
if (!beepedCritical[ev.event_id]) { beepedCritical[ev.event_id] = true; beep(); }
}
});
if (hasUnackedCritical && !alarmTimer) {
alarmTimer = setInterval(beep, 3000);
} else if (!hasUnackedCritical && alarmTimer) {
clearInterval(alarmTimer);
alarmTimer = null;
}
}
function buildRow(ev) { function buildRow(ev) {
var isAcked = ev.acknowledged_by !== null && ev.acknowledged_by !== undefined && ev.acknowledged_by !== ''; var isAcked = ev.acknowledged_by !== null && ev.acknowledged_by !== undefined && ev.acknowledged_by !== '';
@ -68,7 +117,11 @@
var type = document.createElement('div'); var type = document.createElement('div');
type.className = 'event-type'; type.className = 'event-type';
type.textContent = String(ev.event_type || ''); var typeLink = document.createElement('a');
typeLink.className = 'event-detail-link';
typeLink.href = '/events/' + encodeURIComponent(ev.event_id) + '/view';
typeLink.textContent = String(ev.event_type || '');
type.appendChild(typeLink);
meta.appendChild(type); meta.appendChild(type);
var sub = document.createElement('div'); var sub = document.createElement('div');
@ -118,6 +171,8 @@
return aAcked ? 1 : -1; // unacknowledged first return aAcked ? 1 : -1; // unacknowledged first
}); });
updateAlarm(sorted); // before any early return, so an emptied list also silences the alarm
root.textContent = ''; root.textContent = '';
root.removeAttribute('aria-busy'); root.removeAttribute('aria-busy');
if (sorted.length === 0) { if (sorted.length === 0) {
@ -154,6 +209,8 @@
}); });
} }
document.addEventListener('click', unlockAudio);
loadCsrfToken(); loadCsrfToken();
loadEvents(); loadEvents();
setInterval(loadEvents, POLL_INTERVAL_MS); setInterval(loadEvents, POLL_INTERVAL_MS);