`;
- modal.style.display = 'flex';
- }
-
- /**
- * Close device details modal
- */
- function closeModal() {
- const modal = document.getElementById('btDeviceModal');
- if (modal) modal.style.display = 'none';
- selectedDeviceId = null;
+ // Highlight selected card
+ const cards = deviceContainer?.querySelectorAll('[data-bt-device-id]');
+ cards?.forEach(card => {
+ if (card.dataset.btDeviceId === deviceId) {
+ card.style.borderColor = '#00d4ff';
+ card.style.boxShadow = '0 0 0 1px rgba(0, 212, 255, 0.3)';
+ } else {
+ card.style.borderColor = '#444';
+ card.style.boxShadow = 'none';
+ }
+ });
}
/**
@@ -237,7 +391,6 @@ const BluetoothMode = (function() {
*/
function formatDeviceId(address) {
if (!address) return 'Unknown Device';
- // Return shortened format: first 2 and last 2 octets
const parts = address.split(':');
if (parts.length === 6) {
return parts[0] + ':' + parts[1] + ':...:' + parts[4] + ':' + parts[5];
@@ -258,7 +411,6 @@ const BluetoothMode = (function() {
return;
}
- // Update adapter select
if (adapterSelect && data.adapters && data.adapters.length > 0) {
adapterSelect.innerHTML = data.adapters.map(a => {
const status = a.powered ? 'UP' : 'DOWN';
@@ -268,14 +420,12 @@ const BluetoothMode = (function() {
adapterSelect.innerHTML = '
';
}
- // Show any issues
if (data.issues && data.issues.length > 0) {
showCapabilityWarning(data.issues);
} else {
hideCapabilityWarning();
}
- // Update scan mode based on preferred backend
if (scanModeSelect && data.preferred_backend) {
const option = scanModeSelect.querySelector(`option[value="${data.preferred_backend}"]`);
if (option) option.selected = true;
@@ -287,12 +437,8 @@ const BluetoothMode = (function() {
}
}
- /**
- * Show capability warning
- */
function showCapabilityWarning(issues) {
if (!capabilityStatusEl) return;
-
capabilityStatusEl.style.display = 'block';
capabilityStatusEl.innerHTML = `
@@ -301,9 +447,6 @@ const BluetoothMode = (function() {
`;
}
- /**
- * Hide capability warning
- */
function hideCapabilityWarning() {
if (capabilityStatusEl) {
capabilityStatusEl.style.display = 'none';
@@ -311,9 +454,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Check current scan status
- */
async function checkScanStatus() {
try {
const response = await fetch('/api/bluetooth/scan/status');
@@ -324,7 +464,6 @@ const BluetoothMode = (function() {
startEventStream();
}
- // Update baseline status
if (data.baseline_count > 0) {
baselineSet = true;
baselineCount = data.baseline_count;
@@ -336,9 +475,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Start scanning
- */
async function startScan() {
const adapter = adapterSelect?.value || '';
const mode = scanModeSelect?.value || 'auto';
@@ -374,9 +510,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Stop scanning
- */
async function stopScan() {
try {
await fetch('/api/bluetooth/scan/stop', { method: 'POST' });
@@ -387,32 +520,30 @@ const BluetoothMode = (function() {
}
}
- /**
- * Set scanning state
- */
function setScanning(scanning) {
isScanning = scanning;
if (startBtn) startBtn.style.display = scanning ? 'none' : 'block';
if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none';
- // Clear container when starting scan
if (scanning && deviceContainer) {
deviceContainer.innerHTML = '';
devices.clear();
resetStats();
+
+ // Reset selected device panel
+ const selectedPanel = document.getElementById('btSelectedDevice');
+ if (selectedPanel) {
+ selectedPanel.innerHTML = '
Click a device to view details
';
+ }
}
- // Update global status if available
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (statusDot) statusDot.classList.toggle('running', scanning);
if (statusText) statusText.textContent = scanning ? 'Scanning...' : 'Idle';
}
- /**
- * Reset stats
- */
function resetStats() {
deviceStats = {
phones: 0,
@@ -427,11 +558,9 @@ const BluetoothMode = (function() {
findmy: []
};
updateVisualizationPanels();
+ drawHeatmap();
}
- /**
- * Start SSE event stream
- */
function startEventStream() {
if (eventSource) eventSource.close();
@@ -447,13 +576,11 @@ const BluetoothMode = (function() {
});
eventSource.addEventListener('scan_started', (e) => {
- const data = JSON.parse(e.data);
setScanning(true);
});
eventSource.addEventListener('scan_stopped', (e) => {
setScanning(false);
- const data = JSON.parse(e.data);
});
eventSource.onerror = () => {
@@ -461,9 +588,6 @@ const BluetoothMode = (function() {
};
}
- /**
- * Stop SSE event stream
- */
function stopEventStream() {
if (eventSource) {
eventSource.close();
@@ -471,27 +595,70 @@ const BluetoothMode = (function() {
}
}
- /**
- * Handle device update from SSE
- */
function handleDeviceUpdate(device) {
devices.set(device.device_id, device);
renderDevice(device);
updateDeviceCount();
- updateStatsFromDevice(device);
+ updateStatsFromDevices();
updateVisualizationPanels();
- updateRadar();
+ drawHeatmap();
+
+ // Update selected device panel if this device is selected
+ if (selectedDeviceId === device.device_id) {
+ selectDevice(device.device_id);
+ }
+
+ // Feed to activity timeline
+ addToTimeline(device);
}
/**
- * Update stats from device
+ * Add device event to timeline
*/
- function updateStatsFromDevice(device) {
- // Categorize by manufacturer/type
- const mfr = (device.manufacturer_name || '').toLowerCase();
- const name = (device.name || '').toLowerCase();
+ function addToTimeline(device) {
+ if (typeof addTimelineEvent === 'function') {
+ const normalized = {
+ id: device.device_id,
+ label: device.name || formatDeviceId(device.address),
+ strength: device.rssi_current ? Math.min(5, Math.max(1, Math.ceil((device.rssi_current + 100) / 20))) : 3,
+ duration: 1500,
+ type: 'bluetooth'
+ };
+ addTimelineEvent('bluetooth', normalized);
+ }
- // Reset counts and recalculate from all devices
+ // Also update our simple timeline if it exists
+ const activityContent = document.getElementById('btActivityContent');
+ if (activityContent) {
+ const time = new Date().toLocaleTimeString();
+ const existing = activityContent.querySelector('.bt-activity-list');
+
+ if (!existing) {
+ activityContent.innerHTML = '
';
+ }
+
+ const list = activityContent.querySelector('.bt-activity-list');
+ const entry = document.createElement('div');
+ entry.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 10px;';
+ entry.innerHTML = `
+
${time}
+
${escapeHtml(device.name || formatDeviceId(device.address))}
+
${device.rssi_current || '--'} dBm
+ `;
+ list.insertBefore(entry, list.firstChild);
+
+ // Keep only last 50 entries
+ while (list.children.length > 50) {
+ list.removeChild(list.lastChild);
+ }
+ }
+ }
+
+ /**
+ * Update stats from all devices
+ */
+ function updateStatsFromDevices() {
+ // Reset counts
deviceStats.phones = 0;
deviceStats.computers = 0;
deviceStats.audio = 0;
@@ -504,41 +671,84 @@ const BluetoothMode = (function() {
deviceStats.findmy = [];
devices.forEach(d => {
- const m = (d.manufacturer_name || '').toLowerCase();
- const n = (d.name || '').toLowerCase();
+ const mfr = (d.manufacturer_name || '').toLowerCase();
+ const name = (d.name || '').toLowerCase();
const rssi = d.rssi_current;
+ const flags = d.heuristic_flags || [];
- // Device type classification
- if (n.includes('iphone') || n.includes('phone') || n.includes('pixel') || n.includes('galaxy') || n.includes('android')) {
+ // Device type classification - more lenient matching
+ let classified = false;
+
+ // Phones
+ if (name.includes('iphone') || name.includes('phone') || name.includes('pixel') ||
+ name.includes('galaxy') || name.includes('android') || name.includes('samsung') ||
+ name.includes('oneplus') || name.includes('huawei') || name.includes('xiaomi')) {
deviceStats.phones++;
- } else if (n.includes('macbook') || n.includes('laptop') || n.includes('pc') || n.includes('computer') || n.includes('imac')) {
+ classified = true;
+ }
+ // Computers
+ else if (name.includes('macbook') || name.includes('laptop') || name.includes('pc') ||
+ name.includes('computer') || name.includes('imac') || name.includes('mac mini') ||
+ name.includes('thinkpad') || name.includes('surface') || name.includes('dell') ||
+ name.includes('hp ') || name.includes('lenovo')) {
deviceStats.computers++;
- } else if (n.includes('airpod') || n.includes('headphone') || n.includes('speaker') || n.includes('buds') || n.includes('audio') || n.includes('beats')) {
+ classified = true;
+ }
+ // Audio devices
+ else if (name.includes('airpod') || name.includes('headphone') || name.includes('speaker') ||
+ name.includes('buds') || name.includes('audio') || name.includes('beats') ||
+ name.includes('bose') || name.includes('sony wh') || name.includes('sony wf') ||
+ name.includes('jbl') || name.includes('soundbar') || name.includes('earbuds') ||
+ name.includes('jabra') || name.includes('soundcore')) {
deviceStats.audio++;
- } else if (n.includes('watch') || n.includes('band') || n.includes('fitbit') || n.includes('garmin')) {
+ classified = true;
+ }
+ // Wearables
+ else if (name.includes('watch') || name.includes('band') || name.includes('fitbit') ||
+ name.includes('garmin') || name.includes('whoop') || name.includes('oura') ||
+ name.includes('mi band') || name.includes('amazfit')) {
deviceStats.wearables++;
- } else {
- deviceStats.other++;
+ classified = true;
+ }
+
+ // If not classified by name, try manufacturer
+ if (!classified) {
+ if (mfr.includes('apple')) {
+ // Could be various Apple devices - count as other
+ deviceStats.other++;
+ } else {
+ deviceStats.other++;
+ }
}
// Signal strength classification
- if (rssi !== null && rssi !== undefined) {
+ if (rssi != null) {
if (rssi >= -50) deviceStats.strong++;
else if (rssi >= -70) deviceStats.medium++;
else deviceStats.weak++;
}
- // Tracker detection (Apple, Tile, etc.)
- if (m.includes('apple') && (d.heuristic_flags || []).includes('beacon_like')) {
- if (!deviceStats.findmy.find(t => t.address === d.address)) {
- deviceStats.findmy.push(d);
- }
- }
- if (n.includes('tile') || n.includes('airtag') || n.includes('smarttag')) {
+ // Tracker detection - check for known tracker patterns
+ const isTracker = name.includes('tile') || name.includes('airtag') ||
+ name.includes('smarttag') || name.includes('chipolo') ||
+ name.includes('tracker') || name.includes('tag');
+
+ if (isTracker) {
if (!deviceStats.trackers.find(t => t.address === d.address)) {
deviceStats.trackers.push(d);
}
}
+
+ // FindMy detection - Apple devices with specific characteristics
+ // Apple manufacturer ID is 0x004C (76)
+ const isApple = mfr.includes('apple') || d.manufacturer_id === 76;
+ const hasBeaconBehavior = flags.includes('beacon_like') || flags.includes('persistent');
+
+ if (isApple && hasBeaconBehavior) {
+ if (!deviceStats.findmy.find(t => t.address === d.address)) {
+ deviceStats.findmy.push(d);
+ }
+ }
});
}
@@ -578,16 +788,18 @@ const BluetoothMode = (function() {
// Tracker Detection
const trackerList = document.getElementById('btTrackerList');
if (trackerList) {
- if (deviceStats.trackers.length === 0) {
- trackerList.innerHTML = '
No trackers detected
';
+ if (devices.size === 0) {
+ trackerList.innerHTML = '
Start scanning to detect trackers
';
+ } else if (deviceStats.trackers.length === 0) {
+ trackerList.innerHTML = '
✓ No known trackers detected
';
} else {
trackerList.innerHTML = deviceStats.trackers.map(t => `
-
+
- ${t.name || formatDeviceId(t.address)}
- ${t.rssi_current || '--'} dBm
+ ${escapeHtml(t.name || formatDeviceId(t.address))}
+ ${t.rssi_current || '--'} dBm
-
${t.address}
+
${t.address}
`).join('');
}
@@ -596,121 +808,24 @@ const BluetoothMode = (function() {
// FindMy Detection
const findmyList = document.getElementById('btFindMyList');
if (findmyList) {
- if (deviceStats.findmy.length === 0) {
- findmyList.innerHTML = '
No FindMy devices detected
';
+ if (devices.size === 0) {
+ findmyList.innerHTML = '
Start scanning to detect FindMy devices
';
+ } else if (deviceStats.findmy.length === 0) {
+ findmyList.innerHTML = '
No FindMy-compatible devices detected
';
} else {
findmyList.innerHTML = deviceStats.findmy.map(t => `
-
+
- ${t.name || 'Apple Device'}
- ${t.rssi_current || '--'} dBm
+ ${escapeHtml(t.name || 'Apple Device')}
+ ${t.rssi_current || '--'} dBm
-
${t.address}
+
${t.address}
`).join('');
}
}
}
- /**
- * Initialize radar canvas
- */
- function initRadar() {
- const canvas = document.getElementById('btRadarCanvas');
- if (!canvas) return;
-
- const ctx = canvas.getContext('2d');
- drawRadarBackground(ctx, canvas.width, canvas.height);
- }
-
- /**
- * Draw radar background
- */
- function drawRadarBackground(ctx, width, height) {
- const centerX = width / 2;
- const centerY = height / 2;
- const maxRadius = Math.min(width, height) / 2 - 10;
-
- ctx.clearRect(0, 0, width, height);
-
- // Draw concentric circles
- ctx.strokeStyle = 'rgba(0, 212, 255, 0.2)';
- ctx.lineWidth = 1;
- for (let i = 1; i <= 4; i++) {
- ctx.beginPath();
- ctx.arc(centerX, centerY, maxRadius * i / 4, 0, Math.PI * 2);
- ctx.stroke();
- }
-
- // Draw cross lines
- ctx.beginPath();
- ctx.moveTo(centerX, 10);
- ctx.lineTo(centerX, height - 10);
- ctx.moveTo(10, centerY);
- ctx.lineTo(width - 10, centerY);
- ctx.stroke();
-
- // Center dot
- ctx.fillStyle = '#00d4ff';
- ctx.beginPath();
- ctx.arc(centerX, centerY, 3, 0, Math.PI * 2);
- ctx.fill();
- }
-
- /**
- * Update radar with device positions
- */
- function updateRadar() {
- const canvas = document.getElementById('btRadarCanvas');
- if (!canvas) return;
-
- const ctx = canvas.getContext('2d');
- const width = canvas.width;
- const height = canvas.height;
- const centerX = width / 2;
- const centerY = height / 2;
- const maxRadius = Math.min(width, height) / 2 - 15;
-
- // Redraw background
- drawRadarBackground(ctx, width, height);
-
- // Plot devices
- let angle = 0;
- const angleStep = (Math.PI * 2) / Math.max(devices.size, 1);
-
- devices.forEach(device => {
- const rssi = device.rssi_current;
- if (rssi === null || rssi === undefined) return;
-
- // Convert RSSI to distance (closer = smaller radius)
- // RSSI typically ranges from -30 (very close) to -100 (far)
- const normalizedRssi = Math.max(0, Math.min(1, (rssi + 100) / 70));
- const radius = maxRadius * (1 - normalizedRssi);
-
- const x = centerX + Math.cos(angle) * radius;
- const y = centerY + Math.sin(angle) * radius;
-
- // Color based on signal strength
- const color = getRssiColor(rssi);
-
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(x, y, 4, 0, Math.PI * 2);
- ctx.fill();
-
- // Glow effect
- ctx.fillStyle = color.replace(')', ', 0.3)').replace('rgb', 'rgba');
- ctx.beginPath();
- ctx.arc(x, y, 8, 0, Math.PI * 2);
- ctx.fill();
-
- angle += angleStep;
- });
- }
-
- /**
- * Update device count display
- */
function updateDeviceCount() {
const countEl = document.getElementById('btDeviceListCount');
if (countEl) {
@@ -718,9 +833,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Render a device card
- */
function renderDevice(device) {
if (!deviceContainer) {
deviceContainer = document.getElementById('btDeviceListContent');
@@ -738,9 +850,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Simple device card with click handler
- */
function createSimpleDeviceCard(device) {
const protocol = device.protocol || 'ble';
const protoBadge = protocol === 'ble'
@@ -756,20 +865,20 @@ const BluetoothMode = (function() {
badgesHtml += '
PERSISTENT';
}
- // Use device name if available, otherwise format the address nicely
const displayName = device.name || formatDeviceId(device.address);
const name = escapeHtml(displayName);
const addr = escapeHtml(device.address || 'Unknown');
const addrType = escapeHtml(device.address_type || 'unknown');
const rssi = device.rssi_current;
- const rssiStr = (rssi !== null && rssi !== undefined) ? rssi + ' dBm' : '--';
+ const rssiStr = (rssi != null) ? rssi + ' dBm' : '--';
const rssiColor = getRssiColor(rssi);
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
const seenCount = device.seen_count || 0;
const rangeBand = device.range_band || 'unknown';
const inBaseline = device.in_baseline || false;
+ const isSelected = selectedDeviceId === device.device_id;
- const cardStyle = 'display:block;background:#1a1a2e;border:1px solid #444;border-radius:8px;padding:14px;margin-bottom:10px;cursor:pointer;transition:border-color 0.2s;';
+ const cardStyle = 'display:block;background:#1a1a2e;border:1px solid ' + (isSelected ? '#00d4ff' : '#444') + ';border-radius:8px;padding:14px;margin-bottom:10px;cursor:pointer;transition:border-color 0.2s;' + (isSelected ? 'box-shadow:0 0 0 1px rgba(0,212,255,0.3);' : '');
const headerStyle = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;';
const nameStyle = 'font-size:14px;font-weight:600;color:#e0e0e0;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
const addrStyle = 'font-family:monospace;font-size:11px;color:#00d4ff;';
@@ -782,7 +891,7 @@ const BluetoothMode = (function() {
const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'");
- return '
' +
+ return '
' +
'
' +
'
' + protoBadge + badgesHtml + '
' +
'
' + (inBaseline ? '✓ Known' : '● New') + '' +
@@ -803,11 +912,8 @@ const BluetoothMode = (function() {
'
';
}
- /**
- * Get RSSI color
- */
function getRssiColor(rssi) {
- if (rssi === null || rssi === undefined) return '#666';
+ if (rssi == null) return '#666';
if (rssi >= -50) return '#22c55e';
if (rssi >= -60) return '#84cc16';
if (rssi >= -70) return '#eab308';
@@ -815,9 +921,6 @@ const BluetoothMode = (function() {
return '#ef4444';
}
- /**
- * Escape HTML
- */
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
@@ -825,9 +928,6 @@ const BluetoothMode = (function() {
return div.innerHTML;
}
- /**
- * Set baseline
- */
async function setBaseline() {
try {
const response = await fetch('/api/bluetooth/baseline/set', { method: 'POST' });
@@ -843,9 +943,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Clear baseline
- */
async function clearBaseline() {
try {
const response = await fetch('/api/bluetooth/baseline/clear', { method: 'POST' });
@@ -861,9 +958,6 @@ const BluetoothMode = (function() {
}
}
- /**
- * Update baseline status display
- */
function updateBaselineStatus() {
if (!baselineStatusEl) return;
@@ -876,19 +970,12 @@ const BluetoothMode = (function() {
}
}
- /**
- * Export data
- */
function exportData(format) {
window.open(`/api/bluetooth/export?format=${format}`, '_blank');
}
- /**
- * Show error message
- */
function showErrorMessage(message) {
console.error('[BT] Error:', message);
- // Could show a toast notification here
}
// Public API
@@ -900,15 +987,14 @@ const BluetoothMode = (function() {
setBaseline,
clearBaseline,
exportData,
- showModal,
- closeModal,
+ selectDevice,
copyAddress,
getDevices: () => Array.from(devices.values()),
isScanning: () => isScanning
};
})();
-// Global functions for onclick handlers in HTML
+// Global functions for onclick handlers
function btStartScan() { BluetoothMode.startScan(); }
function btStopScan() { BluetoothMode.stopScan(); }
function btCheckCapabilities() { BluetoothMode.checkCapabilities(); }
@@ -929,5 +1015,4 @@ if (document.readyState === 'loading') {
}
}
-// Make globally available
window.BluetoothMode = BluetoothMode;