mirror of
https://github.com/smittix/intercept.git
synced 2026-05-02 10:39:58 -07:00
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:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user