From e3d9349d4b8aeb487b74e102ee86b702a66f2ba6 Mon Sep 17 00:00:00 2001 From: Smittix Date: Wed, 21 Jan 2026 17:12:48 +0000 Subject: [PATCH] Improve Bluetooth device card layout and modal - Remove Details dropdown from device cards for cleaner look - Add grid layout for device cards (responsive, auto-fill columns) - Enhanced modal with full device details: - Large RSSI display with sparkline - Signal statistics (median, min, max, confidence) - Device info grid (address, type, protocol, manufacturer) - Observation timeline (first/last seen, count, rate) - Service UUIDs list - Behavioral analysis heuristics - Copy JSON and Copy Address buttons in modal footer - Escape key closes modal - Responsive design for mobile Co-Authored-By: Claude Opus 4.5 --- static/css/components/device-cards.css | 270 +++++++++++++++++++++++++ static/js/components/device-card.js | 192 ++++++++++++++---- 2 files changed, 426 insertions(+), 36 deletions(-) diff --git a/static/css/components/device-cards.css b/static/css/components/device-cards.css index b69a1a0..4394a04 100644 --- a/static/css/components/device-cards.css +++ b/static/css/components/device-cards.css @@ -554,6 +554,276 @@ } } +/* ============================================ + DEVICE CARD GRID LAYOUT + ============================================ */ +.bt-device-list .wifi-device-list-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; + padding: 12px; +} + +.bt-device-list .device-card { + margin: 0; + height: fit-content; +} + +/* ============================================ + ENHANCED MODAL STYLES + ============================================ */ +.signal-details-modal-header .modal-header-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.signal-details-modal-subtitle { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: var(--text-dim, #666); +} + +.signal-details-modal-footer { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.signal-details-copy-addr-btn { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + padding: 8px 16px; + background: var(--bg-secondary, #252525); + border: 1px solid var(--border-color, #333); + border-radius: 4px; + color: var(--text-secondary, #888); + cursor: pointer; + transition: all 0.15s ease; +} + +.signal-details-copy-addr-btn:hover { + background: var(--bg-tertiary, #1a1a1a); + color: var(--text-primary, #e0e0e0); +} + +/* Modal Header Section */ +.modal-device-header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--border-color, #333); +} + +.modal-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +/* Modal Sections */ +.modal-section { + margin-bottom: 20px; +} + +.modal-section:last-child { + margin-bottom: 0; +} + +.modal-section-title { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-dim, #666); + margin-bottom: 12px; +} + +/* Signal Display */ +.modal-signal-display { + display: flex; + align-items: center; + gap: 24px; + padding: 16px; + background: var(--bg-secondary, #1a1a1a); + border-radius: 8px; + margin-bottom: 12px; +} + +.modal-rssi-large { + font-family: 'JetBrains Mono', monospace; + font-size: 36px; + font-weight: 700; + color: var(--accent-cyan, #00d4ff); + line-height: 1; +} + +.modal-rssi-large .rssi-unit { + font-size: 14px; + font-weight: 400; + color: var(--text-dim, #666); + margin-left: 4px; +} + +.modal-sparkline { + flex: 1; + display: flex; + justify-content: flex-end; +} + +/* Signal Stats Grid */ +.modal-signal-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} + +.modal-signal-stats .stat-item { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px; + background: var(--bg-secondary, #1a1a1a); + border-radius: 6px; + text-align: center; +} + +.modal-signal-stats .stat-label { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim, #666); + margin-bottom: 4px; +} + +.modal-signal-stats .stat-value { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + font-weight: 600; + color: var(--text-primary, #e0e0e0); +} + +/* Info Grid */ +.modal-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.modal-info-grid .info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: var(--bg-secondary, #1a1a1a); + border-radius: 6px; +} + +.modal-info-grid .info-label { + font-size: 11px; + color: var(--text-dim, #666); +} + +.modal-info-grid .info-value { + font-size: 12px; + font-weight: 500; + color: var(--text-primary, #e0e0e0); +} + +.modal-info-grid .info-value.mono { + font-family: 'JetBrains Mono', monospace; + color: var(--accent-cyan, #00d4ff); +} + +/* UUID List */ +.modal-uuid-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.modal-uuid { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + padding: 4px 8px; + background: var(--bg-secondary, #1a1a1a); + border: 1px solid var(--border-color, #333); + border-radius: 4px; + color: var(--text-secondary, #888); +} + +/* Heuristics Grid */ +.modal-heuristics-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; +} + +.heuristic-check { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: var(--bg-secondary, #1a1a1a); + border-radius: 6px; + border: 1px solid var(--border-color, #333); +} + +.heuristic-check.active { + background: rgba(34, 197, 94, 0.1); + border-color: rgba(34, 197, 94, 0.3); +} + +.heuristic-indicator { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + font-weight: 600; + color: var(--text-dim, #666); +} + +.heuristic-check.active .heuristic-indicator { + color: var(--accent-green, #22c55e); +} + +.heuristic-label { + font-size: 11px; + text-transform: capitalize; + color: var(--text-secondary, #888); +} + +/* ============================================ + RESPONSIVE MODAL + ============================================ */ +@media (max-width: 600px) { + .modal-signal-stats { + grid-template-columns: repeat(2, 1fr); + } + + .modal-info-grid { + grid-template-columns: 1fr; + } + + .modal-signal-display { + flex-direction: column; + align-items: flex-start; + gap: 16px; + } + + .modal-sparkline { + width: 100%; + justify-content: center; + } + + .modal-device-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } +} + /* ============================================ DARK MODE OVERRIDES (if needed) ============================================ */ diff --git a/static/js/components/device-card.js b/static/js/components/device-card.js index cd46946..373340a 100644 --- a/static/js/components/device-card.js +++ b/static/js/components/device-card.js @@ -220,32 +220,10 @@ const DeviceCard = (function() { - -
-
- ${createAdvancedPanel(device)} -
-
`; - // Make card clickable - card.addEventListener('click', (e) => { - if (e.target.closest('button') || e.target.closest('.signal-advanced-toggle')) { - return; - } + // Make card clickable - opens modal with full details + card.addEventListener('click', () => { showDeviceDetails(device); }); @@ -361,12 +339,16 @@ const DeviceCard = (function() {
- +
`; @@ -379,23 +361,161 @@ const DeviceCard = (function() { modal.querySelector('.signal-details-modal-close').addEventListener('click', () => { modal.classList.remove('show'); }); - modal.querySelector('.signal-details-copy-btn').addEventListener('click', () => { - navigator.clipboard.writeText(JSON.stringify(device, null, 2)).then(() => { - if (typeof SignalCards !== 'undefined') { - SignalCards.showToast('Device info copied to clipboard'); - } - }); + // Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('show')) { + modal.classList.remove('show'); + } }); } - // Populate modal - modal.querySelector('.signal-details-modal-title').textContent = - device.name || device.address; - modal.querySelector('.signal-details-modal-body').innerHTML = createAdvancedPanel(device); + // Update copy button handlers with current device + const copyBtn = modal.querySelector('.signal-details-copy-btn'); + const copyAddrBtn = modal.querySelector('.signal-details-copy-addr-btn'); + + copyBtn.onclick = () => { + navigator.clipboard.writeText(JSON.stringify(device, null, 2)).then(() => { + copyBtn.textContent = 'Copied!'; + setTimeout(() => { copyBtn.textContent = 'Copy JSON'; }, 1500); + }); + }; + + copyAddrBtn.onclick = () => { + navigator.clipboard.writeText(device.address).then(() => { + copyAddrBtn.textContent = 'Copied!'; + setTimeout(() => { copyAddrBtn.textContent = 'Copy Address'; }, 1500); + }); + }; + + // Populate modal header + modal.querySelector('.signal-details-modal-title').textContent = device.name || 'Unknown Device'; + modal.querySelector('.signal-details-modal-subtitle').textContent = device.address; + + // Populate modal body with enhanced content + modal.querySelector('.signal-details-modal-body').innerHTML = createModalContent(device); modal.classList.add('show'); } + /** + * Create enhanced modal content + */ + function createModalContent(device) { + const protocolLabel = device.protocol === 'ble' ? 'Bluetooth Low Energy' : 'Classic Bluetooth'; + const sparkline = createSparkline(device.rssi_history, { width: 120, height: 30 }); + + return ` + + + + + + + + + ${device.service_uuids && device.service_uuids.length > 0 ? ` + + ` : ''} + + ${device.heuristics ? ` + + ` : ''} + `; + } + /** * Toggle advanced panel */