diff --git a/static/css/index.css b/static/css/index.css index 76558b7..9a74517 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -3269,8 +3269,28 @@ header h1 .tagline { min-height: 400px; } -.bt-layout-container .wifi-visuals { +.bt-visuals-column { flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.bt-radar-panel { + flex: 1; + min-height: 0; +} + +.bt-bottom-panels { + display: flex; + gap: 12px; + flex-shrink: 0; +} + +.bt-compact-panel { + flex: 1; + min-width: 0; } .bt-device-list { @@ -3281,33 +3301,68 @@ header h1 .tagline { color: var(--accent-purple); } +/* Bluetooth Device Filters */ +.bt-device-filters { + display: flex; + gap: 6px; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; +} + +.bt-filter-btn { + padding: 5px 12px; + font-size: 11px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-dim); + cursor: pointer; + transition: all 0.2s ease; +} + +.bt-filter-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.bt-filter-btn.active { + background: var(--accent-purple); + border-color: var(--accent-purple); + color: white; +} + /* Bluetooth Signal Distribution */ .bt-signal-dist { display: flex; flex-direction: column; - gap: 8px; - font-size: 10px; + gap: 12px; + font-size: 11px; + padding: 4px 0; } .signal-range { display: flex; align-items: center; - gap: 8px; + gap: 10px; } .signal-range span:first-child { - width: 70px; + width: 80px; color: var(--text-dim); + font-size: 10px; } .signal-range span:last-child { - width: 20px; + width: 28px; text-align: right; + font-weight: 600; + font-size: 12px; } .signal-bar-bg { flex: 1; - height: 8px; + height: 16px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; @@ -3320,15 +3375,15 @@ header h1 .tagline { } .signal-bar.strong { - background: var(--accent-green); + background: linear-gradient(90deg, #22c55e, #16a34a); } .signal-bar.medium { - background: var(--accent-orange); + background: linear-gradient(90deg, #eab308, #ca8a04); } .signal-bar.weak { - background: var(--accent-red); + background: linear-gradient(90deg, #ef4444, #dc2626); } /* Bluetooth Device Cards */ diff --git a/static/js/modes/bluetooth.js b/static/js/modes/bluetooth.js index d229fd3..2f450dd 100644 --- a/static/js/modes/bluetooth.js +++ b/static/js/modes/bluetooth.js @@ -33,6 +33,9 @@ const BluetoothMode = (function() { let radarInitialized = false; let radarPaused = false; + // Device list filter + let currentDeviceFilter = 'all'; + /** * Initialize the Bluetooth mode */ @@ -64,10 +67,87 @@ const BluetoothMode = (function() { // Initialize legacy heatmap (zone counts) initHeatmap(); + // Initialize device list filters + initDeviceFilters(); + // Set initial panel states updateVisualizationPanels(); } + /** + * Initialize device list filter buttons + */ + function initDeviceFilters() { + const filterContainer = document.getElementById('btDeviceFilters'); + if (!filterContainer) return; + + filterContainer.addEventListener('click', (e) => { + const btn = e.target.closest('.bt-filter-btn'); + if (!btn) return; + + const filter = btn.dataset.filter; + if (!filter) return; + + // Update active state + filterContainer.querySelectorAll('.bt-filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + // Apply filter + currentDeviceFilter = filter; + applyDeviceFilter(); + }); + } + + /** + * Apply current filter to device list + */ + function applyDeviceFilter() { + if (!deviceContainer) return; + + const cards = deviceContainer.querySelectorAll('[data-bt-device-id]'); + cards.forEach(card => { + const isNew = card.dataset.isNew === 'true'; + const hasName = card.dataset.hasName === 'true'; + const rssi = parseInt(card.dataset.rssi) || -100; + + let visible = true; + switch (currentDeviceFilter) { + case 'new': + visible = isNew; + break; + case 'named': + visible = hasName; + break; + case 'strong': + visible = rssi >= -70; + break; + case 'all': + default: + visible = true; + } + + card.style.display = visible ? 'block' : 'none'; + }); + + // Update visible count + updateFilteredCount(); + } + + /** + * Update the device count display based on visible devices + */ + function updateFilteredCount() { + const countEl = document.getElementById('btDeviceListCount'); + if (!countEl || !deviceContainer) return; + + if (currentDeviceFilter === 'all') { + countEl.textContent = devices.size; + } else { + const visible = deviceContainer.querySelectorAll('[data-bt-device-id]:not([style*="display: none"])').length; + countEl.textContent = visible + '/' + devices.size; + } + } + /** * Initialize the new proximity radar component */ @@ -676,10 +756,7 @@ const BluetoothMode = (function() { } function updateDeviceCount() { - const countEl = document.getElementById('btDeviceListCount'); - if (countEl) { - countEl.textContent = devices.size; - } + updateFilteredCount(); } function renderDevice(device) { @@ -697,6 +774,11 @@ const BluetoothMode = (function() { } else { deviceContainer.insertAdjacentHTML('afterbegin', cardHtml); } + + // Re-apply filter after rendering + if (currentDeviceFilter !== 'all') { + applyDeviceFilter(); + } } function createSimpleDeviceCard(device) { @@ -738,8 +820,10 @@ const BluetoothMode = (function() { const statusPillStyle = 'background:' + (inBaseline ? 'rgba(34,197,94,0.15)' : 'rgba(59,130,246,0.15)') + ';color:' + (inBaseline ? '#22c55e' : '#3b82f6') + ';padding:3px 10px;border-radius:12px;font-size:10px;font-weight:500;'; const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'"); + const isNew = !inBaseline; + const hasName = !!device.name; - return '
' + + return '
' + '
' + '
' + protoBadge + badgesHtml + '
' + '' + (inBaseline ? '✓ Known' : '● New') + '' + diff --git a/templates/index.html b/templates/index.html index aadc266..a39cf6e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -706,9 +706,9 @@