diff --git a/static/css/index.css b/static/css/index.css index 35d8cf6..c615e2b 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -3377,6 +3377,209 @@ header h1 .tagline { background: rgba(0, 122, 255, 0.05); } +/* Bluetooth Device Modal */ +.bt-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(4px); +} + +.bt-modal { + background: var(--bg-secondary, #1a1a2e); + border: 1px solid var(--border-color, #333); + border-radius: 12px; + width: 90%; + max-width: 500px; + max-height: 85vh; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.bt-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color, #333); + background: var(--bg-tertiary, #141428); +} + +.bt-modal-header h4 { + margin: 0; + color: var(--text-primary, #e0e0e0); + font-size: 16px; + font-weight: 600; +} + +.bt-modal-close { + background: none; + border: none; + color: var(--text-dim, #666); + font-size: 24px; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all 0.2s; +} + +.bt-modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary, #e0e0e0); +} + +.bt-modal-body { + padding: 20px; + overflow-y: auto; + max-height: calc(85vh - 60px); +} + +.bt-modal-section { + margin-bottom: 16px; +} + +.bt-modal-section:last-child { + margin-bottom: 0; +} + +.bt-modal-section-title { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-dim, #666); + margin-bottom: 8px; +} + +.bt-modal-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; +} + +.bt-modal-stat { + background: var(--bg-tertiary, #141428); + padding: 10px 12px; + border-radius: 6px; +} + +.bt-modal-stat-label { + font-size: 9px; + text-transform: uppercase; + color: var(--text-dim, #666); + margin-bottom: 4px; +} + +.bt-modal-stat-value { + font-size: 13px; + color: var(--text-primary, #e0e0e0); + font-family: monospace; +} + +.bt-modal-badge { + display: inline-block; + padding: 3px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + margin-right: 6px; + margin-bottom: 6px; +} + +.bt-modal-badge.ble { + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.bt-modal-badge.classic { + background: rgba(139, 92, 246, 0.15); + color: #8b5cf6; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.bt-modal-badge.new { + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; +} + +.bt-modal-badge.baseline { + background: rgba(34, 197, 94, 0.15); + color: #22c55e; +} + +.bt-modal-badge.flag { + background: rgba(107, 114, 128, 0.15); + color: #9ca3af; +} + +.bt-modal-rssi { + text-align: center; + padding: 16px; + background: var(--bg-tertiary, #141428); + border-radius: 8px; + margin-bottom: 16px; +} + +.bt-modal-rssi-value { + font-size: 36px; + font-weight: 700; + font-family: monospace; +} + +.bt-modal-rssi-label { + font-size: 11px; + color: var(--text-dim, #666); + margin-top: 4px; +} + +.bt-modal-actions { + display: flex; + gap: 10px; + margin-top: 16px; +} + +.bt-modal-actions button { + flex: 1; + padding: 10px; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.bt-modal-btn-primary { + background: var(--accent-cyan, #00d4ff); + border: none; + color: #000; + font-weight: 600; +} + +.bt-modal-btn-primary:hover { + background: #00b8e6; +} + +.bt-modal-btn-secondary { + background: var(--bg-tertiary, #141428); + border: 1px solid var(--border-color, #333); + color: var(--text-primary, #e0e0e0); +} + +.bt-modal-btn-secondary:hover { + background: var(--bg-secondary, #1a1a2e); +} + @media (max-width: 1200px) { .bt-layout-container { flex-direction: column; diff --git a/static/js/modes/bluetooth.js b/static/js/modes/bluetooth.js index c4c16f8..b0c6b42 100644 --- a/static/js/modes/bluetooth.js +++ b/static/js/modes/bluetooth.js @@ -12,7 +12,6 @@ const BluetoothMode = (function() { let devices = new Map(); let baselineSet = false; let baselineCount = 0; - let selectedDeviceId = null; // DOM elements (cached) let startBtn, stopBtn, messageContainer, deviceContainer; @@ -33,9 +32,8 @@ const BluetoothMode = (function() { findmy: [] }; - // Proximity visualization state - let deviceAngles = new Map(); // Store assigned angles for each device - let pendingVisualizationUpdate = false; + // Zone counts for proximity display + let zoneCounts = { veryClose: 0, close: 0, nearby: 0, far: 0 }; /** * Initialize the Bluetooth mode @@ -73,163 +71,192 @@ const BluetoothMode = (function() { } /** - * Initialize the proximity visualization + * Initialize proximity zones display */ function initHeatmap() { - const canvas = document.getElementById('btRadarCanvas'); - if (!canvas) return; - - canvas.width = 180; - canvas.height = 180; - drawProximityVisualization(); + updateProximityZones(); } /** - * Draw clean zone-based proximity visualization + * Update proximity zone counts (simple HTML, no canvas) */ - function drawProximityVisualization() { - const canvas = document.getElementById('btRadarCanvas'); - if (!canvas) return; + function updateProximityZones() { + zoneCounts = { veryClose: 0, close: 0, nearby: 0, far: 0 }; - 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 - 10; - - // Clear canvas - ctx.clearRect(0, 0, width, height); - - // Define zones - const zones = [ - { radius: 1.0, color: 'rgba(239, 68, 68, 0.04)', border: 'rgba(239, 68, 68, 0.2)' }, - { radius: 0.75, color: 'rgba(234, 179, 8, 0.05)', border: 'rgba(234, 179, 8, 0.25)' }, - { radius: 0.5, color: 'rgba(132, 204, 22, 0.06)', border: 'rgba(132, 204, 22, 0.3)' }, - { radius: 0.25, color: 'rgba(34, 197, 94, 0.08)', border: 'rgba(34, 197, 94, 0.4)' } - ]; - - // Draw zones - zones.forEach(zone => { - const r = maxRadius * zone.radius; - ctx.beginPath(); - ctx.arc(centerX, centerY, r, 0, Math.PI * 2); - ctx.fillStyle = zone.color; - ctx.fill(); - ctx.strokeStyle = zone.border; - ctx.lineWidth = 1; - ctx.stroke(); - }); - - // Count devices per zone and draw dots - const zoneCounts = [0, 0, 0, 0]; - - devices.forEach((device, deviceId) => { + devices.forEach(device => { const rssi = device.rssi_current; if (rssi == null) return; - // Count zone - if (rssi >= -40) zoneCounts[0]++; - else if (rssi >= -55) zoneCounts[1]++; - else if (rssi >= -70) zoneCounts[2]++; - else zoneCounts[3]++; - - // Get or assign angle for this device - let angle = deviceAngles.get(deviceId); - if (angle === undefined) { - angle = Math.random() * Math.PI * 2; - deviceAngles.set(deviceId, angle); - } - - // Calculate position based on RSSI - const normalizedRssi = Math.max(0, Math.min(1, (rssi + 100) / 70)); - const radius = maxRadius * (1 - normalizedRssi * 0.85 + 0.1); - const x = centerX + Math.cos(angle) * radius; - const y = centerY + Math.sin(angle) * radius; - - // Get color - const color = getRssiColorRgb(rssi); - - // Draw glow - const gradient = ctx.createRadialGradient(x, y, 0, x, y, 10); - gradient.addColorStop(0, `rgba(${color.r}, ${color.g}, ${color.b}, 0.5)`); - gradient.addColorStop(1, `rgba(${color.r}, ${color.g}, ${color.b}, 0)`); - ctx.fillStyle = gradient; - ctx.beginPath(); - ctx.arc(x, y, 10, 0, Math.PI * 2); - ctx.fill(); - - // Draw dot - ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`; - ctx.beginPath(); - ctx.arc(x, y, 3, 0, Math.PI * 2); - ctx.fill(); + if (rssi >= -40) zoneCounts.veryClose++; + else if (rssi >= -55) zoneCounts.close++; + else if (rssi >= -70) zoneCounts.nearby++; + else zoneCounts.far++; }); - // Draw center point - ctx.fillStyle = '#00d4ff'; - ctx.beginPath(); - ctx.arc(centerX, centerY, 5, 0, Math.PI * 2); - ctx.fill(); + // Update DOM elements + const veryCloseEl = document.getElementById('btZoneVeryClose'); + const closeEl = document.getElementById('btZoneClose'); + const nearbyEl = document.getElementById('btZoneNearby'); + const farEl = document.getElementById('btZoneFar'); - // Draw "YOU" label - ctx.fillStyle = 'rgba(0, 212, 255, 0.8)'; - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText('YOU', centerX, centerY + 14); - - // Draw zone counts - ctx.textAlign = 'right'; - ctx.font = '9px monospace'; - const countX = width - 6; - const colors = ['#22c55e', '#84cc16', '#eab308', '#ef4444']; - const yPositions = [centerY - 45, centerY - 15, centerY + 15, centerY + 45]; - - zoneCounts.forEach((count, i) => { - if (count > 0) { - ctx.fillStyle = colors[i]; - ctx.fillText(count.toString(), countX, yPositions[i]); - } - }); - - // Total count - ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.font = '10px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(`${devices.size} devices`, centerX, height - 4); - - // Empty state message - if (devices.size === 0 && !isScanning) { - ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; - ctx.font = '11px sans-serif'; - ctx.fillText('Start scan to', centerX, centerY - 8); - ctx.fillText('detect devices', centerX, centerY + 8); - } + if (veryCloseEl) veryCloseEl.textContent = zoneCounts.veryClose; + if (closeEl) closeEl.textContent = zoneCounts.close; + if (nearbyEl) nearbyEl.textContent = zoneCounts.nearby; + if (farEl) farEl.textContent = zoneCounts.far; } /** - * Schedule visualization update using requestAnimationFrame + * Show device detail modal */ - function scheduleVisualizationUpdate() { - if (!pendingVisualizationUpdate) { - pendingVisualizationUpdate = true; - requestAnimationFrame(() => { - pendingVisualizationUpdate = false; - drawProximityVisualization(); - }); - } + function showModal(deviceId) { + const device = devices.get(deviceId); + if (!device) return; + + const modal = document.getElementById('btDeviceModal'); + const title = document.getElementById('btModalTitle'); + const body = document.getElementById('btModalBody'); + + if (!modal || !body) return; + + const rssi = device.rssi_current; + const rssiColor = getRssiColor(rssi); + const flags = device.heuristic_flags || []; + const protocol = device.protocol || 'ble'; + + title.textContent = device.name || formatDeviceId(device.address); + + body.innerHTML = ` + +
+
+ ${rssi != null ? rssi : '--'} dBm +
+
${device.range_band || 'Unknown'} Range
+
+ + +
+ ${protocol.toUpperCase()} + ${device.in_baseline ? '✓ BASELINE' : '● NEW'} + ${flags.map(f => `${f.replace('_', ' ').toUpperCase()}`).join('')} +
+ + +
+
Address
+
+ ${escapeHtml(device.address)} +
+
Type: ${device.address_type || 'unknown'}
+
+ + +
+
Device Information
+
+
+
Manufacturer
+
${escapeHtml(device.manufacturer_name || 'Unknown')}
+
+
+
Manufacturer ID
+
${device.manufacturer_id != null ? '0x' + device.manufacturer_id.toString(16).toUpperCase().padStart(4, '0') : '--'}
+
+
+
Seen Count
+
${device.seen_count || 0} times
+
+
+
Confidence
+
${device.rssi_confidence ? Math.round(device.rssi_confidence * 100) + '%' : '--'}
+
+
+
+ + +
+
Signal Statistics
+
+
+
Minimum
+
${device.rssi_min != null ? device.rssi_min + ' dBm' : '--'}
+
+
+
Maximum
+
${device.rssi_max != null ? device.rssi_max + ' dBm' : '--'}
+
+
+
Median
+
${device.rssi_median != null ? Math.round(device.rssi_median) + ' dBm' : '--'}
+
+
+
Current
+
${rssi != null ? rssi + ' dBm' : '--'}
+
+
+
+ + + ${device.service_uuids && device.service_uuids.length > 0 ? ` +
+
Service UUIDs (${device.service_uuids.length})
+
+ ${device.service_uuids.map(uuid => `${uuid}`).join('')} +
+
+ ` : ''} + + +
+
Timestamps
+
+
+
First Seen
+
${device.first_seen ? new Date(device.first_seen).toLocaleTimeString() : '--'}
+
+
+
Last Seen
+
${device.last_seen ? new Date(device.last_seen).toLocaleTimeString() : '--'}
+
+
+
+ + +
+ + +
+ `; + + modal.style.display = 'flex'; + + // Close on overlay click + modal.onclick = (e) => { + if (e.target === modal) closeModal(); + }; + + // Close on Escape key + document.addEventListener('keydown', handleModalKeydown); } /** - * Get RSSI color as RGB object + * Close device detail modal */ - function getRssiColorRgb(rssi) { - if (rssi == null) return { r: 102, g: 102, b: 102 }; - if (rssi >= -40) return { r: 34, g: 197, b: 94 }; - if (rssi >= -55) return { r: 132, g: 204, b: 22 }; - if (rssi >= -70) return { r: 234, g: 179, b: 8 }; - if (rssi >= -85) return { r: 249, g: 115, b: 22 }; - return { r: 239, g: 68, b: 68 }; + function closeModal() { + const modal = document.getElementById('btDeviceModal'); + if (modal) modal.style.display = 'none'; + document.removeEventListener('keydown', handleModalKeydown); + } + + /** + * Handle keydown for modal + */ + function handleModalKeydown(e) { + if (e.key === 'Escape') closeModal(); } /** @@ -268,137 +295,10 @@ const BluetoothMode = (function() { } /** - * Select a device and show in Selected Device panel + * Select a device - opens modal with details */ function selectDevice(deviceId) { - const device = devices.get(deviceId); - if (!device) return; - - selectedDeviceId = deviceId; - - // Update selected device panel - const panel = document.getElementById('btSelectedDevice'); - if (!panel) return; - - const rssi = device.rssi_current; - const rssiColor = getRssiColor(rssi); - const flags = device.heuristic_flags || []; - - panel.innerHTML = ` -
- -
-
-
- ${escapeHtml(device.name || formatDeviceId(device.address))} -
-
- ${escapeHtml(device.address)} - (${device.address_type || 'unknown'}) -
-
-
-
- ${rssi != null ? rssi : '--'} - dBm -
-
${device.range_band || 'unknown'}
-
-
- - -
- - ${(device.protocol || 'BLE').toUpperCase()} - - ${flags.map(f => `${f.replace('_', ' ').toUpperCase()}`).join('')} - - ${device.in_baseline ? '✓ BASELINE' : '● NEW'} - -
- - -
-
-
Manufacturer
-
${device.manufacturer_name || 'Unknown'}
-
-
-
Mfr ID
-
- ${device.manufacturer_id != null ? '0x' + device.manufacturer_id.toString(16).toUpperCase().padStart(4, '0') : '--'} -
-
-
-
Seen
-
${device.seen_count || 0} times
-
-
-
Confidence
-
${device.rssi_confidence ? Math.round(device.rssi_confidence * 100) + '%' : '--'}
-
-
- - -
-
Signal Statistics
-
-
-
MIN
-
${device.rssi_min != null ? device.rssi_min : '--'}
-
-
-
MEDIAN
-
${device.rssi_median != null ? Math.round(device.rssi_median) : '--'}
-
-
-
MAX
-
${device.rssi_max != null ? device.rssi_max : '--'}
-
-
-
CURRENT
-
${rssi != null ? rssi : '--'}
-
-
-
- - - ${device.service_uuids && device.service_uuids.length > 0 ? ` -
-
Service UUIDs (${device.service_uuids.length})
-
- ${device.service_uuids.slice(0, 6).map(uuid => `${uuid.substring(0, 8)}...`).join('')} - ${device.service_uuids.length > 6 ? `+${device.service_uuids.length - 6} more` : ''} -
-
- ` : ''} - - -
- First: ${device.first_seen ? new Date(device.first_seen).toLocaleTimeString() : '--'} - Last: ${device.last_seen ? new Date(device.last_seen).toLocaleTimeString() : '--'} -
- - -
- -
-
- `; - - // 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'; - } - }); + showModal(deviceId); } /** @@ -562,12 +462,6 @@ const BluetoothMode = (function() { deviceContainer.innerHTML = ''; devices.clear(); resetStats(); - - // Reset selected device panel - const selectedPanel = document.getElementById('btSelectedDevice'); - if (selectedPanel) { - selectedPanel.innerHTML = '
Click a device to view details
'; - } } const statusDot = document.getElementById('statusDot'); @@ -589,9 +483,8 @@ const BluetoothMode = (function() { trackers: [], findmy: [] }; - deviceAngles.clear(); updateVisualizationPanels(); - drawProximityVisualization(); + updateProximityZones(); } function startEventStream() { @@ -634,12 +527,7 @@ const BluetoothMode = (function() { updateDeviceCount(); updateStatsFromDevices(); updateVisualizationPanels(); - scheduleVisualizationUpdate(); // Throttled visualization update - - // Update selected device panel if this device is selected - if (selectedDeviceId === device.device_id) { - selectDevice(device.device_id); - } + updateProximityZones(); // Feed to activity timeline addToTimeline(device); @@ -909,9 +797,8 @@ const BluetoothMode = (function() { 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 ' + (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 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 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;'; @@ -924,7 +811,7 @@ const BluetoothMode = (function() { const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'"); - return '
' + + return '
' + '
' + '
' + protoBadge + badgesHtml + '
' + '' + (inBaseline ? '✓ Known' : '● New') + '' + @@ -1021,6 +908,8 @@ const BluetoothMode = (function() { clearBaseline, exportData, selectDevice, + showModal, + closeModal, copyAddress, getDevices: () => Array.from(devices.values()), isScanning: () => isScanning diff --git a/templates/index.html b/templates/index.html index b50d292..1f93cc5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -706,19 +706,30 @@