Files
esp32-cluster/coordinator/templates/dashboard.html
T

355 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESP32 Recon</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/static/dashboard.css" />
</head>
<body>
<div id="app">
<!-- Topbar -->
<div id="topbar">
<span class="title">ESP32 Recon</span>
<span class="sep">/</span>
<span class="build-info">WiFi Recon Cluster</span>
<div id="live-dot"></div>
<span id="live-label">LIVE</span>
</div>
<!-- Navbar -->
<div id="navbar">
<button class="tab-btn active" id="tab-feed" onclick="showFeed()">Feed</button>
<button class="tab-btn" id="tab-networks" onclick="showNetworks()">Networks</button>
<button class="tab-btn" id="tab-crossnode" onclick="showCrossNode()">Cross-node</button>
<button class="tab-btn" id="tab-clients" onclick="showClients()">Clients</button>
<button class="tab-btn" id="tab-presence" onclick="showPresence()">Presence</button>
<button class="tab-btn" id="tab-alerts" onclick="showAlerts()">Alerts</button>
</div>
<!-- Sidebar -->
<div id="sidebar">
<div class="sidebar-section">
<div class="sidebar-label">Summary</div>
<div class="stat-row"><span>nodes</span><span id="sum-nodes"></span></div>
<div class="stat-row"><span>online</span><span id="sum-online"></span></div>
<div class="stat-row"><span>events (session)</span><span id="sum-events"></span></div>
</div>
<div class="sidebar-section">
<div class="sidebar-label">Nodes</div>
<div id="node-list"></div>
</div>
</div>
<!-- Main -->
<div id="main">
<!-- Feed view -->
<div id="view-general" class="view active">
<div class="section-header">
<span class="section-title">Live Feed</span>
<span class="section-count" id="general-count"></span>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>Time</th>
<th>Node</th>
<th>SSID</th>
<th>BSSID</th>
<th>RSSI</th>
<th>Ch</th>
<th>Enc</th>
<th>Imp</th>
</tr>
</thead>
<tbody id="general-tbody"></tbody>
</table>
<div class="empty" id="general-empty" style="display:none">No events yet.</div>
</div>
</div>
<!-- Node detail view -->
<div id="view-detail" class="view">
<button id="back-btn" onclick="showGeneral()">&#8592; All nodes</button>
<div class="section-header">
<span class="section-title">Node</span>
<span class="section-count" id="detail-node-id"></span>
</div>
<div id="detail-stats-grid">
<div class="stat-card"><div class="label">Status</div><div class="value" id="ds-status"></div></div>
<div class="stat-card"><div class="label">Total Events</div><div class="value" id="ds-total"></div></div>
<div class="stat-card"><div class="label">Unique SSIDs</div><div class="value" id="ds-ssids"></div></div>
<div class="stat-card"><div class="label">Unique BSSIDs</div><div class="value" id="ds-bssids"></div></div>
<div class="stat-card"><div class="label">Avg RSSI</div><div class="value" id="ds-rssi-avg"></div><div class="sub" id="ds-rssi-range"></div></div>
<div class="stat-card"><div class="label">First Seen</div><div class="value" style="font-size:12px" id="ds-first"></div></div>
<div class="stat-card"><div class="label">Last Seen</div><div class="value" style="font-size:12px" id="ds-last"></div></div>
<div class="stat-card"><div class="label">Uptime</div><div class="value" id="ds-uptime"></div></div>
<div class="stat-card"><div class="label">Free Heap</div><div class="value" id="ds-heap"></div></div>
<div class="stat-card"><div class="label">AP Signal</div><div class="value" id="ds-ap-rssi"></div><div class="sub">node → router</div></div>
</div>
<div class="section-header" style="margin-top:4px">
<span class="section-title">Recent Events</span>
<span class="section-count" id="detail-count"></span>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>Time</th>
<th>SSID</th>
<th>BSSID</th>
<th>RSSI</th>
<th>Ch</th>
<th>Enc</th>
<th>Imp</th>
<th>Conf</th>
</tr>
</thead>
<tbody id="detail-tbody"></tbody>
</table>
</div>
</div>
<!-- Clients view -->
<div id="view-clients" class="view">
<div class="section-header">
<span class="section-title">Client Devices</span>
<span class="section-count" id="clients-count"></span>
<span style="margin-left:auto; font-size:10px; color:var(--text-muted)">
note: modern devices use randomised MACs — one physical device may appear as multiple entries
</span>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>MAC</th>
<th>Vendor</th>
<th>Probing for</th>
<th>Best RSSI</th>
<th>Avg RSSI</th>
<th>Nodes</th>
<th>Times Seen</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="clients-tbody"></tbody>
</table>
<div class="empty" id="clients-empty" style="display:none">No probe events yet. Nodes need to be flashed with probe sniffing enabled.</div>
</div>
</div>
<!-- Cross-node view -->
<div id="view-crossnode" class="view">
<div class="section-header">
<span class="section-title">Cross-node RSSI</span>
<span class="section-count" id="crossnode-count"></span>
<label style="margin-left:auto; font-size:10px; color:var(--text-dim); display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="crossnode-filter" onchange="renderCrossNodeTable()" style="accent-color:var(--green-hi)">
multi-node only
</label>
</div>
<div class="tbl-wrap">
<table id="crossnode-table">
<thead id="crossnode-thead"></thead>
<tbody id="crossnode-tbody"></tbody>
</table>
<div class="empty" id="crossnode-empty" style="display:none">No data yet.</div>
</div>
</div>
<!-- Network RSSI chart view -->
<div id="view-network-chart" class="view">
<button class="back-btn" onclick="showNetworks()">&#8592; Networks</button>
<div class="section-header">
<span class="section-title" id="chart-title">RSSI History</span>
<span class="section-count" id="chart-subtitle"></span>
<div class="range-btns" style="margin-left:auto">
<button class="range-btn" data-hours="1" onclick="setChartRange(1)">1h</button>
<button class="range-btn active" data-hours="2" onclick="setChartRange(2)">2h</button>
<button class="range-btn" data-hours="6" onclick="setChartRange(6)">6h</button>
<button class="range-btn" data-hours="24" onclick="setChartRange(24)">24h</button>
</div>
</div>
<div id="chart-legend"></div>
<canvas id="rssi-chart" height="240"></canvas>
<div class="empty" id="chart-empty" style="display:none">No data for this time range.</div>
</div>
<!-- Networks view -->
<div id="view-networks" class="view">
<div class="section-header">
<span class="section-title">Networks</span>
<span class="section-count" id="networks-count"></span>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th class="sortable" data-col="ssid" data-arrow="">SSID</th>
<th class="sortable" data-col="bssid" data-arrow="">BSSID</th>
<th class="sortable" data-col="best_rssi" data-arrow="">Best RSSI</th>
<th class="sortable" data-col="avg_rssi" data-arrow="">Avg RSSI</th>
<th class="sortable" data-col="channel" data-arrow="">Ch</th>
<th class="sortable" data-col="encryption" data-arrow="">Enc</th>
<th class="sortable sort-active" data-col="times_seen" data-arrow="↓">Times Seen</th>
<th class="sortable" data-col="node_count" data-arrow="">Nodes</th>
<th class="sortable" data-col="last_seen" data-arrow="">Last Seen</th>
<th></th>
</tr>
</thead>
<tbody id="networks-tbody"></tbody>
</table>
<div class="empty" id="networks-empty" style="display:none">No networks seen yet.</div>
</div>
</div>
<!-- Presence view -->
<div id="view-presence" class="view">
<!-- Present now -->
<div class="section-header">
<span class="section-title">Present Now</span>
<span class="section-count" id="present-count"></span>
<span class="presence-sub">real MACs · seen 2+ times in last hour</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>MAC</th><th>Vendor</th><th>Best RSSI</th><th>Avg RSSI</th>
<th>Times Seen</th><th>Nodes</th><th>Last Seen</th>
</tr></thead>
<tbody id="present-tbody"></tbody>
</table>
<div class="empty" id="present-empty" style="display:none">No devices currently present.</div>
</div>
<!-- New arrivals -->
<div class="section-header" style="margin-top:8px">
<span class="section-title">New Arrivals</span>
<span class="section-count" id="arrivals-count"></span>
<span class="presence-sub">first seen in last 24h</span>
</div>
<div class="presence-split">
<div>
<div class="presence-sub-label">Devices</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>MAC</th><th>Vendor</th><th>First Seen</th>
<th>Best RSSI</th><th>Times Seen</th>
</tr></thead>
<tbody id="arrivals-macs-tbody"></tbody>
</table>
<div class="empty" id="arrivals-macs-empty" style="display:none">No new devices.</div>
</div>
</div>
<div>
<div class="presence-sub-label">Networks</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>SSID</th><th>BSSID</th><th>First Seen</th>
<th>Best RSSI</th><th>Ch</th><th>Enc</th>
</tr></thead>
<tbody id="arrivals-nets-tbody"></tbody>
</table>
<div class="empty" id="arrivals-nets-empty" style="display:none">No new networks.</div>
</div>
</div>
</div>
<!-- Regulars -->
<div class="section-header" style="margin-top:8px">
<span class="section-title">Regulars</span>
<span class="section-count" id="regulars-count"></span>
<span class="presence-sub">seen on 2+ distinct days</span>
</div>
<div class="presence-split">
<div>
<div class="presence-sub-label">Devices</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>MAC</th><th>Vendor</th><th>Days Seen</th>
<th>First Seen</th><th>Last Seen</th><th>Best RSSI</th>
</tr></thead>
<tbody id="regulars-macs-tbody"></tbody>
</table>
<div class="empty" id="regulars-macs-empty" style="display:none">Regulars appear after 2+ days of data.</div>
</div>
</div>
<div>
<div class="presence-sub-label">Networks</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>SSID</th><th>BSSID</th><th>Days Seen</th>
<th>First Seen</th><th>Best RSSI</th><th>Ch</th>
</tr></thead>
<tbody id="regulars-nets-tbody"></tbody>
</table>
<div class="empty" id="regulars-nets-empty" style="display:none">Regulars appear after 2+ days of data.</div>
</div>
</div>
</div>
</div><!-- /#view-presence -->
<!-- Alerts view -->
<div id="view-alerts" class="view">
<!-- Summary strip -->
<div id="alerts-summary" class="alerts-summary"></div>
<!-- Detected bursts -->
<div class="section-header">
<span class="section-title">Detected Bursts</span>
<span class="section-count" id="bursts-count"></span>
<span class="presence-sub">≥10 deauth/disassoc frames for same BSSID in last 5 min</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>BSSID</th><th>SSID</th><th>Type</th><th>Frames</th>
<th>Unique Srcs</th><th>Nodes</th><th>First</th><th>Last</th>
</tr></thead>
<tbody id="bursts-tbody"></tbody>
</table>
<div class="empty" id="bursts-empty" style="display:none">No bursts detected in last 5 minutes.</div>
</div>
<!-- Raw deauth feed -->
<div class="section-header" style="margin-top:8px">
<span class="section-title">Recent Deauth / Disassoc Events</span>
<span class="section-count" id="deauth-count"></span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>Time</th><th>Node</th><th>Type</th><th>Src</th>
<th>Dst</th><th>BSSID</th><th>Reason</th><th>RSSI</th>
</tr></thead>
<tbody id="deauth-tbody"></tbody>
</table>
<div class="empty" id="deauth-empty" style="display:none">No deauth events yet. Nodes need to be reflashed with deauth detection enabled.</div>
</div>
</div><!-- /#view-alerts -->
</div><!-- /#main -->
</div><!-- /#app -->
<script src="/static/dashboard.js"></script>
</body>
</html>