/** * Events dashboard: polls GET /api/events every 5s, renders the list with * severity text+icon+color (never color alone, per NF9), pins unacknowledged * events to the top, and wires the acknowledge button to POST /events/{id}/ack. * * XSS-safe: all dynamic values are set via textContent, never innerHTML. */ (function () { 'use strict'; var POLL_INTERVAL_MS = 5000; var SEVERITY_LABELS = { critical: { icon: '⛔', text: '위급', cls: 'sev-critical' }, // ⛔ high: { icon: '⚠', text: '높음', cls: 'sev-high' }, // ⚠ warning: { icon: '△', text: '경고', cls: 'sev-warning' }, // △ info: { icon: 'ℹ', text: '정보', cls: 'sev-info' } // ℹ }; var root = document.getElementById('events-root'); if (!root) return; function severityInfo(sev) { return SEVERITY_LABELS[sev] || { icon: 'ℹ', text: sev || '알수없음', cls: 'sev-info' }; } function formatTime(occurredAt) { if (!occurredAt && occurredAt !== 0) return ''; var d = new Date(Number(occurredAt) * 1000); if (isNaN(d.getTime())) return ''; return d.toLocaleString('ko-KR'); } function buildBadge(sev) { var info = severityInfo(sev); var badge = document.createElement('span'); badge.className = 'severity-badge ' + info.cls; var icon = document.createElement('span'); icon.className = 'icon'; icon.setAttribute('aria-hidden', 'true'); icon.textContent = info.icon; var label = document.createElement('span'); label.className = 'label'; label.textContent = info.text; badge.appendChild(icon); badge.appendChild(label); return badge; } function buildRow(ev) { var isAcked = ev.acknowledged_by !== null && ev.acknowledged_by !== undefined && ev.acknowledged_by !== ''; var row = document.createElement('div'); row.className = 'event-row' + (isAcked ? '' : ' is-unacknowledged'); row.appendChild(buildBadge(ev.severity)); var meta = document.createElement('div'); meta.className = 'event-meta'; var type = document.createElement('div'); type.className = 'event-type'; type.textContent = String(ev.event_type || ''); meta.appendChild(type); var sub = document.createElement('div'); sub.className = 'event-time'; var cam = ev.camera_id ? String(ev.camera_id) : ''; var time = formatTime(ev.occurred_at); sub.textContent = [cam, time].filter(Boolean).join(' · '); meta.appendChild(sub); row.appendChild(meta); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ack-btn'; if (isAcked) { btn.textContent = '확인됨 (' + String(ev.acknowledged_by) + ')'; btn.disabled = true; } else { btn.textContent = '확인'; btn.addEventListener('click', function () { acknowledge(ev.event_id, btn); }); } row.appendChild(btn); return row; } function acknowledge(eventId, btn) { btn.disabled = true; fetch('/events/' + encodeURIComponent(eventId) + '/ack', { method: 'POST' }) .then(function (res) { if (!res.ok) throw new Error('ack_failed:' + res.status); return loadEvents(); }) .catch(function () { btn.disabled = false; }); } function render(events) { var sorted = events.slice().sort(function (a, b) { var aAcked = a.acknowledged_by !== null && a.acknowledged_by !== undefined && a.acknowledged_by !== ''; var bAcked = b.acknowledged_by !== null && b.acknowledged_by !== undefined && b.acknowledged_by !== ''; if (aAcked === bAcked) return 0; return aAcked ? 1 : -1; // unacknowledged first }); root.textContent = ''; root.removeAttribute('aria-busy'); if (sorted.length === 0) { root.textContent = '표시할 이벤트가 없습니다.'; return; } var frag = document.createDocumentFragment(); sorted.forEach(function (ev) { frag.appendChild(buildRow(ev)); }); root.appendChild(frag); } function loadEvents() { return fetch('/api/events', { headers: { Accept: 'application/json' } }) .then(function (res) { if (res.status === 401) { window.location.href = '/login'; return null; } if (!res.ok) throw new Error('list_failed:' + res.status); return res.json(); }) .then(function (data) { if (data && Array.isArray(data.events)) render(data.events); }) .catch(function () { // Leave the last rendered state on transient poll failure. }); } var logoutBtn = document.getElementById('logout-btn'); if (logoutBtn) { logoutBtn.addEventListener('click', function () { fetch('/logout', { method: 'POST' }).then(function () { window.location.href = '/login'; }); }); } loadEvents(); setInterval(loadEvents, POLL_INTERVAL_MS); })();