Files
esp32-cluster/coordinator/templates/dashboard.html
T
2026-04-05 11:29:59 +03:00

439 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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=Roboto:wght@400;500;700&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>
<button class="tab-btn" id="tab-analysis" onclick="showAnalysis()">Analysis</button>
<button class="tab-btn" id="tab-search" onclick="showSearch()">Search</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-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Status</div><div class="value" id="ds-status"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Total Events</div><div class="value" id="ds-total"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Unique SSIDs</div><div class="value" id="ds-ssids"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Unique BSSIDs</div><div class="value" id="ds-bssids"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Avg RSSI</div><div class="value" id="ds-rssi-avg"></div><div class="sub" id="ds-rssi-range"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">First Seen</div><div class="value value-sm" id="ds-first"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Last Seen</div><div class="value value-sm" id="ds-last"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Uptime</div><div class="value" id="ds-uptime"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">Free Heap</div><div class="value" id="ds-heap"></div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></div></div>
<div class="stat-outer"><div class="stat-dot"></div><div class="stat-card"><div class="stat-ray"></div><div class="label">AP Signal</div><div class="value" id="ds-ap-rssi"></div><div class="sub">node → router</div><div class="line topl"></div><div class="line leftl"></div><div class="line bottoml"></div><div class="line rightl"></div></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>
<!-- Top targets / attackers -->
<div class="presence-split" style="margin-top:8px">
<div>
<div class="section-header">
<span class="section-title">Most Impersonated Networks</span>
<span class="section-count" id="targets-count"></span>
<span class="presence-sub">all time · APs spoofed as deauth source</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>BSSID</th><th>SSID</th><th>Frames</th>
<th>Unique Srcs</th><th>Nodes</th><th>Last Seen</th>
</tr></thead>
<tbody id="targets-tbody"></tbody>
</table>
<div class="empty" id="targets-empty" style="display:none">No data yet.</div>
</div>
</div>
<div>
<div class="section-header">
<span class="section-title">Most Targeted Devices</span>
<span class="section-count" id="attackers-count"></span>
<span class="presence-sub">all time · actual victim devices by dst MAC</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>Dst MAC</th><th>Vendor</th><th>Frames</th>
<th>Networks Used</th><th>Nodes</th><th>Last Seen</th>
</tr></thead>
<tbody id="attackers-tbody"></tbody>
</table>
<div class="empty" id="attackers-empty" style="display:none">No data yet.</div>
</div>
</div>
</div>
<!-- Reason code breakdown -->
<div class="section-header" style="margin-top:8px">
<span class="section-title">Reason Code Breakdown</span>
<span class="section-count" id="reasons-count"></span>
<span class="presence-sub">all time · normal / suspicious / attack classification</span>
</div>
<div class="tbl-wrap">
<table>
<thead><tr>
<th>Code</th><th>Description</th><th>Class</th>
<th>Frames</th><th>Unique BSSIDs</th><th>Unique Targets</th><th>Last Seen</th>
</tr></thead>
<tbody id="reasons-tbody"></tbody>
</table>
<div class="empty" id="reasons-empty" style="display:none">No data yet.</div>
</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 -->
<!-- Analysis view -->
<div id="view-analysis" class="view">
<!-- Deauth activity heatmap -->
<div class="section-header">
<span class="section-title">Deauth Activity Heatmap</span>
<span class="section-count" id="heatmap-subtitle"></span>
<span class="presence-sub">frame count per day × hour — pattern reveals manual vs automated activity</span>
</div>
<canvas id="deauth-heatmap"></canvas>
<div class="empty" id="heatmap-empty" style="display:none">No deauth data yet.</div>
</div><!-- /#view-analysis -->
<!-- Search view -->
<div id="view-search" class="view">
<div class="search-bar">
<input type="text" id="search-input" class="search-input"
placeholder="MAC address, SSID, BSSID — full or partial..."
onkeydown="if(event.key==='Enter') doSearch()" />
<button class="search-btn" onclick="doSearch()">Search</button>
</div>
<div id="search-results">
<div class="empty" id="search-empty">Enter a value above to search across all collected data.</div>
</div>
</div>
</div><!-- /#main -->
</div><!-- /#app -->
<script src="/static/dashboard.js"></script>
</body>
</html>