/* ESPILON Honeypot Dashboard — Server-Sent Events */ import { S } from './state.js'; import { $id } from './utils.js'; import { playAlertSound } from './audio.js'; import { showToast } from './ui.js'; // Callbacks set by app.js to avoid circular imports. // onNewEvent(evt) — called for every new event (render row, update sidebar, etc.) // onStatsUpdate() — called after in-memory stats are patched let _onNewEvent = null; let _onStatsUpdate = null; export function setSSECallbacks({ onNewEvent, onStatsUpdate }) { _onNewEvent = onNewEvent; _onStatsUpdate = onStatsUpdate; } export function connectSSE() { if (S._eventSource) { try { S._eventSource.close(); } catch (x) { /* ignore */ } } const sev = S.minSeverity || 'MEDIUM'; const es = new EventSource('/api/honeypot/stream?min_severity=' + sev); S._eventSource = es; es.onopen = function() { S.sseConnected = true; S._sseRetry = 0; const d1 = $id('conn-dot'), d2 = $id('status-conn-dot'); if (d1) d1.classList.add('connected'); if (d2) d2.classList.add('connected'); const l1 = $id('conn-label'), l2 = $id('status-conn-text'); if (l1) l1.textContent = 'Live'; if (l2) l2.textContent = 'Connected'; }; es.onmessage = function(e) { let evt; try { evt = JSON.parse(e.data); } catch (x) { return; } if (evt.type === 'connected' || evt.type === 'keepalive') return; // Buffer into events array S.events.unshift(evt); if (S.events.length > 500) S.events.length = 500; if (evt.id && evt.id > S.lastId) S.lastId = evt.id; // Track event times for rate calculation S._eventTimes.push(Date.now()); if (S._eventTimes.length > 1000) S._eventTimes = S._eventTimes.slice(-500); // Update in-memory stats if (S.stats.by_severity) { S.stats.by_severity[evt.severity] = (S.stats.by_severity[evt.severity] || 0) + 1; } if (S.stats.by_type) { S.stats.by_type[evt.event_type] = (S.stats.by_type[evt.event_type] || 0) + 1; } S.stats.total_events = (S.stats.total_events || 0) + 1; // Notify app-level callbacks if (_onNewEvent) _onNewEvent(evt); if (_onStatsUpdate) _onStatsUpdate(); // Update events badge const badge = $id('nav-badge-events'); if (badge && S.tab !== 'timeline') { const n = parseInt(badge.textContent || '0') + 1; badge.textContent = n; badge.style.display = 'inline-flex'; } // Alert sounds if (S.soundEnabled && (evt.severity === 'CRITICAL' || evt.severity === 'HIGH')) { playAlertSound(evt.severity); } // Browser notifications if (S.notifEnabled && (evt.severity === 'CRITICAL' || evt.severity === 'HIGH')) { try { if (Notification.permission === 'granted') { new Notification('ESPILON Alert — ' + evt.severity, { body: (evt.detail || evt.event_type || 'New event').substring(0, 120), icon: 'data:image/svg+xml,', tag: 'espilon-' + (evt.id || Date.now()), silent: true }); } } catch (x) { /* Notification API not available */ } } }; es.onerror = function() { S.sseConnected = false; es.close(); const d1 = $id('conn-dot'), d2 = $id('status-conn-dot'); if (d1) d1.classList.remove('connected'); if (d2) d2.classList.remove('connected'); const l1 = $id('conn-label'), l2 = $id('status-conn-text'); if (l1) l1.textContent = 'SSE'; if (l2) l2.textContent = 'Disconnected'; S._sseRetry++; const delay = Math.min(1000 * Math.pow(2, S._sseRetry), 30000); setTimeout(connectSSE, delay); }; }