diff --git a/static/css/index.css b/static/css/index.css index b6d3fc6..e8a03b2 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -4060,8 +4060,8 @@ header h1 .tagline { /* Bluetooth Layout Container */ .bt-layout-container { display: flex; - gap: 15px; - padding: 15px; + gap: 12px; + padding: 12px; background: var(--bg-secondary); margin: 0 15px 10px 15px; border: 1px solid var(--border-color); @@ -4082,14 +4082,14 @@ header h1 .tagline { display: flex; gap: 12px; flex: 1; - min-height: 360px; /* ensure radar area (SVG 280px + controls + summary) is never crushed */ + min-height: 380px; } .bt-side-panels { display: flex; flex-direction: column; gap: 12px; - width: 220px; + width: 240px; flex-shrink: 0; } @@ -4107,6 +4107,90 @@ header h1 .tagline { flex-direction: column; } +#btRadarControls { + gap: 6px; +} + +.bt-radar-filter-btn, +#btRadarPauseBtn { + min-width: 84px; + padding: 4px 10px; + font-size: 10px; + 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-radar-filter-btn:hover, +#btRadarPauseBtn:hover { + color: var(--text-primary); + border-color: var(--accent-cyan); +} + +.bt-radar-filter-btn.active, +#btRadarPauseBtn.active { + color: #0f172a; + background: var(--accent-cyan); + border-color: var(--accent-cyan); +} + +.bt-zone-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + margin-top: 12px; + font-size: 11px; +} + +.bt-zone-card { + text-align: center; + border-radius: 6px; + padding: 8px 6px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} + +.bt-zone-card.immediate { + border-color: rgba(34, 197, 94, 0.35); +} + +.bt-zone-card.near { + border-color: rgba(234, 179, 8, 0.35); +} + +.bt-zone-card.far { + border-color: rgba(239, 68, 68, 0.35); +} + +.bt-zone-value { + display: block; + font-size: 19px; + font-weight: 700; +} + +.bt-zone-card.immediate .bt-zone-value { + color: #22c55e; +} + +.bt-zone-card.near .bt-zone-value { + color: #eab308; +} + +.bt-zone-card.far .bt-zone-value { + color: #ef4444; +} + +.bt-zone-label { + color: var(--text-dim); + font-size: 10px; + margin-top: 3px; + text-transform: uppercase; + letter-spacing: 0.4px; +} + .bt-radar-panel #btProximityRadar { flex: 1; min-height: 0; @@ -4248,6 +4332,70 @@ header h1 .tagline { color: #9ca3af; } +.bt-detail-badge.tracker-high { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; +} + +.bt-detail-badge.tracker-medium { + background: rgba(249, 115, 22, 0.2); + color: #f97316; +} + +.bt-detail-badge.tracker-low { + background: rgba(234, 179, 8, 0.2); + color: #eab308; +} + +.bt-detail-tracker-analysis { + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.25); + border-radius: 6px; + padding: 8px 10px; + margin-bottom: 8px; +} + +.bt-analysis-header { + color: #fca5a5; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.35px; + text-transform: uppercase; + margin-bottom: 6px; +} + +.bt-analysis-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 10px; + margin-top: 4px; +} + +.bt-analysis-label { + color: var(--text-dim); + font-size: 9px; +} + +.bt-analysis-section { + margin-top: 6px; +} + +.bt-evidence-list { + margin: 4px 0 0 0; + padding-left: 14px; + color: var(--text-primary); + font-size: 10px; +} + +.bt-analysis-warning { + margin-top: 8px; + color: #fca5a5; + font-size: 9px; + line-height: 1.35; +} + .bt-detail-grid { display: grid; grid-template-columns: repeat(4, 1fr); @@ -4283,6 +4431,8 @@ header h1 .tagline { display: flex; justify-content: space-between; align-items: center; + gap: 6px; + flex-wrap: wrap; } .bt-detail-services { @@ -4438,8 +4588,8 @@ header h1 .tagline { border-left-color: var(--accent-purple) !important; display: flex; flex-direction: column; - min-width: 280px; - max-width: 320px; + min-width: 330px; + max-width: 420px; max-height: 100%; background: var(--bg-primary); border: 1px solid var(--border-color); @@ -4451,6 +4601,8 @@ header h1 .tagline { flex: 1; overflow-y: auto; min-height: 0; + padding: 8px 10px 12px; + background: var(--bg-primary); } .bt-device-list .wifi-device-list-header { @@ -4460,6 +4612,10 @@ header h1 .tagline { padding: 10px 12px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; + position: sticky; + top: 0; + z-index: 4; + background: var(--bg-primary); } .bt-device-list .wifi-device-list-header h5 { @@ -4469,6 +4625,63 @@ header h1 .tagline { font-weight: 600; } +.bt-list-summary { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); +} + +.bt-summary-item { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 5px 6px; + min-width: 0; +} + +.bt-summary-label { + display: block; + font-size: 8px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.35px; +} + +.bt-summary-value { + display: block; + font-size: 11px; + font-weight: 700; + color: var(--text-primary); + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.bt-device-toolbar { + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-primary); +} + +.bt-device-search { + width: 100%; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 11px; + padding: 7px 8px; +} + +.bt-device-search:focus { + outline: none; + border-color: var(--accent-cyan); +} + /* Bluetooth Device Filters */ .bt-device-filters { display: flex; @@ -4477,6 +4690,10 @@ header h1 .tagline { border-bottom: 1px solid var(--border-color); flex-wrap: wrap; flex-shrink: 0; + background: var(--bg-primary); + position: sticky; + top: 44px; + z-index: 3; } .bt-filter-btn { @@ -4501,6 +4718,14 @@ header h1 .tagline { color: white; } +.bt-tracker-item { + transition: background 0.15s ease; +} + +.bt-tracker-item:hover { + background: rgba(239, 68, 68, 0.08); +} + /* Bluetooth Signal Distribution */ .bt-signal-dist { display: flex; @@ -4570,6 +4795,10 @@ header h1 .tagline { transition: all 0.15s ease; } +.bt-device-row:last-child { + margin-bottom: 0; +} + .bt-device-row:hover { background: rgba(0, 212, 255, 0.05); border-color: var(--accent-cyan); @@ -4922,14 +5151,23 @@ header h1 .tagline { min-height: 0; } - .bt-layout-container .wifi-visuals { + .bt-layout-container .bt-visuals-column { max-height: 50vh; } + .bt-main-area { + min-height: 0; + } + .bt-device-list { width: 100%; min-width: auto; - max-height: 300px; + max-width: none; + max-height: 320px; + } + + .bt-list-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); } } diff --git a/static/css/responsive.css b/static/css/responsive.css index bc83d52..54aba08 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -428,7 +428,7 @@ /* Visual panels should be scrollable, not clipped */ .wifi-visuals, - .bt-visuals { + .bt-visuals-column { max-height: none !important; overflow: visible !important; margin-bottom: 15px; @@ -444,7 +444,7 @@ /* Visual panels should stack in single column on mobile when visible */ .wifi-visuals, - .bt-visuals { + .bt-visuals-column { display: flex; flex-direction: column; gap: 10px; @@ -465,6 +465,34 @@ .wifi-visual-panel { grid-column: auto !important; } + + .bt-main-area { + flex-direction: column !important; + min-height: auto !important; + } + + .bt-side-panels { + width: 100% !important; + flex-direction: column !important; + } + + .bt-detail-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + } + + .bt-row-secondary { + padding-left: 0 !important; + white-space: normal !important; + } + + .bt-row-actions { + padding-left: 0 !important; + justify-content: flex-start !important; + } + + .bt-list-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + } } /* ============== MOBILE MAP FIXES ============== */ diff --git a/static/js/modes/bluetooth.js b/static/js/modes/bluetooth.js index 99ad162..2b64857 100644 --- a/static/js/modes/bluetooth.js +++ b/static/js/modes/bluetooth.js @@ -27,15 +27,17 @@ const BluetoothMode = (function() { trackers: [] }; - // Zone counts for proximity display - let zoneCounts = { veryClose: 0, close: 0, nearby: 0, far: 0 }; + // Zone counts for proximity display + let zoneCounts = { immediate: 0, near: 0, far: 0 }; // New visualization components let radarInitialized = false; let radarPaused = false; - // Device list filter - let currentDeviceFilter = 'all'; + // Device list filter + let currentDeviceFilter = 'all'; + let currentSearchTerm = ''; + let visibleDeviceCount = 0; // Agent support let showAllAgentsMode = false; @@ -119,80 +121,91 @@ const BluetoothMode = (function() { /** * 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(); - }); - } + function initDeviceFilters() { + const filterContainer = document.getElementById('btDeviceFilters'); + if (filterContainer) { + 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(); + }); + } + + const searchInput = document.getElementById('btDeviceSearch'); + if (searchInput) { + searchInput.addEventListener('input', () => { + currentSearchTerm = searchInput.value.trim().toLowerCase(); + 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; - const isTracker = card.dataset.isTracker === 'true'; - - let visible = true; - switch (currentDeviceFilter) { - case 'new': - visible = isNew; - break; - case 'named': - visible = hasName; - break; - case 'strong': - visible = rssi >= -70; - break; - case 'trackers': - visible = isTracker; - break; - case 'all': - default: - visible = true; - } - - card.style.display = visible ? '' : 'none'; - }); - - // Update visible count - updateFilteredCount(); - } + function applyDeviceFilter() { + if (!deviceContainer) return; + + const cards = deviceContainer.querySelectorAll('[data-bt-device-id]'); + let visibleCount = 0; + cards.forEach(card => { + const isNew = card.dataset.isNew === 'true'; + const hasName = card.dataset.hasName === 'true'; + const rssi = parseInt(card.dataset.rssi) || -100; + const isTracker = card.dataset.isTracker === 'true'; + const searchHaystack = (card.dataset.search || '').toLowerCase(); + + let matchesFilter = true; + switch (currentDeviceFilter) { + case 'new': + matchesFilter = isNew; + break; + case 'named': + matchesFilter = hasName; + break; + case 'strong': + matchesFilter = rssi >= -70; + break; + case 'trackers': + matchesFilter = isTracker; + break; + case 'all': + default: + matchesFilter = true; + } + + const matchesSearch = !currentSearchTerm || searchHaystack.includes(currentSearchTerm); + const visible = matchesFilter && matchesSearch; + card.style.display = visible ? '' : 'none'; + if (visible) visibleCount++; + }); + + visibleDeviceCount = visibleCount; + + // 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; - } - } + function updateFilteredCount() { + const countEl = document.getElementById('btDeviceListCount'); + if (!countEl || !deviceContainer) return; + + const hasFilter = currentDeviceFilter !== 'all' || currentSearchTerm.length > 0; + countEl.textContent = hasFilter ? `${visibleDeviceCount}/${devices.size}` : devices.size; + } /** * Initialize the new proximity radar component @@ -308,30 +321,20 @@ const BluetoothMode = (function() { /** * Update proximity zone counts (simple HTML, no canvas) */ - function updateProximityZones() { - zoneCounts = { veryClose: 0, close: 0, nearby: 0, far: 0 }; - - devices.forEach(device => { - const rssi = device.rssi_current; - if (rssi == null) return; - - if (rssi >= -40) zoneCounts.veryClose++; - else if (rssi >= -55) zoneCounts.close++; - else if (rssi >= -70) zoneCounts.nearby++; - else zoneCounts.far++; - }); - - // Update DOM elements - const veryCloseEl = document.getElementById('btZoneVeryClose'); - const closeEl = document.getElementById('btZoneClose'); - const nearbyEl = document.getElementById('btZoneNearby'); - const farEl = document.getElementById('btZoneFar'); - - if (veryCloseEl) veryCloseEl.textContent = zoneCounts.veryClose; - if (closeEl) closeEl.textContent = zoneCounts.close; - if (nearbyEl) nearbyEl.textContent = zoneCounts.nearby; - if (farEl) farEl.textContent = zoneCounts.far; - } + function updateProximityZones() { + zoneCounts = { immediate: 0, near: 0, far: 0 }; + + devices.forEach(device => { + const rssi = device.rssi_current; + if (rssi == null) return; + + if (rssi >= -50) zoneCounts.immediate++; + else if (rssi >= -70) zoneCounts.near++; + else zoneCounts.far++; + }); + + updateProximityZoneCounts(zoneCounts); + } // Currently selected device let selectedDeviceId = null; @@ -927,20 +930,22 @@ const BluetoothMode = (function() { if (statusText) statusText.textContent = scanning ? 'Scanning...' : 'Idle'; } - function resetStats() { - deviceStats = { - strong: 0, - medium: 0, - weak: 0, - trackers: [] - }; - updateVisualizationPanels(); - updateProximityZones(); - - // Clear radar - if (radarInitialized && typeof ProximityRadar !== 'undefined') { - ProximityRadar.clear(); - } + function resetStats() { + deviceStats = { + strong: 0, + medium: 0, + weak: 0, + trackers: [] + }; + visibleDeviceCount = 0; + updateVisualizationPanels(); + updateProximityZones(); + updateFilteredCount(); + + // Clear radar + if (radarInitialized && typeof ProximityRadar !== 'undefined') { + ProximityRadar.clear(); + } } function startEventStream() { @@ -1127,9 +1132,9 @@ const BluetoothMode = (function() { /** * Update visualization panels */ - function updateVisualizationPanels() { - // Signal Distribution - const total = devices.size || 1; + function updateVisualizationPanels() { + // Signal Distribution + const total = devices.size || 1; const strongBar = document.getElementById('btSignalStrong'); const mediumBar = document.getElementById('btSignalMedium'); const weakBar = document.getElementById('btSignalWeak'); @@ -1140,12 +1145,32 @@ const BluetoothMode = (function() { if (strongBar) strongBar.style.width = (deviceStats.strong / total * 100) + '%'; if (mediumBar) mediumBar.style.width = (deviceStats.medium / total * 100) + '%'; if (weakBar) weakBar.style.width = (deviceStats.weak / total * 100) + '%'; - if (strongCount) strongCount.textContent = deviceStats.strong; - if (mediumCount) mediumCount.textContent = deviceStats.medium; - if (weakCount) weakCount.textContent = deviceStats.weak; - - // Tracker Detection - Enhanced display with confidence and evidence - const trackerList = document.getElementById('btTrackerList'); + if (strongCount) strongCount.textContent = deviceStats.strong; + if (mediumCount) mediumCount.textContent = deviceStats.medium; + if (weakCount) weakCount.textContent = deviceStats.weak; + + // Device summary strip + const totalEl = document.getElementById('btSummaryTotal'); + const newEl = document.getElementById('btSummaryNew'); + const trackersEl = document.getElementById('btSummaryTrackers'); + const strongestEl = document.getElementById('btSummaryStrongest'); + if (totalEl || newEl || trackersEl || strongestEl) { + let newCount = 0; + let strongest = null; + devices.forEach(d => { + if (!d.in_baseline) newCount++; + if (d.rssi_current != null) { + strongest = strongest == null ? d.rssi_current : Math.max(strongest, d.rssi_current); + } + }); + if (totalEl) totalEl.textContent = devices.size; + if (newEl) newEl.textContent = newCount; + if (trackersEl) trackersEl.textContent = deviceStats.trackers.length; + if (strongestEl) strongestEl.textContent = strongest == null ? '--' : `${strongest} dBm`; + } + + // Tracker Detection - Enhanced display with confidence and evidence + const trackerList = document.getElementById('btTrackerList'); if (trackerList) { if (devices.size === 0) { trackerList.innerHTML = '
Start scanning to detect trackers
'; @@ -1214,27 +1239,25 @@ const BluetoothMode = (function() { updateFilteredCount(); } - function renderDevice(device) { - if (!deviceContainer) { - deviceContainer = document.getElementById('btDeviceListContent'); - if (!deviceContainer) return; - } + function renderDevice(device) { + if (!deviceContainer) { + deviceContainer = document.getElementById('btDeviceListContent'); + if (!deviceContainer) return; + } const escapedId = CSS.escape(device.device_id); const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]'); const cardHtml = createSimpleDeviceCard(device); - if (existingCard) { - existingCard.outerHTML = cardHtml; - } else { - deviceContainer.insertAdjacentHTML('afterbegin', cardHtml); - } - - // Re-apply filter after rendering - if (currentDeviceFilter !== 'all') { - applyDeviceFilter(); - } - } + if (existingCard) { + existingCard.outerHTML = cardHtml; + } else { + deviceContainer.insertAdjacentHTML('afterbegin', cardHtml); + } + + // Re-apply filter after rendering + applyDeviceFilter(); + } function createSimpleDeviceCard(device) { const protocol = device.protocol || 'ble'; @@ -1257,9 +1280,17 @@ const BluetoothMode = (function() { const displayName = device.name || formatDeviceId(device.address); const name = escapeHtml(displayName); const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown')); - const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : ''; - const seenCount = device.seen_count || 0; - const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'"); + const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : ''; + const seenCount = device.seen_count || 0; + const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'"); + const searchIndex = [ + displayName, + device.address, + device.manufacturer_name, + device.tracker_name, + device.tracker_type, + agentName + ].filter(Boolean).join(' ').toLowerCase(); // Protocol badge - compact const protoBadge = protocol === 'ble' @@ -1346,7 +1377,7 @@ const BluetoothMode = (function() { const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' : isTracker ? '#f97316' : rssiColor; - return '
' + + return '
' + '
' + '
' + protoBadge + @@ -1383,12 +1414,16 @@ const BluetoothMode = (function() { return '#ef4444'; } - function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = String(text); - return div.innerHTML; - } + function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = String(text); + return div.innerHTML; + } + + function escapeAttr(text) { + return escapeHtml(text).replace(/"/g, '"').replace(/'/g, '''); + } async function setBaseline() { try { diff --git a/templates/index.html b/templates/index.html index e3047c6..ce7e17c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -830,6 +830,7 @@
+
Manufacturer @@ -939,23 +940,23 @@
Proximity Radar
- - - - + + + +
-
-
- 0 -
Immediate
+
+
+ 0 +
Immediate
-
- 0 -
Near
+
+ 0 +
Near
-
- 0 -
Far
+
+ 0 +
Far
@@ -967,11 +968,33 @@
Bluetooth Devices
(0)
+
+
+ Total + 0 +
+
+ New + 0 +
+
+ Trackers + 0 +
+
+ Strongest + -- +
+
+
+ +
+
diff --git a/templates/partials/modes/bluetooth.html b/templates/partials/modes/bluetooth.html index 1232e6f..752b515 100644 --- a/templates/partials/modes/bluetooth.html +++ b/templates/partials/modes/bluetooth.html @@ -52,6 +52,7 @@
+
No baseline