feat: v0.1.4 — device intelligence dashboard

Add tabbed dashboard at /dashboard/ with three D3.js visualizations:
- Vendor treemap (devices grouped by type and vendor)
- SSID social graph (force-directed, shared probed SSIDs as edges)
- Fingerprint clusters (packed circles by device behavior)

Intelligence API endpoints at /api/v1/intelligence/ with param
validation. Dashboard built on htmx + Pico CSS dark theme + D3 v7,
all vendored locally (make vendor). 13 new tests (59 total).
This commit is contained in:
user
2026-02-06 18:59:53 +01:00
parent c1f580ba16
commit dfbd2a2196
27 changed files with 1177 additions and 15 deletions

30
templates/base.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}ESP32-Web{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/vendor/pico.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<nav class="container-fluid">
<ul>
<li><strong>ESP32-Web</strong></li>
</ul>
<ul>
<li><a href="{{ url_for('dashboard.index') }}">Dashboard</a></li>
<li><a href="/docs">API Docs</a></li>
<li><a href="/health">Health</a></li>
</ul>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='js/vendor/htmx.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/vendor/d3.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}Device Intelligence — ESP32-Web{% endblock %}
{% block content %}
<hgroup>
<h2>Device Intelligence</h2>
<p>{{ total_devices }} devices tracked across {{ total_sensors }} sensors</p>
</hgroup>
<nav class="tab-nav">
<a href="#" hx-get="{{ url_for('dashboard.tab_vendor_treemap') }}" hx-target="#tab-content">Vendor Treemap</a>
<a href="#" hx-get="{{ url_for('dashboard.tab_ssid_graph') }}" hx-target="#tab-content">SSID Graph</a>
<a href="#" hx-get="{{ url_for('dashboard.tab_fingerprint_clusters') }}" hx-target="#tab-content">Fingerprint Clusters</a>
</nav>
<div id="tab-content">
<div class="viz-loading" aria-busy="true">Loading...</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
<div class="viz-container" id="fingerprint-clusters"></div>
<script src="{{ url_for('static', filename='js/viz/fingerprint_clusters.js') }}"></script>
<script>renderFingerprintClusters('#fingerprint-clusters', '{{ url_for("api.fingerprint_clusters") }}');</script>

View File

@@ -0,0 +1,3 @@
<div class="viz-container" id="ssid-graph"></div>
<script src="{{ url_for('static', filename='js/viz/ssid_graph.js') }}"></script>
<script>renderSsidGraph('#ssid-graph', '{{ url_for("api.ssid_graph") }}');</script>

View File

@@ -0,0 +1,3 @@
<div class="viz-container" id="vendor-treemap"></div>
<script src="{{ url_for('static', filename='js/viz/vendor_treemap.js') }}"></script>
<script>renderVendorTreemap('#vendor-treemap', '{{ url_for("api.vendor_treemap") }}');</script>