diff --git a/static/css/index.css b/static/css/index.css index ec52d18..de466e7 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -3293,6 +3293,233 @@ header h1 .tagline { min-width: 0; } +/* Bluetooth Device Detail Panel */ +.bt-detail-panel { + background: var(--bg-tertiary); + border: 1px solid var(--accent-cyan); + border-radius: 8px; + margin-bottom: 12px; + overflow: hidden; + flex-shrink: 0; +} + +.bt-detail-header { + background: linear-gradient(135deg, rgba(0, 212, 255, 0.1), rgba(139, 92, 246, 0.1)); + padding: 12px 14px; + border-bottom: 1px solid var(--border-color); +} + +.bt-detail-title-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.bt-detail-title-row h5 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.bt-detail-close { + background: none; + border: none; + color: var(--text-dim); + font-size: 20px; + cursor: pointer; + padding: 0; + line-height: 1; + transition: color 0.2s; +} + +.bt-detail-close:hover { + color: var(--accent-red); +} + +.bt-detail-address { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + margin-top: 4px; +} + +.bt-detail-body { + padding: 12px 14px; +} + +.bt-detail-rssi-section { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; + padding: 10px; + background: var(--bg-secondary); + border-radius: 6px; +} + +.bt-detail-rssi-main { + display: flex; + align-items: baseline; + gap: 4px; +} + +.bt-detail-rssi-value { + font-family: 'JetBrains Mono', monospace; + font-size: 28px; + font-weight: 700; +} + +.bt-detail-rssi-unit { + font-size: 12px; + color: var(--text-dim); +} + +.bt-detail-rssi-bar-container { + flex: 1; + height: 10px; + background: var(--bg-tertiary); + border-radius: 5px; + overflow: hidden; +} + +.bt-detail-rssi-bar { + height: 100%; + border-radius: 5px; + transition: width 0.3s ease; +} + +.bt-detail-rssi-range { + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + white-space: nowrap; +} + +.bt-detail-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 12px; +} + +.bt-detail-badge { + padding: 3px 8px; + border-radius: 4px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.bt-detail-badge.ble { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.bt-detail-badge.classic { + background: rgba(139, 92, 246, 0.2); + color: #8b5cf6; + border: 1px solid rgba(139, 92, 246, 0.3); +} + +.bt-detail-badge.new { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.bt-detail-badge.baseline { + background: rgba(34, 197, 94, 0.2); + color: #22c55e; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.bt-detail-badge.flag { + background: rgba(107, 114, 128, 0.2); + color: #9ca3af; + border: 1px solid rgba(107, 114, 128, 0.3); +} + +.bt-detail-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 12px; +} + +.bt-detail-stat { + background: var(--bg-secondary); + padding: 8px 10px; + border-radius: 4px; +} + +.bt-detail-stat-label { + display: block; + font-size: 9px; + color: var(--text-dim); + text-transform: uppercase; + margin-bottom: 2px; +} + +.bt-detail-stat-value { + display: block; + font-size: 11px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bt-detail-services { + margin-bottom: 12px; +} + +.bt-detail-services-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 6px; +} + +.bt-detail-service { + font-family: 'JetBrains Mono', monospace; + font-size: 9px; + background: var(--bg-secondary); + padding: 4px 8px; + border-radius: 4px; + color: var(--text-dim); +} + +.bt-detail-actions { + display: flex; + gap: 8px; +} + +.bt-detail-btn { + padding: 6px 14px; + font-size: 11px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; +} + +.bt-detail-btn:hover { + background: var(--accent-cyan); + border-color: var(--accent-cyan); + color: #000; +} + +/* Selected device highlight */ +.bt-device-row.selected { + background: rgba(0, 212, 255, 0.1); + border-color: var(--accent-cyan); +} + .bt-device-list { border-left-color: var(--accent-purple) !important; } diff --git a/static/js/modes/bluetooth.js b/static/js/modes/bluetooth.js index f227bcc..752274e 100644 --- a/static/js/modes/bluetooth.js +++ b/static/js/modes/bluetooth.js @@ -287,182 +287,155 @@ const BluetoothMode = (function() { if (farEl) farEl.textContent = zoneCounts.far; } + // Currently selected device + let selectedDeviceId = null; + /** - * Show device detail modal + * Show device detail panel */ - function showModal(deviceId) { + function showDeviceDetail(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; + selectedDeviceId = deviceId; + const panel = document.getElementById('btDetailPanel'); + if (!panel) return; const rssi = device.rssi_current; const rssiColor = getRssiColor(rssi); + const rssiPercent = rssi != null ? Math.max(0, Math.min(100, ((rssi + 100) / 70) * 100)) : 0; const flags = device.heuristic_flags || []; const protocol = device.protocol || 'ble'; - title.textContent = device.name || formatDeviceId(device.address); + // Update panel elements + document.getElementById('btDetailName').textContent = device.name || formatDeviceId(device.address); + document.getElementById('btDetailAddress').textContent = device.address; + document.getElementById('btDetailAddress').style.color = '#00d4ff'; - body.innerHTML = ` - -
-
- ${rssi != null ? rssi : '--'} dBm -
-
${device.range_band || 'Unknown'} Range
-
+ // RSSI section + const rssiEl = document.getElementById('btDetailRssi'); + rssiEl.textContent = rssi != null ? rssi : '--'; + rssiEl.style.color = rssiColor; - -
- ${protocol.toUpperCase()} - ${device.in_baseline ? '✓ BASELINE' : '● NEW'} - ${flags.map(f => `${f.replace('_', ' ').toUpperCase()}`).join('')} -
+ const rssiBar = document.getElementById('btDetailRssiBar'); + rssiBar.style.width = rssiPercent + '%'; + rssiBar.style.background = rssiColor; - -
-
Address
-
- ${escapeHtml(device.address)} -
-
Type: ${device.address_type || 'unknown'}
-
+ document.getElementById('btDetailRange').textContent = (device.range_band || 'Unknown') + ' Range'; - -
-
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) + '%' : '--'}
-
-
-
+ // Badges + const badgesEl = document.getElementById('btDetailBadges'); + let badgesHtml = `${protocol.toUpperCase()}`; + badgesHtml += `${device.in_baseline ? '✓ KNOWN' : '● NEW'}`; + flags.forEach(f => { + badgesHtml += `${f.replace(/_/g, ' ').toUpperCase()}`; + }); + badgesEl.innerHTML = badgesHtml; - -
-
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' : '--'}
-
-
-
+ // Stats grid + document.getElementById('btDetailMfr').textContent = device.manufacturer_name || 'Unknown'; + document.getElementById('btDetailMfrId').textContent = device.manufacturer_id != null + ? '0x' + device.manufacturer_id.toString(16).toUpperCase().padStart(4, '0') + : '--'; + document.getElementById('btDetailAddrType').textContent = device.address_type || 'unknown'; + document.getElementById('btDetailSeen').textContent = (device.seen_count || 0) + '×'; - - ${device.service_uuids && device.service_uuids.length > 0 ? ` -
-
Service UUIDs (${device.service_uuids.length})
-
- ${device.service_uuids.map(uuid => `${uuid}`).join('')} -
-
- ` : ''} + const rssiMinEl = document.getElementById('btDetailRssiMin'); + rssiMinEl.textContent = device.rssi_min != null ? device.rssi_min + ' dBm' : '--'; + rssiMinEl.style.color = '#ef4444'; - -
-
Timestamps
-
-
-
First Seen
-
${device.first_seen ? new Date(device.first_seen).toLocaleTimeString() : '--'}
-
-
-
Last Seen
-
${device.last_seen ? new Date(device.last_seen).toLocaleTimeString() : '--'}
-
-
-
+ const rssiMaxEl = document.getElementById('btDetailRssiMax'); + rssiMaxEl.textContent = device.rssi_max != null ? device.rssi_max + ' dBm' : '--'; + rssiMaxEl.style.color = '#22c55e'; - -
- - -
- `; + 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() + : '--'; - modal.style.display = 'flex'; + // Services + const servicesContainer = document.getElementById('btDetailServices'); + const servicesList = document.getElementById('btDetailServicesList'); + if (device.service_uuids && device.service_uuids.length > 0) { + servicesContainer.style.display = 'block'; + servicesList.innerHTML = device.service_uuids.map(uuid => + `${uuid}` + ).join(''); + } else { + servicesContainer.style.display = 'none'; + } - // Close on overlay click - modal.onclick = (e) => { - if (e.target === modal) closeModal(); - }; + // Show panel + panel.style.display = 'block'; - // Close on Escape key - document.addEventListener('keydown', handleModalKeydown); + // Highlight selected device in list + highlightSelectedDevice(deviceId); } /** - * Close device detail modal + * Clear device selection */ - function closeModal() { - const modal = document.getElementById('btDeviceModal'); - if (modal) modal.style.display = 'none'; - document.removeEventListener('keydown', handleModalKeydown); + function clearSelection() { + selectedDeviceId = null; + const panel = document.getElementById('btDetailPanel'); + if (panel) panel.style.display = 'none'; + + // Remove highlight from device list + if (deviceContainer) { + deviceContainer.querySelectorAll('.bt-device-row.selected').forEach(el => { + el.classList.remove('selected'); + }); + } } /** - * Handle keydown for modal + * Highlight selected device in the list */ - function handleModalKeydown(e) { - if (e.key === 'Escape') closeModal(); + function highlightSelectedDevice(deviceId) { + if (!deviceContainer) return; + + // Remove existing highlights + deviceContainer.querySelectorAll('.bt-device-row.selected').forEach(el => { + el.classList.remove('selected'); + }); + + // Add highlight to selected device + const escapedId = CSS.escape(deviceId); + const card = deviceContainer.querySelector(`[data-bt-device-id="${escapedId}"]`); + if (card) { + card.classList.add('selected'); + } + } + + /** + * Copy selected device address to clipboard + */ + function copyAddress() { + if (!selectedDeviceId) return; + const device = devices.get(selectedDeviceId); + if (!device) return; + + navigator.clipboard.writeText(device.address).then(() => { + const btn = document.querySelector('.bt-detail-btn'); + if (btn) { + const originalText = btn.textContent; + btn.textContent = 'Copied!'; + btn.style.background = '#22c55e'; + setTimeout(() => { + btn.textContent = originalText; + btn.style.background = ''; + }, 1500); + } + }); } /** * Select a device - opens modal with details */ function selectDevice(deviceId) { - showModal(deviceId); - } - - /** - * Copy address to clipboard - */ - function copyAddress(address) { - navigator.clipboard.writeText(address).then(() => { - // Brief visual feedback - const btn = event.target; - const originalText = btn.textContent; - btn.textContent = 'Copied!'; - btn.style.background = '#22c55e'; - setTimeout(() => { - btn.textContent = originalText; - btn.style.background = '#252538'; - }, 1500); - }); + showDeviceDetail(deviceId); } /** @@ -910,8 +883,7 @@ const BluetoothMode = (function() { clearBaseline, exportData, selectDevice, - showModal, - closeModal, + clearSelection, copyAddress, getDevices: () => Array.from(devices.values()), isScanning: () => isScanning diff --git a/templates/index.html b/templates/index.html index a39cf6e..af3e890 100644 --- a/templates/index.html +++ b/templates/index.html @@ -707,6 +707,70 @@