Add BLE layer: firmware, coordinator support, dashboard tab
This commit is contained in:
@@ -607,6 +607,33 @@ td.disassoc-type { color: var(--orange); }
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── BLE ── */
|
||||
.ble-type-apple { color: #a8c8ff; }
|
||||
.ble-type-samsung { color: #7eb3ff; }
|
||||
.ble-type-other { color: var(--text-muted); }
|
||||
.ble-addr-public { color: var(--green); font-size: 10px; }
|
||||
.ble-addr-random { color: var(--text-muted); font-size: 10px; }
|
||||
|
||||
.ble-breakdown-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.ble-breakdown-chip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.ble-breakdown-chip strong {
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ── */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
|
||||
@@ -1190,6 +1190,9 @@ function setView(view) {
|
||||
document.getElementById('tab-sessions').classList.toggle('active', view === 'sessions');
|
||||
document.getElementById('tab-analysis').classList.toggle('active', view === 'analysis');
|
||||
document.getElementById('tab-search').classList.toggle('active', view === 'search');
|
||||
document.getElementById('view-ble').classList.toggle('active', view === 'ble');
|
||||
document.getElementById('tab-ble').classList.toggle('active', view === 'ble');
|
||||
if (view !== 'ble' && _blePoll) { clearInterval(_blePoll); _blePoll = null; }
|
||||
}
|
||||
|
||||
function showFeed() {
|
||||
@@ -1280,6 +1283,7 @@ function startSSE() {
|
||||
else if (currentView === 'presence') await fetchPresence();
|
||||
else if (currentView === 'alerts') await fetchAlerts();
|
||||
else if (currentView === 'analysis') await showAnalysis();
|
||||
else if (currentView === 'ble') await fetchBle();
|
||||
|
||||
if (!_nodesFetching) {
|
||||
_nodesFetching = true;
|
||||
@@ -1306,6 +1310,96 @@ function startSSE() {
|
||||
};
|
||||
}
|
||||
|
||||
// ── BLE ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
let bleData = { devices: [], feed: [], breakdown: [] };
|
||||
let _blePoll = null;
|
||||
|
||||
function showBle() {
|
||||
setView('ble');
|
||||
fetchBle();
|
||||
if (_blePoll) clearInterval(_blePoll);
|
||||
_blePoll = setInterval(fetchBle, 5000);
|
||||
}
|
||||
|
||||
async function fetchBle() {
|
||||
try {
|
||||
bleData = await fetch('/api/ble').then(r => r.json());
|
||||
renderBle();
|
||||
} catch (e) { /* network hiccup */ }
|
||||
}
|
||||
|
||||
function bleTypeClass(type) {
|
||||
if (!type) return 'ble-type-other';
|
||||
if (type.startsWith('Apple') || type === 'iBeacon' || type.includes('AirPods') ||
|
||||
type.includes('FindMy') || type.includes('AirTag') || type.includes('HomeKit') ||
|
||||
type.includes('AirDrop') || type.includes('Proximity'))
|
||||
return 'ble-type-apple';
|
||||
if (type.startsWith('Samsung')) return 'ble-type-samsung';
|
||||
return 'ble-type-other';
|
||||
}
|
||||
|
||||
function renderBle() {
|
||||
// Breakdown chips
|
||||
const bd = bleData.breakdown || [];
|
||||
const bdEl = document.getElementById('ble-breakdown');
|
||||
document.getElementById('ble-breakdown-count').textContent = bd.length ? `${bd.length} manufacturers` : '';
|
||||
bdEl.innerHTML = bd.map(b => {
|
||||
const label = b.vendor && b.vendor !== 'Unknown' && b.vendor !== 'No mfr data'
|
||||
? b.vendor
|
||||
: b.mfr_id != null ? `0x${b.mfr_id.toString(16).padStart(4,'0').toUpperCase()}` : 'No mfr data';
|
||||
return `<div class="ble-breakdown-chip"><strong>${b.unique_devices}</strong>${label}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Devices table
|
||||
const devs = bleData.devices || [];
|
||||
const devsEl = document.getElementById('ble-devices-body');
|
||||
const emptyEl = document.getElementById('ble-devices-empty');
|
||||
const tableEl = document.getElementById('ble-devices-table');
|
||||
document.getElementById('ble-devices-count').textContent = devs.length ? `${devs.length} unique` : '';
|
||||
if (!devs.length) {
|
||||
emptyEl.style.display = '';
|
||||
tableEl.style.display = 'none';
|
||||
} else {
|
||||
emptyEl.style.display = 'none';
|
||||
tableEl.style.display = '';
|
||||
devsEl.innerHTML = devs.map(d => {
|
||||
const typeClass = bleTypeClass(d.type);
|
||||
const addrClass = d.addr_type === 'public' ? 'ble-addr-public' : 'ble-addr-random';
|
||||
const name = d.name || '<span class="muted">—</span>';
|
||||
const vendor = d.vendor && d.vendor !== 'Unknown' ? d.vendor : '<span class="muted">—</span>';
|
||||
return `<tr>
|
||||
<td class="${typeClass}">${fmt(d.type)}</td>
|
||||
<td class="mono">${fmt(d.mac)}</td>
|
||||
<td>${name}</td>
|
||||
<td>${vendor}</td>
|
||||
<td class="${addrClass}">${d.addr_type || '—'}</td>
|
||||
<td class="${rssiClass(d.best_rssi)}">${fmt(d.best_rssi)} dBm</td>
|
||||
<td>${fmt(d.times_seen)}</td>
|
||||
<td>${fmt(d.node_count)}</td>
|
||||
<td class="muted">${shortTime(d.last_seen)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Feed
|
||||
const feed = bleData.feed || [];
|
||||
document.getElementById('ble-feed-count').textContent = feed.length ? `last ${feed.length}` : '';
|
||||
document.getElementById('ble-feed-body').innerHTML = feed.map(f => {
|
||||
const typeClass = bleTypeClass(f.type);
|
||||
const addrClass = f.addr_type === 'public' ? 'ble-addr-public' : 'ble-addr-random';
|
||||
return `<tr>
|
||||
<td class="muted" style="font-size:11px">${shortTime(f.received_at)}</td>
|
||||
<td class="muted">${fmt(f.node_id)}</td>
|
||||
<td class="${typeClass}">${fmt(f.type)}</td>
|
||||
<td class="mono">${fmt(f.mac)}</td>
|
||||
<td>${f.name || '<span class="muted">—</span>'}</td>
|
||||
<td class="${rssiClass(f.rssi)}">${fmt(f.rssi)} dBm</td>
|
||||
<td class="${addrClass}">${f.addr_type || '—'}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Boot ──────────────────────────────────────────────────────────────────────
|
||||
initNetworkSort();
|
||||
init().then(startSSE);
|
||||
|
||||
Reference in New Issue
Block a user