/* ESPILON Honeypot Dashboard — Overview Tab */ import { S, SERVICES, MONITORS } from './state.js'; import { $id, escHtml, formatTime, countryFlag, sevClass, layerForType, layerColor, animateCounter, emptyState } from './utils.js'; import { renderTimeline, renderSevDonut } from './charts.js'; import { postApi, fetchAll } from './api.js'; import { showToast } from './ui.js'; const DEVICE_IMG = '/hp-static/hp/img/floating.png'; // ── Device Cards ──────────────────────────────────────────── function renderDeviceCards() { if (!S.devices.length) { return emptyState('\uD83D\uDCE1', 'No honeypot devices connected', 'Connect an ESP32 honeypot to start monitoring'); } let html = ''; S.devices.forEach(d => { // BUG FIX: case-insensitive status comparison const st = (d.status || '').toLowerCase(); const isOnline = st === 'online' || st === 'connected'; const dotColor = isOnline ? 'var(--status-success)' : 'var(--sev-high)'; const statusTxt = isOnline ? 'Online' : 'Offline'; const lastSeen = d.last_seen ? formatTime(d.last_seen) : '--'; let runCount = 0, totalSvc = 0; const devSvc = S.services[d.id] || {}; for (const sn in devSvc) { totalSvc++; if (devSvc[sn] && devSvc[sn].running) runCount++; } let evtCount = 0; S.events.forEach(ev => { if (ev.device_id === d.id) evtCount++; }); const sevCounts = {CRITICAL:0, HIGH:0, MEDIUM:0, LOW:0}; S.events.forEach(ev => { if (ev.device_id === d.id && sevCounts[ev.severity] !== undefined) sevCounts[ev.severity]++; }); html += '
'; html += '
'; html += '
'; html += 'Honeypot'; html += '
'; html += '
'; html += '
'; html += '
' + escHtml(d.id) + '
'; html += '
' + escHtml(d.ip || 'unknown') + '
'; html += '
' + statusTxt + '
'; html += '
Last seen: ' + lastSeen + '
'; html += '
'; // Stats bar html += '
'; html += '
' + runCount + '/' + totalSvc + '
Services
'; html += '
' + evtCount + '
Events
'; html += '
' + sevCounts.CRITICAL + '
Critical
'; html += '
'; // Severity mini-bar const sevTotal = Object.values(sevCounts).reduce((a, b) => a + b, 0) || 1; html += '
'; if (sevCounts.CRITICAL) html += '
'; if (sevCounts.HIGH) html += '
'; if (sevCounts.MEDIUM) html += '
'; if (sevCounts.LOW) html += '
'; html += '
'; html += '
'; }); return html; } // ── Top Attackers Table ───────────────────────────────────── function renderTopAttackersTable() { const top = S.attackers.slice(0, 8); if (!top.length) return emptyState('\uD83D\uDC64', 'No attackers', 'Attacker data will appear here'); let html = ''; top.forEach((a, i) => { const flag = countryFlag(a.country_code); html += '' + '' + '' + '' + ''; }); return html + '
#IPVendorCount
' + (i + 1) + '' + (flag ? flag + ' ' : '') + escHtml(a.ip) + '' + escHtml(a.vendor || '-') + '' + (a.count || 0) + '
'; } // ── Service Grid ──────────────────────────────────────────── function renderServiceGrid() { let svcStatus = {}; for (const devId in S.services) { const ds = S.services[devId]; for (const sn in ds) if (!svcStatus[sn] || ds[sn].running) svcStatus[sn] = ds[sn]; } let html = '
'; Object.keys(SERVICES).forEach(name => { const running = svcStatus[name]?.running; html += '
' + '' + '' + escHtml(name) + '' + '' + escHtml(String(SERVICES[name].port)) + '' + '
'; }); MONITORS.forEach(name => { const running = svcStatus[name]?.running; html += '
' + '' + '' + escHtml(name) + '' + 'mon' + '
'; }); return html + '
'; } // ── Recent Events ─────────────────────────────────────────── function renderRecentEvents() { const recent = S.events.slice(0, 10); if (!recent.length) return emptyState('\uD83D\uDCCA', 'No events', 'Events will appear here as they are detected'); return '
' + recent.map(ev => { const layer = layerForType(ev.event_type); const detail = ev.detail ? (ev.detail.length > 60 ? ev.detail.slice(0, 60) + '...' : ev.detail) : ''; return '
' + '
' + '' + formatTime(ev.timestamp) + '' + '' + layer + '' + '' + escHtml(ev.service || '-') + '' + '' + escHtml(ev.severity) + '' + '' + escHtml(ev.src_ip || '') + '' + '' + escHtml(detail) + '' + '
'; }).join('') + '
'; } // ── Overview Render ───────────────────────────────────────── export function renderOverview() { const ml = $id('main-list'); if (!ml) return; const bs = S.stats.by_severity || {}; const totalEvts = S.stats.total_events || 0; const critCount = bs.CRITICAL || 0; let activeCount = 0; for (const devId in S.services) for (const sn in S.services[devId]) if (S.services[devId][sn]?.running) activeCount++; const kpis = [ {val: totalEvts, label: 'Total Events', color: 'var(--accent-primary)'}, {val: critCount, label: 'Critical', color: 'var(--sev-crit)'}, {val: S.attackers.length, label: 'Attackers', color: 'var(--status-warning)'}, {val: activeCount, label: 'Services', color: 'var(--status-success)'}, {val: S.alerts.filter(a => !a.acknowledged).length, label: 'Alerts', color: 'var(--accent-secondary)'} ]; let html = '
'; html += '
'; kpis.forEach(k => { html += '
' + '
' + k.val + '
' + '
' + k.label + '
'; }); html += '
'; // Device cards row if (S.devices.length) { html += '
'; html += '
\uD83D\uDCE1 Honeypot Devices
'; html += '
'; html += renderDeviceCards(); html += '
'; } // Charts row html += '
'; html += '
'; html += '
Activity Timeline
'; html += '
'; html += '
'; html += '
Severity
'; html += '
'; // Data row html += '
'; html += '
'; html += '
Top Attackers
'; html += renderTopAttackersTable() + '
'; html += '
'; html += '
Services'; html += ''; html += ''; html += ''; html += ''; html += '
'; html += renderServiceGrid() + '
'; // Recent events html += '
'; html += '
Recent Events
'; html += renderRecentEvents() + '
'; ml.innerHTML = html; renderTimeline($id('overview-timeline'), S.timeline, 100); renderSevDonut($id('overview-donut'), bs); } // ── Header KPI Updates ────────────────────────────────────── export function updateHeaderKpis() { const bs = S.stats.by_severity || {}; animateCounter($id('kpi-events'), S.stats.total_events || 0); animateCounter($id('kpi-critical'), bs.CRITICAL || 0); animateCounter($id('kpi-attackers'), S.attackers.length); const unacked = S.alerts.filter(a => !a.acknowledged).length; animateCounter($id('kpi-alerts'), unacked); } // ── Device Select ─────────────────────────────────────────── export function renderDeviceSelect() { const sel = $id('device-select'); if (!sel) return; const cur = sel.value; let html = ''; S.devices.forEach(d => { html += ''; }); sel.innerHTML = html; } // ── Status Bar ────────────────────────────────────────────── export function updateStatusBar() { const dot = $id('status-conn-dot'), text = $id('status-conn-text'); if (dot) dot.classList.toggle('connected', S.sseConnected); if (text) text.textContent = S.sseConnected ? 'Connected' : 'Disconnected'; const ref = $id('status-refresh'); if (ref) ref.textContent = new Date().toLocaleTimeString('en-GB'); const dev = $id('status-device'); if (dev) dev.textContent = S.selectedDevice || 'All Devices'; // Rate calc const now = Date.now(), cutoff = now - 60000; S._eventTimes = S._eventTimes.filter(t => t > cutoff); S.eventRate = S._eventTimes.length; const rEl = $id('status-rate'); if (rEl) rEl.textContent = S.eventRate + ' evt/min'; const dbEl = $id('status-db-count'); if (dbEl) dbEl.textContent = (S.stats.total_events || 0).toLocaleString() + ' stored'; } // ── Alert Banner ──────────────────────────────────────────── export function updateAlertBanner() { const banner = $id('alert-banner'); if (!banner) return; const unacked = S.alerts.filter(a => !a.acknowledged); if (!unacked.length) { banner.classList.remove('active'); return; } banner.classList.add('active'); const txt = $id('alert-banner-text'), cnt = $id('alert-banner-count'); if (txt) txt.textContent = unacked[0].message || unacked[0].rule_name || 'Alert'; if (cnt) cnt.textContent = unacked.length > 1 ? '+' + (unacked.length - 1) + ' more' : ''; } // ── Service Control Functions (migrated from config.js) ───── export async function toggleService(name, currentlyRunning) { if (!S.selectedDevice) { showToast('Error', 'No device selected', 'Select a device first.', 'error'); return; } /* Loading state feedback */ const btn = document.querySelector('[data-action="toggle-service"][data-name="' + name + '"]'); if (btn) { btn.disabled = true; btn.textContent = '...'; } const action = currentlyRunning ? 'stop' : 'start'; const cmd = 'hp_' + name + '_' + action; const res = await postApi('/api/honeypot/command', { device_id: S.selectedDevice, command: cmd, argv: [] }); if (res && res.ok) { showToast('Command Sent', cmd, 'Request ID: ' + (res.request_id || '?'), 'success'); } else { showToast('Error', 'Failed to send ' + cmd, '', 'error'); } setTimeout(() => { if (btn) btn.disabled = false; fetchAll(); }, 2000); } export async function startAll() { await postApi('/api/honeypot/start_all', { device_id: S.selectedDevice }); showToast('Services', 'Start all command sent', '', 'info'); setTimeout(fetchAll, 2000); } export async function stopAll() { await postApi('/api/honeypot/stop_all', { device_id: S.selectedDevice }); showToast('Services', 'Stop all command sent', '', 'info'); setTimeout(fetchAll, 2000); } export async function refreshStatus() { await postApi('/api/honeypot/refresh_status', { device_id: S.selectedDevice }); showToast('Status', 'Refresh requested', '', 'info'); setTimeout(fetchAll, 1000); } export async function ackAlert(id) { const res = await postApi('/api/honeypot/alerts/ack/' + id, {}); if (res && res.ok) { S.alerts = S.alerts.filter(a => a.id !== id); showToast('Alert', 'Alert acknowledged', '', 'success'); } else { showToast('Error', 'Failed to acknowledge alert', '', 'error'); } }