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:
58
static/js/main.js
Normal file
58
static/js/main.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/* ESP32-Web Dashboard — tab switching & shared utilities */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTabs();
|
||||
});
|
||||
|
||||
function initTabs() {
|
||||
const tabs = document.querySelectorAll('.tab-nav a');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (tab.classList.contains('active')) return;
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
// htmx handles the AJAX load via hx-get on the element
|
||||
});
|
||||
});
|
||||
|
||||
// Load first tab on page load
|
||||
const first = document.querySelector('.tab-nav a');
|
||||
if (first) {
|
||||
first.click();
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared D3 tooltip */
|
||||
function createTooltip() {
|
||||
let tip = document.querySelector('.d3-tooltip');
|
||||
if (!tip) {
|
||||
tip = document.createElement('div');
|
||||
tip.className = 'd3-tooltip';
|
||||
tip.style.display = 'none';
|
||||
document.body.appendChild(tip);
|
||||
}
|
||||
return {
|
||||
show(html, event) {
|
||||
tip.innerHTML = html;
|
||||
tip.style.display = 'block';
|
||||
tip.style.left = (event.pageX + 12) + 'px';
|
||||
tip.style.top = (event.pageY - 12) + 'px';
|
||||
},
|
||||
move(event) {
|
||||
tip.style.left = (event.pageX + 12) + 'px';
|
||||
tip.style.top = (event.pageY - 12) + 'px';
|
||||
},
|
||||
hide() {
|
||||
tip.style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* Responsive SVG dimensions from container */
|
||||
function getVizSize(container) {
|
||||
return {
|
||||
width: Math.max(container.clientWidth || container.parentElement.clientWidth, 300),
|
||||
height: Math.max(container.clientHeight, 400)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user