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).
59 lines
1.5 KiB
JavaScript
59 lines
1.5 KiB
JavaScript
/* 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)
|
|
};
|
|
}
|