/* ESPILON Honeypot Dashboard — Application Entry Point */ import { S } from './state.js'; import { $id } from './utils.js'; import { setToastHandler, setDefaultRender, fetchAll } from './api.js'; import { connectSSE, setSSECallbacks } from './sse.js'; import { showToast, closeDetail, closeModal, toggleSidebar, toggleNavMenu, toggleSbSection, scrollToAlerts, restoreSidebarState, registerActions, setupEventDelegation, setupResponsive } from './ui.js'; import { setTabRenderers, switchTab, setupKeyboardShortcuts } from './router.js'; import { renderOverview, updateHeaderKpis, updateStatusBar, updateAlertBanner, renderDeviceSelect, toggleService, startAll, stopAll, refreshStatus, ackAlert } from './overview.js'; import { renderEvents, prependEventRow, showDetail, showAttackerModal, switchModalTab, onSearchKey, applyFilters, toggleFilters, exportData, loadMoreEvents } from './events.js'; import { renderSessions, openReplay, toggleReplayPlayback, replayStep, replayReset, seekReplay, updateReplaySpeed, closeReplay } from './sessions.js'; import { renderCredentials } from './credentials.js'; import { renderKillChain, showKillChainDetail } from './killchain.js'; import { renderMitre } from './mitre.js'; import { renderSidebar } from './sidebar.js'; // ── Wire Dependencies ─────────────────────────────────────── // Break circular: api.js needs showToast from ui.js setToastHandler(showToast); // Default render callback for fetchAll() (so calls without explicit callback still re-render) setDefaultRender(renderAll); // Wire tab renderers into the router setTabRenderers({ overview: renderAll, timeline: renderEvents, sessions: renderSessions, credentials: renderCredentials, killchain: renderKillChain, mitre: renderMitre, }); // Wire SSE callbacks setSSECallbacks({ onNewEvent(evt) { // Prepend row if on events tab prependEventRow(evt); // Update sidebar renderSidebar(); // Update header KPIs updateHeaderKpis(); updateAlertBanner(); }, onStatsUpdate() { // Refresh overview KPIs without full re-render updateHeaderKpis(); } }); // ── Tab content renderers (main-list area) ────────────────── const _contentRenderers = { overview: renderOverview, timeline: renderEvents, sessions: renderSessions, credentials: renderCredentials, killchain: renderKillChain, mitre: renderMitre, }; // ── Render All (after fetchAll) ───────────────────────────── function renderAll() { renderDeviceSelect(); // Render the currently active tab, not always overview const renderer = _contentRenderers[S.tab] || renderOverview; renderer(); renderSidebar(); updateHeaderKpis(); updateAlertBanner(); updateStatusBar(); } // ── Event Delegation: register all actions ────────────────── registerActions({ 'tab': (el) => switchTab(el.dataset.tab), 'detail': (el) => showDetail(parseInt(el.dataset.id)), 'attacker': (el) => showAttackerModal(el.dataset.ip), 'replay': (el) => openReplay(el.dataset.session, el.dataset.ip, el.dataset.service), 'killchain-detail': (el) => showKillChainDetail(el.dataset.ip), 'toggle-sidebar': () => toggleSidebar(), 'toggle-nav': () => toggleNavMenu(), 'toggle-section': (el) => toggleSbSection(el), 'close-detail': () => closeDetail(), 'close-modal': () => closeModal(), 'close-replay': () => closeReplay(), 'scroll-alerts': () => scrollToAlerts(), 'toggle-sound': () => { S.soundEnabled = !S.soundEnabled; const btn = $id('sound-btn'); if (btn) btn.style.opacity = S.soundEnabled ? '1' : '0.4'; showToast('Sound', S.soundEnabled ? 'Alert sounds enabled' : 'Alert sounds muted', '', 'info'); }, 'toggle-notif': () => { if (!S.notifEnabled && typeof Notification !== 'undefined' && Notification.permission !== 'granted') { Notification.requestPermission().then(function(perm) { if (perm === 'granted') { S.notifEnabled = true; var btn = $id('notif-btn'); if (btn) btn.style.opacity = '1'; showToast('Notifications', 'Browser notifications enabled', '', 'success'); } else { showToast('Notifications', 'Permission denied by browser', '', 'warning'); } }); } else { S.notifEnabled = !S.notifEnabled; var btn = $id('notif-btn'); if (btn) btn.style.opacity = S.notifEnabled ? '1' : '0.4'; showToast('Notifications', S.notifEnabled ? 'Browser notifications enabled' : 'Browser notifications disabled', '', 'info'); } }, 'toggle-service': (el) => toggleService(el.dataset.name, el.dataset.running === '1'), 'start-all': () => startAll(), 'stop-all': () => stopAll(), 'refresh-status': () => refreshStatus(), 'ack-alert': (el) => ackAlert(parseInt(el.dataset.id)), 'toggle-filters': () => toggleFilters(), 'load-more-events': () => loadMoreEvents(), 'export-csv': () => exportData('csv'), 'export-json': () => exportData('json'), 'dismiss-banner': (el, e) => { e.stopPropagation(); $id('alert-banner')?.classList.remove('active'); }, 'replay-play': () => toggleReplayPlayback(), 'replay-step': () => replayStep(), 'replay-reset': () => replayReset(), 'replay-seek': (el, e) => seekReplay(e), 'modal-tab': (el) => switchModalTab(el.dataset.tab), 'copy-json': () => { if (window._detailEvtJson) { navigator.clipboard.writeText(window._detailEvtJson); showToast('Copied', 'Event JSON copied', '', 'success'); } }, }); // ── Init ──────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', function() { restoreSidebarState(); setupEventDelegation(); setupResponsive(); // Form element listeners (change/keyup — not click-based) const devSel = $id('device-select'); if (devSel) devSel.addEventListener('change', () => { S.selectedDevice = devSel.value; fetchAll(renderAll); }); const searchIn = $id('search-input'); if (searchIn) searchIn.addEventListener('keyup', () => onSearchKey()); const searchClear = $id('search-clear'); if (searchClear) searchClear.addEventListener('click', () => { if (searchIn) { searchIn.value = ''; onSearchKey(); searchIn.focus(); } }); ['f-type', 'f-sev', 'f-service'].forEach(id => { const el = $id(id); if (el) el.addEventListener('change', () => applyFilters()); }); const fIp = $id('f-ip'); if (fIp) fIp.addEventListener('change', () => applyFilters()); const replaySpd = $id('replay-speed'); if (replaySpd) replaySpd.addEventListener('change', () => updateReplaySpeed()); const sevFilter = $id('sse-sev-filter'); if (sevFilter) sevFilter.addEventListener('change', () => { S.minSeverity = sevFilter.value; connectSSE(); }); setupKeyboardShortcuts({ onEscape() { closeDetail(); closeModal(); closeReplay(); }, onRefresh() { refreshStatus(); }, onToggleSound() { S.soundEnabled = !S.soundEnabled; const btn = $id('sound-btn'); if (btn) btn.style.opacity = S.soundEnabled ? '1' : '0.4'; showToast('Sound', S.soundEnabled ? 'Alert sounds enabled' : 'Alert sounds muted', '', 'info'); } }); fetchAll(renderAll); connectSSE(); /* Conditional auto-refresh: only re-render if data actually changed */ let _lastDataHash = ''; function renderIfChanged() { const hash = JSON.stringify([ S.stats.total_events, S.events.length, S.alerts.length, S.devices.length, Object.keys(S.services).length ]); if (hash !== _lastDataHash) { _lastDataHash = hash; renderAll(); } } S._refreshTimer = setInterval(() => fetchAll(renderIfChanged), 30000); setInterval(updateStatusBar, 1000); });