espilon-source/tools/C3PO/hp_dashboard/static/hp/js/sse.js
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
Replace monolithic CLI and web server with route-based Flask API.
New routes: api_commands, api_build, api_can, api_monitor, api_ota,
api_tunnel. Add honeypot security dashboard with real-time SSE,
MITRE ATT&CK mapping, kill chain analysis.

New TUI with commander/help modules. Add session management,
tunnel proxy core, CAN bus data store. Docker support.
2026-02-28 20:12:27 +01:00

107 lines
4.1 KiB
JavaScript

/* 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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="%231a1a28"/><circle cx="16" cy="16" r="9" fill="none" stroke="%23a855f7" stroke-width="1.5"/></svg>',
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);
};
}