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 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 @@
+
+
+
+
+
+
+
+
+ Manufacturer
+ Unknown
+
+
+ Mfr ID
+ --
+
+
+ Type
+ --
+
+
+ Seen
+ 0×
+
+
+ Min RSSI
+
+
+
+ Max RSSI
+
+
+
+ First Seen
+ --
+
+
+ Last Seen
+ --
+
+
+
+
+
+
+
+
Proximity Radar