chore: Bump version to v2.18.0

Bluetooth enhancements (service data inspector, appearance codes, MAC
cluster tracking, behavioral flags, IRK badges, distance estimation),
ACARS SoapySDR multi-backend support, dump1090 stale process cleanup,
GPS error state, and proximity radar/signal card UI improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-16 15:12:10 +00:00
parent 2a73318457
commit 99d52eafe7
28 changed files with 1212 additions and 169 deletions

View File

@@ -356,7 +356,9 @@ const BluetoothMode = (function() {
// Update panel elements
document.getElementById('btDetailName').textContent = device.name || formatDeviceId(device.address);
document.getElementById('btDetailAddress').textContent = device.address;
document.getElementById('btDetailAddress').textContent = isUuidAddress(device)
? 'CB: ' + device.address
: device.address;
// RSSI
const rssiEl = document.getElementById('btDetailRssi');
@@ -458,8 +460,98 @@ const BluetoothMode = (function() {
? new Date(device.last_seen).toLocaleTimeString()
: '--';
// New stat cells
document.getElementById('btDetailTxPower').textContent = device.tx_power != null
? device.tx_power + ' dBm' : '--';
document.getElementById('btDetailSeenRate').textContent = device.seen_rate != null
? device.seen_rate.toFixed(1) + '/min' : '--';
// Stability from variance
const stabilityEl = document.getElementById('btDetailStability');
if (device.rssi_variance != null) {
let stabLabel, stabColor;
if (device.rssi_variance <= 5) { stabLabel = 'Stable'; stabColor = '#22c55e'; }
else if (device.rssi_variance <= 25) { stabLabel = 'Moderate'; stabColor = '#eab308'; }
else { stabLabel = 'Unstable'; stabColor = '#ef4444'; }
stabilityEl.textContent = stabLabel;
stabilityEl.style.color = stabColor;
} else {
stabilityEl.textContent = '--';
stabilityEl.style.color = '';
}
// Distance with confidence
const distEl = document.getElementById('btDetailDistance');
if (device.estimated_distance_m != null) {
const confPct = Math.round((device.distance_confidence || 0) * 100);
distEl.textContent = device.estimated_distance_m.toFixed(1) + 'm ±' + confPct + '%';
} else {
distEl.textContent = '--';
}
// Appearance badge
if (device.appearance_name) {
badgesHtml += '<span class="bt-detail-badge flag">' + escapeHtml(device.appearance_name) + '</span>';
badgesEl.innerHTML = badgesHtml;
}
// MAC cluster indicator
const macClusterEl = document.getElementById('btDetailMacCluster');
if (macClusterEl) {
if (device.mac_cluster_count > 1) {
macClusterEl.textContent = device.mac_cluster_count + ' MACs';
macClusterEl.style.display = '';
} else {
macClusterEl.style.display = 'none';
}
}
// Service data inspector
const inspectorEl = document.getElementById('btDetailServiceInspector');
const inspectorContent = document.getElementById('btInspectorContent');
if (inspectorEl && inspectorContent) {
const hasData = device.manufacturer_bytes || device.appearance != null ||
(device.service_data && Object.keys(device.service_data).length > 0);
if (hasData) {
inspectorEl.style.display = '';
let inspHtml = '';
if (device.appearance != null) {
const name = device.appearance_name || '';
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">Appearance</span><span class="bt-inspector-value">0x' + device.appearance.toString(16).toUpperCase().padStart(4, '0') + (name ? ' (' + escapeHtml(name) + ')' : '') + '</span></div>';
}
if (device.manufacturer_bytes) {
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">Mfr Data</span><span class="bt-inspector-value">' + escapeHtml(device.manufacturer_bytes) + '</span></div>';
}
if (device.service_data) {
Object.entries(device.service_data).forEach(([uuid, hex]) => {
inspHtml += '<div class="bt-inspector-row"><span class="bt-inspector-label">' + escapeHtml(uuid) + '</span><span class="bt-inspector-value">' + escapeHtml(hex) + '</span></div>';
});
}
inspectorContent.innerHTML = inspHtml;
} else {
inspectorEl.style.display = 'none';
}
}
updateWatchlistButton(device);
// IRK
const irkContainer = document.getElementById('btDetailIrk');
if (irkContainer) {
if (device.has_irk) {
irkContainer.style.display = 'block';
const irkVal = document.getElementById('btDetailIrkValue');
if (irkVal) {
const label = device.irk_source_name
? device.irk_source_name + ' — ' + device.irk_hex
: device.irk_hex;
irkVal.textContent = label;
}
} else {
irkContainer.style.display = 'none';
}
}
// Services
const servicesContainer = document.getElementById('btDetailServices');
const servicesList = document.getElementById('btDetailServicesList');
@@ -600,9 +692,25 @@ const BluetoothMode = (function() {
if (parts.length === 6) {
return parts[0] + ':' + parts[1] + ':...:' + parts[4] + ':' + parts[5];
}
// CoreBluetooth UUID format (8-4-4-4-12)
if (/^[0-9A-F]{8}-[0-9A-F]{4}-/i.test(address)) {
return address.substring(0, 8) + '...';
}
return address;
}
function isUuidAddress(device) {
return device.address_type === 'uuid';
}
function formatAddress(device) {
if (!device || !device.address) return '--';
if (isUuidAddress(device)) {
return device.address.substring(0, 8) + '-...' + device.address.slice(-4);
}
return device.address;
}
/**
* Check system capabilities
*/
@@ -660,6 +768,12 @@ const BluetoothMode = (function() {
hideCapabilityWarning();
}
// Show/hide Ubertooth option based on capabilities
const ubertoothOption = document.getElementById('btScanModeUbertooth');
if (ubertoothOption) {
ubertoothOption.style.display = data.has_ubertooth ? '' : 'none';
}
if (scanModeSelect && data.preferred_backend) {
const option = scanModeSelect.querySelector(`option[value="${data.preferred_backend}"]`);
if (option) option.selected = true;
@@ -1085,7 +1199,7 @@ const BluetoothMode = (function() {
'</div>' +
'</div>' +
'<div style="display:flex;justify-content:space-between;margin-top:3px;">' +
'<span style="font-size:9px;color:#888;font-family:monospace;">' + t.address + '</span>' +
'<span style="font-size:9px;color:#888;font-family:monospace;">' + (t.address_type === 'uuid' ? formatAddress(t) : t.address) + '</span>' +
'<span style="font-size:9px;color:#666;">Seen ' + (t.seen_count || 0) + 'x</span>' +
'</div>' +
evidenceHtml +
@@ -1142,7 +1256,7 @@ const BluetoothMode = (function() {
const displayName = device.name || formatDeviceId(device.address);
const name = escapeHtml(displayName);
const addr = escapeHtml(device.address || 'Unknown');
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
const seenCount = device.seen_count || 0;
const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'");
@@ -1167,6 +1281,12 @@ const BluetoothMode = (function() {
trackerBadge = '<span class="bt-tracker-badge" style="background:' + confBg + ';color:' + confColor + ';font-size:9px;padding:1px 4px;border-radius:3px;margin-left:4px;font-weight:600;">' + typeLabel + '</span>';
}
// IRK badge - show if paired IRK is available
let irkBadge = '';
if (device.has_irk) {
irkBadge = '<span class="bt-irk-badge">IRK</span>';
}
// Risk badge - show if risk score is significant
let riskBadge = '';
if (riskScore >= 0.3) {
@@ -1184,9 +1304,36 @@ const BluetoothMode = (function() {
statusDot = '<span class="bt-status-dot known"></span>';
}
// Distance display
const distM = device.estimated_distance_m;
let distStr = '';
if (distM != null) {
distStr = '~' + distM.toFixed(1) + 'm';
}
// Behavioral flag badges
const hFlags = device.heuristic_flags || [];
let flagBadges = '';
if (device.is_persistent || hFlags.includes('persistent')) {
flagBadges += '<span class="bt-flag-badge persistent">PERSIST</span>';
}
if (device.is_beacon_like || hFlags.includes('beacon_like')) {
flagBadges += '<span class="bt-flag-badge beacon-like">BEACON</span>';
}
if (device.is_strong_stable || hFlags.includes('strong_stable')) {
flagBadges += '<span class="bt-flag-badge strong-stable">STABLE</span>';
}
// MAC cluster badge
let clusterBadge = '';
if (device.mac_cluster_count > 1) {
clusterBadge = '<span class="bt-mac-cluster-badge">' + device.mac_cluster_count + ' MACs</span>';
}
// Build secondary info line
let secondaryParts = [addr];
if (mfr) secondaryParts.push(mfr);
if (distStr) secondaryParts.push(distStr);
secondaryParts.push('Seen ' + seenCount + '×');
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
// Add agent name if not Local
@@ -1205,7 +1352,10 @@ const BluetoothMode = (function() {
protoBadge +
'<span class="bt-device-name">' + name + '</span>' +
trackerBadge +
irkBadge +
riskBadge +
flagBadges +
clusterBadge +
'</div>' +
'<div class="bt-row-right">' +
'<div class="bt-rssi-container">' +
@@ -1300,6 +1450,18 @@ const BluetoothMode = (function() {
}
}
/**
* Toggle the service data inspector panel
*/
function toggleServiceInspector() {
const content = document.getElementById('btInspectorContent');
const arrow = document.getElementById('btInspectorArrow');
if (!content) return;
const open = content.style.display === 'none';
content.style.display = open ? '' : 'none';
if (arrow) arrow.classList.toggle('open', open);
}
// ==========================================================================
// Agent Handling
// ==========================================================================
@@ -1425,9 +1587,15 @@ const BluetoothMode = (function() {
BtLocate.handoff({
device_id: device.device_id,
mac_address: device.address,
address_type: device.address_type || null,
irk_hex: device.irk_hex || null,
known_name: device.name || null,
known_manufacturer: device.manufacturer_name || null,
last_known_rssi: device.rssi_current
last_known_rssi: device.rssi_current,
tx_power: device.tx_power || null,
appearance_name: device.appearance_name || null,
fingerprint_id: device.fingerprint_id || null,
mac_cluster_count: device.mac_cluster_count || 0
});
}
}
@@ -1447,6 +1615,7 @@ const BluetoothMode = (function() {
toggleWatchlist,
locateDevice,
locateById,
toggleServiceInspector,
// Agent handling
handleAgentChange,

View File

@@ -322,7 +322,8 @@ const BtLocate = (function() {
const t = data.target;
const name = t.known_name || t.name_pattern || '';
const addr = t.mac_address || t.device_id || '';
targetEl.textContent = name ? (name + (addr ? ' (' + addr.substring(0, 8) + '...)' : '')) : addr || '--';
const addrDisplay = formatAddr(addr);
targetEl.textContent = name ? (name + (addrDisplay ? ' (' + addrDisplay + ')' : '')) : addrDisplay || '--';
}
// Environment info
@@ -602,6 +603,16 @@ const BtLocate = (function() {
}).catch(() => {});
}
function isUuid(addr) {
return addr && /^[0-9A-F]{8}-[0-9A-F]{4}-/i.test(addr);
}
function formatAddr(addr) {
if (!addr) return '';
if (isUuid(addr)) return addr.substring(0, 8) + '-...' + addr.slice(-4);
return addr;
}
function handoff(deviceInfo) {
console.log('[BtLocate] Handoff received:', deviceInfo);
handoffData = deviceInfo;
@@ -617,15 +628,21 @@ const BtLocate = (function() {
const nameEl = document.getElementById('btLocateHandoffName');
const metaEl = document.getElementById('btLocateHandoffMeta');
if (card) card.style.display = '';
if (nameEl) nameEl.textContent = deviceInfo.known_name || deviceInfo.mac_address || 'Unknown';
if (nameEl) nameEl.textContent = deviceInfo.known_name || formatAddr(deviceInfo.mac_address) || 'Unknown';
if (metaEl) {
const parts = [];
if (deviceInfo.mac_address) parts.push(deviceInfo.mac_address);
if (deviceInfo.mac_address) parts.push(formatAddr(deviceInfo.mac_address));
if (deviceInfo.known_manufacturer) parts.push(deviceInfo.known_manufacturer);
if (deviceInfo.last_known_rssi != null) parts.push(deviceInfo.last_known_rssi + ' dBm');
metaEl.textContent = parts.join(' \u00b7 ');
}
// Auto-fill IRK if available from scanner
if (deviceInfo.irk_hex) {
const irkInput = document.getElementById('btLocateIrk');
if (irkInput) irkInput.value = deviceInfo.irk_hex;
}
// Switch to bt_locate mode
if (typeof switchMode === 'function') {
switchMode('bt_locate');

View File

@@ -5,7 +5,6 @@
*/
const GPS = (function() {
let eventSource = null;
let connected = false;
let lastPosition = null;
let lastSky = null;
@@ -26,6 +25,7 @@ const GPS = (function() {
}
function connect() {
updateConnectionUI(false, false, 'connecting');
fetch('/gps/auto-connect', { method: 'POST' })
.then(r => r.json())
.then(data => {
@@ -40,23 +40,24 @@ const GPS = (function() {
lastSky = data.sky;
updateSkyUI(data.sky);
}
startStream();
subscribeToStream();
// Ensure the global GPS stream is running
if (typeof startGpsStream === 'function' && !gpsEventSource) {
startGpsStream();
}
} else {
connected = false;
updateConnectionUI(false);
updateConnectionUI(false, false, 'error', data.message || 'gpsd not available');
}
})
.catch(() => {
connected = false;
updateConnectionUI(false);
updateConnectionUI(false, false, 'error', 'Connection failed — is the server running?');
});
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
unsubscribeFromStream();
fetch('/gps/stop', { method: 'POST' })
.then(() => {
connected = false;
@@ -64,36 +65,36 @@ const GPS = (function() {
});
}
function startStream() {
if (eventSource) {
eventSource.close();
function onGpsStreamData(data) {
if (!connected) return;
if (data.type === 'position') {
lastPosition = data;
updatePositionUI(data);
updateConnectionUI(true, true);
} else if (data.type === 'sky') {
lastSky = data;
updateSkyUI(data);
}
}
function subscribeToStream() {
// Subscribe to the global GPS stream instead of opening a separate SSE connection
if (typeof addGpsStreamSubscriber === 'function') {
addGpsStreamSubscriber(onGpsStreamData);
}
}
function unsubscribeFromStream() {
if (typeof removeGpsStreamSubscriber === 'function') {
removeGpsStreamSubscriber(onGpsStreamData);
}
eventSource = new EventSource('/gps/stream');
eventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
if (data.type === 'position') {
lastPosition = data;
updatePositionUI(data);
updateConnectionUI(true, true);
} else if (data.type === 'sky') {
lastSky = data;
updateSkyUI(data);
}
} catch (err) {
// ignore parse errors
}
};
eventSource.onerror = function() {
// Reconnect handled by browser automatically
};
}
// ========================
// UI Updates
// ========================
function updateConnectionUI(isConnected, hasFix) {
function updateConnectionUI(isConnected, hasFix, state, message) {
const dot = document.getElementById('gpsStatusDot');
const text = document.getElementById('gpsStatusText');
const connectBtn = document.getElementById('gpsConnectBtn');
@@ -102,15 +103,22 @@ const GPS = (function() {
if (dot) {
dot.className = 'gps-status-dot';
if (isConnected && hasFix) dot.classList.add('connected');
if (state === 'connecting') dot.classList.add('waiting');
else if (state === 'error') dot.classList.add('error');
else if (isConnected && hasFix) dot.classList.add('connected');
else if (isConnected) dot.classList.add('waiting');
}
if (text) {
if (isConnected && hasFix) text.textContent = 'Connected (Fix)';
if (state === 'connecting') text.textContent = 'Connecting...';
else if (state === 'error') text.textContent = message || 'Connection failed';
else if (isConnected && hasFix) text.textContent = 'Connected (Fix)';
else if (isConnected) text.textContent = 'Connected (No Fix)';
else text.textContent = 'Disconnected';
}
if (connectBtn) connectBtn.style.display = isConnected ? 'none' : '';
if (connectBtn) {
connectBtn.style.display = isConnected ? 'none' : '';
connectBtn.disabled = state === 'connecting';
}
if (disconnectBtn) disconnectBtn.style.display = isConnected ? '' : 'none';
if (devicePath) devicePath.textContent = isConnected ? 'gpsd://localhost:2947' : '';
}
@@ -386,10 +394,7 @@ const GPS = (function() {
// ========================
function destroy() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
unsubscribeFromStream();
}
return {