feat: Add BT Locate and GPS modes with IRK auto-detection

New modes:
- BT Locate: SAR Bluetooth device location with GPS-tagged signal trail,
  RSSI-based proximity bands, audio alerts, and IRK auto-extraction from
  paired devices (macOS plist / Linux BlueZ)
- GPS: Real-time position tracking with live map, speed, heading, altitude,
  satellite info, and track recording via gpsd

Bug fixes:
- Fix ABBA deadlock between session lock and aggregator lock in BT Locate
- Fix bleak scan lifecycle tracking in BluetoothScanner (is_scanning property
  now cross-checks backend state)
- Fix map tile persistence when switching modes
- Use 15s max_age window for fresh detections in BT Locate poll loop

Documentation:
- Update README, FEATURES.md, USAGE.md, and GitHub Pages with new modes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-15 21:59:38 +00:00
parent c60769f795
commit d8d08a8b1e
26 changed files with 4481 additions and 510 deletions

View File

@@ -366,10 +366,10 @@ const BluetoothMode = (function() {
// Badges
const badgesEl = document.getElementById('btDetailBadges');
let badgesHtml = `<span class="bt-detail-badge ${protocol}">${protocol.toUpperCase()}</span>`;
badgesHtml += `<span class="bt-detail-badge ${device.in_baseline ? 'baseline' : 'new'}">${device.in_baseline ? '✓ KNOWN' : '● NEW'}</span>`;
if (device.seen_before) {
badgesHtml += `<span class="bt-detail-badge flag">SEEN BEFORE</span>`;
}
badgesHtml += `<span class="bt-detail-badge ${device.in_baseline ? 'baseline' : 'new'}">${device.in_baseline ? '✓ KNOWN' : '● NEW'}</span>`;
if (device.seen_before) {
badgesHtml += `<span class="bt-detail-badge flag">SEEN BEFORE</span>`;
}
// Tracker badge
if (device.is_tracker) {
@@ -451,14 +451,14 @@ const BluetoothMode = (function() {
? minMax[0] + '/' + minMax[1]
: '--';
document.getElementById('btDetailFirstSeen').textContent = device.first_seen
? new Date(device.first_seen).toLocaleTimeString()
: '--';
document.getElementById('btDetailLastSeen').textContent = device.last_seen
? new Date(device.last_seen).toLocaleTimeString()
: '--';
updateWatchlistButton(device);
document.getElementById('btDetailFirstSeen').textContent = device.first_seen
? new Date(device.first_seen).toLocaleTimeString()
: '--';
document.getElementById('btDetailLastSeen').textContent = device.last_seen
? new Date(device.last_seen).toLocaleTimeString()
: '--';
updateWatchlistButton(device);
// Services
const servicesContainer = document.getElementById('btDetailServices');
@@ -470,29 +470,29 @@ const BluetoothMode = (function() {
servicesContainer.style.display = 'none';
}
// Show content, hide placeholder
placeholder.style.display = 'none';
content.style.display = 'block';
// Show content, hide placeholder
placeholder.style.display = 'none';
content.style.display = 'block';
// Highlight selected device in list
highlightSelectedDevice(deviceId);
}
/**
* Update watchlist button state
*/
function updateWatchlistButton(device) {
const btn = document.getElementById('btDetailWatchBtn');
if (!btn) return;
if (typeof AlertCenter === 'undefined') {
btn.style.display = 'none';
return;
}
btn.style.display = '';
const watchlisted = AlertCenter.isWatchlisted(device.address);
btn.textContent = watchlisted ? 'Watching' : 'Watchlist';
btn.classList.toggle('active', watchlisted);
}
}
/**
* Update watchlist button state
*/
function updateWatchlistButton(device) {
const btn = document.getElementById('btDetailWatchBtn');
if (!btn) return;
if (typeof AlertCenter === 'undefined') {
btn.style.display = 'none';
return;
}
btn.style.display = '';
const watchlisted = AlertCenter.isWatchlisted(device.address);
btn.textContent = watchlisted ? 'Watching' : 'Watchlist';
btn.classList.toggle('active', watchlisted);
}
/**
* Clear device selection
@@ -546,43 +546,43 @@ const BluetoothMode = (function() {
/**
* Copy selected device address to clipboard
*/
function copyAddress() {
if (!selectedDeviceId) return;
const device = devices.get(selectedDeviceId);
if (!device) return;
function copyAddress() {
if (!selectedDeviceId) return;
const device = devices.get(selectedDeviceId);
if (!device) return;
navigator.clipboard.writeText(device.address).then(() => {
const btn = document.getElementById('btDetailCopyBtn');
if (btn) {
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#22c55e';
navigator.clipboard.writeText(device.address).then(() => {
const btn = document.getElementById('btDetailCopyBtn');
if (btn) {
const originalText = btn.textContent;
btn.textContent = 'Copied!';
btn.style.background = '#22c55e';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
}, 1500);
}
});
}
/**
* Toggle Bluetooth watchlist for selected device
*/
function toggleWatchlist() {
if (!selectedDeviceId) return;
const device = devices.get(selectedDeviceId);
if (!device || typeof AlertCenter === 'undefined') return;
if (AlertCenter.isWatchlisted(device.address)) {
AlertCenter.removeBluetoothWatchlist(device.address);
showInfo('Removed from watchlist');
} else {
AlertCenter.addBluetoothWatchlist(device.address, device.name || device.address);
showInfo('Added to watchlist');
}
setTimeout(() => updateWatchlistButton(device), 200);
}
});
}
/**
* Toggle Bluetooth watchlist for selected device
*/
function toggleWatchlist() {
if (!selectedDeviceId) return;
const device = devices.get(selectedDeviceId);
if (!device || typeof AlertCenter === 'undefined') return;
if (AlertCenter.isWatchlisted(device.address)) {
AlertCenter.removeBluetoothWatchlist(device.address);
showInfo('Removed from watchlist');
} else {
AlertCenter.addBluetoothWatchlist(device.address, device.name || device.address);
showInfo('Added to watchlist');
}
setTimeout(() => updateWatchlistButton(device), 200);
}
/**
* Select a device - opens modal with details
@@ -1130,11 +1130,11 @@ const BluetoothMode = (function() {
const isNew = !inBaseline;
const hasName = !!device.name;
const isTracker = device.is_tracker === true;
const trackerType = device.tracker_type;
const trackerConfidence = device.tracker_confidence;
const riskScore = device.risk_score || 0;
const agentName = device._agent || 'Local';
const seenBefore = device.seen_before === true;
const trackerType = device.tracker_type;
const trackerConfidence = device.tracker_confidence;
const riskScore = device.risk_score || 0;
const agentName = device._agent || 'Local';
const seenBefore = device.seen_before === true;
// Calculate RSSI bar width (0-100%)
// RSSI typically ranges from -100 (weak) to -30 (very strong)
@@ -1186,9 +1186,9 @@ const BluetoothMode = (function() {
// Build secondary info line
let secondaryParts = [addr];
if (mfr) secondaryParts.push(mfr);
secondaryParts.push('Seen ' + seenCount + '×');
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
if (mfr) secondaryParts.push(mfr);
secondaryParts.push('Seen ' + seenCount + '×');
if (seenBefore) secondaryParts.push('<span class="bt-history-badge">SEEN BEFORE</span>');
// Add agent name if not Local
if (agentName !== 'Local') {
secondaryParts.push('<span class="agent-badge agent-remote" style="font-size:8px;padding:1px 4px;">' + escapeHtml(agentName) + '</span>');
@@ -1216,6 +1216,11 @@ const BluetoothMode = (function() {
'</div>' +
'</div>' +
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
'<div class="bt-row-actions">' +
'<button class="bt-locate-btn" data-locate-id="' + escapeHtml(device.device_id) + '" onclick="event.stopPropagation(); BluetoothMode.locateById(this.dataset.locateId)">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
'Locate</button>' +
'</div>' +
'</div>';
}
@@ -1391,6 +1396,42 @@ const BluetoothMode = (function() {
updateRadar();
}
/**
* Hand off a device to BT Locate mode by device_id lookup.
*/
function locateById(deviceId) {
console.log('[BT] locateById called with:', deviceId);
const device = devices.get(deviceId);
if (!device) {
console.warn('[BT] Device not found in map for id:', deviceId);
return;
}
doLocateHandoff(device);
}
/**
* Hand off the currently selected device to BT Locate mode.
*/
function locateDevice() {
if (!selectedDeviceId) return;
const device = devices.get(selectedDeviceId);
if (!device) return;
doLocateHandoff(device);
}
function doLocateHandoff(device) {
console.log('[BT] doLocateHandoff, BtLocate defined:', typeof BtLocate !== 'undefined');
if (typeof BtLocate !== 'undefined') {
BtLocate.handoff({
device_id: device.device_id,
mac_address: device.address,
known_name: device.name || null,
known_manufacturer: device.manufacturer_name || null,
last_known_rssi: device.rssi_current
});
}
}
// Public API
return {
init,
@@ -1400,10 +1441,12 @@ const BluetoothMode = (function() {
setBaseline,
clearBaseline,
exportData,
selectDevice,
clearSelection,
copyAddress,
toggleWatchlist,
selectDevice,
clearSelection,
copyAddress,
toggleWatchlist,
locateDevice,
locateById,
// Agent handling
handleAgentChange,