diff --git a/static/css/components/function-strip.css b/static/css/components/function-strip.css new file mode 100644 index 0000000..458c42f --- /dev/null +++ b/static/css/components/function-strip.css @@ -0,0 +1,349 @@ +/* Function Strip (Action Bar) - Shared across modes + * Based on APRS strip pattern, reusable for Pager, Sensor, Bluetooth, WiFi, TSCM, etc. + */ + +.function-strip { + background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 6px 12px; + margin-bottom: 10px; + overflow-x: auto; +} + +.function-strip-inner { + display: flex; + align-items: center; + gap: 8px; + min-width: max-content; +} + +/* Stats */ +.function-strip .strip-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 10px; + background: rgba(74, 158, 255, 0.05); + border: 1px solid rgba(74, 158, 255, 0.15); + border-radius: 4px; + min-width: 55px; +} + +.function-strip .strip-stat:hover { + background: rgba(74, 158, 255, 0.1); + border-color: rgba(74, 158, 255, 0.3); +} + +.function-strip .strip-value { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + font-weight: 600; + color: var(--accent-cyan); + line-height: 1.2; +} + +.function-strip .strip-label { + font-size: 8px; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 1px; +} + +.function-strip .strip-divider { + width: 1px; + height: 28px; + background: var(--border-color); + margin: 0 4px; +} + +/* Signal stat coloring */ +.function-strip .signal-stat.good .strip-value { color: var(--accent-green); } +.function-strip .signal-stat.warning .strip-value { color: var(--accent-yellow); } +.function-strip .signal-stat.poor .strip-value { color: var(--accent-red); } + +/* Controls */ +.function-strip .strip-control { + display: flex; + align-items: center; + gap: 4px; +} + +.function-strip .strip-select { + background: rgba(0,0,0,0.3); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + cursor: pointer; +} + +.function-strip .strip-select:hover { + border-color: var(--accent-cyan); +} + +.function-strip .strip-select:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.function-strip .strip-input-label { + font-size: 9px; + color: var(--text-muted); + font-weight: 600; +} + +.function-strip .strip-input { + background: rgba(0,0,0,0.3); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 4px 6px; + border-radius: 4px; + font-size: 10px; + width: 50px; + text-align: center; +} + +.function-strip .strip-input:hover, +.function-strip .strip-input:focus { + border-color: var(--accent-cyan); + outline: none; +} + +.function-strip .strip-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Wider input for frequency values */ +.function-strip .strip-input.wide { + width: 70px; +} + +/* Tool Status Indicators */ +.function-strip .strip-tools { + display: flex; + gap: 4px; +} + +.function-strip .strip-tool { + font-size: 9px; + font-weight: 600; + padding: 3px 6px; + border-radius: 3px; + background: rgba(255, 59, 48, 0.2); + color: var(--accent-red); + border: 1px solid rgba(255, 59, 48, 0.3); +} + +.function-strip .strip-tool.ok { + background: rgba(0, 255, 136, 0.1); + color: var(--accent-green); + border-color: rgba(0, 255, 136, 0.3); +} + +.function-strip .strip-tool.warn { + background: rgba(255, 193, 7, 0.2); + color: var(--accent-yellow); + border-color: rgba(255, 193, 7, 0.3); +} + +/* Buttons */ +.function-strip .strip-btn { + background: rgba(74, 158, 255, 0.1); + border: 1px solid rgba(74, 158, 255, 0.2); + color: var(--text-primary); + padding: 6px 12px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.function-strip .strip-btn:hover:not(:disabled) { + background: rgba(74, 158, 255, 0.2); + border-color: rgba(74, 158, 255, 0.4); +} + +.function-strip .strip-btn.primary { + background: linear-gradient(135deg, var(--accent-green) 0%, #10b981 100%); + border: none; + color: #000; +} + +.function-strip .strip-btn.primary:hover:not(:disabled) { + filter: brightness(1.1); +} + +.function-strip .strip-btn.stop { + background: linear-gradient(135deg, var(--accent-red) 0%, #dc2626 100%); + border: none; + color: #fff; +} + +.function-strip .strip-btn.stop:hover:not(:disabled) { + filter: brightness(1.1); +} + +.function-strip .strip-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Status indicator */ +.function-strip .strip-status { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: rgba(0,0,0,0.2); + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: var(--text-secondary); +} + +.function-strip .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); +} + +.function-strip .status-dot.inactive { + background: var(--text-muted); +} + +.function-strip .status-dot.active, +.function-strip .status-dot.listening, +.function-strip .status-dot.scanning, +.function-strip .status-dot.decoding { + background: var(--accent-cyan); + animation: strip-pulse 1.5s ease-in-out infinite; +} + +.function-strip .status-dot.tracking, +.function-strip .status-dot.receiving { + background: var(--accent-green); + animation: strip-pulse 1.5s ease-in-out infinite; +} + +.function-strip .status-dot.sweeping { + background: var(--accent-orange); + animation: strip-pulse 1s ease-in-out infinite; +} + +.function-strip .status-dot.error { + background: var(--accent-red); +} + +@keyframes strip-pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 4px 2px currentColor; } + 50% { opacity: 0.6; box-shadow: none; } +} + +/* Time display */ +.function-strip .strip-time { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: var(--text-muted); + padding: 4px 8px; + background: rgba(0,0,0,0.2); + border-radius: 4px; + white-space: nowrap; +} + +/* Mode-specific accent colors */ +.function-strip.pager-strip .strip-stat { + background: rgba(255, 193, 7, 0.05); + border-color: rgba(255, 193, 7, 0.15); +} +.function-strip.pager-strip .strip-stat:hover { + background: rgba(255, 193, 7, 0.1); + border-color: rgba(255, 193, 7, 0.3); +} +.function-strip.pager-strip .strip-value { + color: var(--accent-yellow); +} + +.function-strip.sensor-strip .strip-stat { + background: rgba(0, 255, 136, 0.05); + border-color: rgba(0, 255, 136, 0.15); +} +.function-strip.sensor-strip .strip-stat:hover { + background: rgba(0, 255, 136, 0.1); + border-color: rgba(0, 255, 136, 0.3); +} +.function-strip.sensor-strip .strip-value { + color: var(--accent-green); +} + +.function-strip.bt-strip .strip-stat { + background: rgba(0, 122, 255, 0.05); + border-color: rgba(0, 122, 255, 0.15); +} +.function-strip.bt-strip .strip-stat:hover { + background: rgba(0, 122, 255, 0.1); + border-color: rgba(0, 122, 255, 0.3); +} +.function-strip.bt-strip .strip-value { + color: #0a84ff; +} + +.function-strip.wifi-strip .strip-stat { + background: rgba(255, 149, 0, 0.05); + border-color: rgba(255, 149, 0, 0.15); +} +.function-strip.wifi-strip .strip-stat:hover { + background: rgba(255, 149, 0, 0.1); + border-color: rgba(255, 149, 0, 0.3); +} +.function-strip.wifi-strip .strip-value { + color: var(--accent-orange); +} + +.function-strip.tscm-strip .strip-stat { + background: rgba(255, 59, 48, 0.05); + border-color: rgba(255, 59, 48, 0.15); +} +.function-strip.tscm-strip .strip-stat:hover { + background: rgba(255, 59, 48, 0.1); + border-color: rgba(255, 59, 48, 0.3); +} +.function-strip.tscm-strip .strip-value { + color: var(--accent-red); +} + +.function-strip.rtlamr-strip .strip-stat { + background: rgba(175, 82, 222, 0.05); + border-color: rgba(175, 82, 222, 0.15); +} +.function-strip.rtlamr-strip .strip-stat:hover { + background: rgba(175, 82, 222, 0.1); + border-color: rgba(175, 82, 222, 0.3); +} +.function-strip.rtlamr-strip .strip-value { + color: #af52de; +} + +.function-strip.listening-strip .strip-stat { + background: rgba(74, 158, 255, 0.05); + border-color: rgba(74, 158, 255, 0.15); +} +.function-strip.listening-strip .strip-stat:hover { + background: rgba(74, 158, 255, 0.1); + border-color: rgba(74, 158, 255, 0.3); +} +.function-strip.listening-strip .strip-value { + color: var(--accent-cyan); +} + +/* Threat-colored stats for TSCM */ +.function-strip .strip-stat.threat-high .strip-value { color: var(--accent-red); } +.function-strip .strip-stat.threat-review .strip-value { color: var(--accent-orange); } +.function-strip .strip-stat.threat-info .strip-value { color: var(--accent-cyan); } diff --git a/static/css/index.css b/static/css/index.css index 7268081..bb9952b 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -5991,13 +5991,15 @@ body::before { cursor: not-allowed; } -.radio-action-btn.scan { +.radio-action-btn.scan, +.radio-action-btn.listen { background: var(--accent-green); border-color: var(--accent-green); - color: #000; + color: #fff; } -.radio-action-btn.scan:hover:not(:disabled) { +.radio-action-btn.scan:hover:not(:disabled), +.radio-action-btn.listen:hover:not(:disabled) { box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); } diff --git a/templates/index.html b/templates/index.html index f6da5d5..60295e4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -49,6 +49,7 @@ +
@@ -535,23 +536,59 @@ @@ -1353,6 +1501,62 @@ + + + + + + + + + @@ -2044,6 +2430,8 @@ let isSensorRunning = false; let isWifiRunning = false; let isBtRunning = false; + let isAprsRunning = false; + let isTscmRunning = false; let currentMode = 'pager'; let msgCount = 0; let pocsagCount = 0; @@ -2324,6 +2712,25 @@ // Initialize dropdown nav active state updateDropdownActiveState(); + // Global strip time updater - updates all visible function strips + setInterval(function() { + const now = new Date(); + const utcStr = now.toUTCString().slice(17, 25) + ' UTC'; + + // Update all strip time elements + const stripTimeIds = [ + 'pagerStripTime', 'sensorStripTime', 'rtlamrStripTime', + 'btStripTime', 'wifiStripTime', 'tscmStripTime', 'listeningStripTime' + ]; + stripTimeIds.forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = utcStr; + }); + }, 1000); + + // Check tool availability for all strip modes + checkStripToolStatus(); + // Handle mode parameter from URL (e.g., /?mode=aprs) const urlParams = new URLSearchParams(window.location.search); const modeParam = urlParams.get('mode'); @@ -2520,6 +2927,14 @@ if (pagerTimelineContainer) pagerTimelineContainer.style.display = mode === 'pager' ? 'block' : 'none'; if (sensorTimelineContainer) sensorTimelineContainer.style.display = mode === 'sensor' ? 'block' : 'none'; + // Show/hide mode-specific function strips + const pagerStrip = document.getElementById('pagerStrip'); + const sensorStrip = document.getElementById('sensorStrip'); + const rtlamrStrip = document.getElementById('rtlamrStrip'); + if (pagerStrip) pagerStrip.style.display = mode === 'pager' ? 'block' : 'none'; + if (sensorStrip) sensorStrip.style.display = mode === 'sensor' ? 'block' : 'none'; + if (rtlamrStrip) rtlamrStrip.style.display = mode === 'rtlamr' ? 'block' : 'none'; + // Update output panel title based on mode const titles = { 'pager': 'Pager Decoder', @@ -2574,6 +2989,41 @@ const rtlDeviceSection = document.getElementById('rtlDeviceSection'); if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv') ? 'block' : 'none'; + // Always restore all moved sections back to their containers first + const sidebar = document.getElementById('mainSidebar'); + if (sidebar) { + // Restore Signal Source and SDR Device to expanded state + if (agentSection) agentSection.classList.remove('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.remove('collapsed'); + + // Helper function to restore sections to their mode container + function restoreSectionsToMode(modeId, selectors, titleMatch) { + const modeEl = document.getElementById(modeId); + if (!modeEl) return; + const movedSections = sidebar.querySelectorAll(':scope > .section:not(#agentSection):not(#rtlDeviceSection)'); + movedSections.forEach(section => { + const matchesSelector = selectors.some(sel => section.querySelector(sel)); + const matchesTitle = titleMatch && section.querySelector('h3')?.textContent?.includes(titleMatch); + if (matchesSelector || matchesTitle) { + modeEl.appendChild(section); + } + }); + } + + // Restore all mode sections to their containers + restoreSectionsToMode('listeningPostMode', ['#lpQuickStatus', '#bookmarkFreqInput', '#sidebarRecentSignals'], 'Spectrum Analyzer'); + restoreSectionsToMode('pagerMode', ['#proto_pocsag512', '#squelch', '#loggingEnabled', '#filterToneOnly'], 'Pager Decoding'); + restoreSectionsToMode('sensorMode', ['#sensorPpm', '#sensorLogging'], '433MHz Sensors'); + restoreSectionsToMode('rtlamrMode', ['#rtlamrPpm', '#rtlamrUnique', '#rtlamrLogging'], 'Utility Meter'); + restoreSectionsToMode('aprsMode', [], 'APRS Tracking'); + restoreSectionsToMode('spystationsMode', ['#filterTypeNumber', '#countryFilters', '#modeFilters', '#spyStatsNumber'], 'Spy Stations'); + restoreSectionsToMode('wifiMode', ['#wifiScanModeQuick', '#monitorStartBtn', '#wifiChannel', '#watchMacInput', '#targetBssid'], 'WiFi Scanning'); + restoreSectionsToMode('bluetoothMode', ['#btAdapterSelect', '#btTransport', '#btScanDuration'], 'Bluetooth Scanning'); + restoreSectionsToMode('tscmMode', ['#tscmWifiEnabled', '#tscmVerboseResults', '#tscmBaselineName'], 'TSCM Sweep'); + restoreSectionsToMode('satelliteMode', ['#satTrackingList', '#obsLat', '#obsLon'], 'Satellite Command'); + restoreSectionsToMode('sstvMode', ['#sstvFrequency'], 'ISS SSTV Decoder'); + } + // Toggle mode-specific tool status displays const toolStatusPager = document.getElementById('toolStatusPager'); const toolStatusSensor = document.getElementById('toolStatusSensor'); @@ -2603,9 +3053,46 @@ if (typeof WiFiMode !== 'undefined') { WiFiMode.init(); } + // WiFi: move sections to top in order: WiFi Scanning, Signal Source, then rest + const wifiModeEl = document.getElementById('wifiMode'); + const sidebar = document.getElementById('mainSidebar'); + if (wifiModeEl && sidebar && agentSection) { + const wifiSections = Array.from(wifiModeEl.querySelectorAll('.section')); + // Insert in reverse order so they end up in correct order + // First, move all wifi sections except first to top (in reverse) + for (let i = wifiSections.length - 1; i >= 1; i--) { + sidebar.insertBefore(wifiSections[i], sidebar.firstChild); + wifiSections[i].classList.add('collapsed'); + } + // Then insert Signal Source + sidebar.insertBefore(agentSection, sidebar.firstChild); + agentSection.classList.add('collapsed'); + // Finally insert WiFi Scanning at the very top (expanded) + if (wifiSections[0]) { + sidebar.insertBefore(wifiSections[0], sidebar.firstChild); + wifiSections[0].classList.remove('collapsed'); + } + } } else if (mode === 'bluetooth') { refreshBtInterfaces(); initBtRadar(); + // Bluetooth: move Bluetooth Scanning to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + + const btModeEl = document.getElementById('bluetoothMode'); + const sidebar = document.getElementById('mainSidebar'); + if (btModeEl && sidebar) { + const btSections = btModeEl.querySelectorAll('.section'); + // Move bluetooth sections to top of sidebar (before agentSection) + btSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Bluetooth Scanning expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } else if (mode === 'aprs') { checkAprsTools(); initAprsMap(); @@ -2613,16 +3100,159 @@ setTimeout(() => { if (aprsMap) aprsMap.invalidateSize(); }, 100); + // APRS: move APRS Tracking to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const aprsModeEl = document.getElementById('aprsMode'); + const sidebar = document.getElementById('mainSidebar'); + if (aprsModeEl && sidebar) { + const aprsSections = aprsModeEl.querySelectorAll('.section'); + // Move aprs sections to top of sidebar (before agentSection) + aprsSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // APRS Tracking expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } else if (mode === 'satellite') { initPolarPlot(); initSatelliteList(); + // Satellite: move Satellite Command to top, collapse others + const satelliteModeEl = document.getElementById('satelliteMode'); + const sidebar = document.getElementById('mainSidebar'); + if (satelliteModeEl && sidebar) { + const satSections = satelliteModeEl.querySelectorAll('.section'); + // Move satellite sections to top of sidebar (before agentSection) + satSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Satellite Command expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } + } else if (mode === 'tscm') { + // TSCM: move TSCM Sweep to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + + const tscmModeEl = document.getElementById('tscmMode'); + const sidebar = document.getElementById('mainSidebar'); + if (tscmModeEl && sidebar) { + const tscmSections = tscmModeEl.querySelectorAll('.section'); + // Move tscm sections to top of sidebar (before agentSection) + tscmSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // TSCM Sweep expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } else if (mode === 'listening') { // Check for incoming tune requests from Spy Stations if (typeof checkIncomingTuneRequest === 'function') { checkIncomingTuneRequest(); } + // Listening Post: move Spectrum Analyzer to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const lpMode = document.getElementById('listeningPostMode'); + const sidebar = document.getElementById('mainSidebar'); + if (lpMode && sidebar) { + const lpSections = lpMode.querySelectorAll('.section'); + // Move listening post sections to top of sidebar (before agentSection) + lpSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Spectrum Analyzer expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } + } else if (mode === 'pager') { + // Pager: move Pager Decoding to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const pagerModeEl = document.getElementById('pagerMode'); + const sidebar = document.getElementById('mainSidebar'); + if (pagerModeEl && sidebar) { + const pagerSections = pagerModeEl.querySelectorAll('.section'); + // Move pager sections to top of sidebar (before agentSection) + pagerSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Pager Decoding expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } + } else if (mode === 'sensor') { + // Sensor: move 433MHz Sensors to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const sensorModeEl = document.getElementById('sensorMode'); + const sidebar = document.getElementById('mainSidebar'); + if (sensorModeEl && sidebar) { + const sensorSections = sensorModeEl.querySelectorAll('.section'); + // Move sensor sections to top of sidebar (before agentSection) + sensorSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // 433MHz Sensors expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } + } else if (mode === 'rtlamr') { + // RTLAMR: move Utility Meter Reading to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const rtlamrModeEl = document.getElementById('rtlamrMode'); + const sidebar = document.getElementById('mainSidebar'); + if (rtlamrModeEl && sidebar) { + const rtlamrSections = rtlamrModeEl.querySelectorAll('.section'); + // Move rtlamr sections to top of sidebar (before agentSection) + rtlamrSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Utility Meter Reading expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } else if (mode === 'spystations') { SpyStations.init(); + // Spy Stations: move Spy Stations to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + + const spystationsModeEl = document.getElementById('spystationsMode'); + const sidebar = document.getElementById('mainSidebar'); + if (spystationsModeEl && sidebar) { + const spySections = spystationsModeEl.querySelectorAll('.section'); + // Move spy stations sections to top of sidebar (before agentSection) + spySections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // Spy Stations expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } else if (mode === 'meshtastic') { Meshtastic.init(); // Fix map sizing after container becomes visible @@ -2631,6 +3261,24 @@ }, 100); } else if (mode === 'sstv') { SSTV.init(); + // SSTV: move ISS SSTV Decoder to top, collapse others + if (agentSection) agentSection.classList.add('collapsed'); + if (rtlDeviceSection) rtlDeviceSection.classList.add('collapsed'); + + const sstvModeEl = document.getElementById('sstvMode'); + const sidebar = document.getElementById('mainSidebar'); + if (sstvModeEl && sidebar) { + const sstvSections = sstvModeEl.querySelectorAll('.section'); + // Move sstv sections to top of sidebar (before agentSection) + sstvSections.forEach((section, idx) => { + sidebar.insertBefore(section, agentSection); + if (idx === 0) { + section.classList.remove('collapsed'); // ISS SSTV Decoder expanded + } else { + section.classList.add('collapsed'); // Others collapsed + } + }); + } } } @@ -2845,10 +3493,68 @@ function setSensorRunning(running) { isSensorRunning = running; - document.getElementById('statusDot').classList.toggle('running', running); - document.getElementById('statusText').textContent = running ? 'Listening...' : 'Idle'; - document.getElementById('startSensorBtn').style.display = running ? 'none' : 'block'; - document.getElementById('stopSensorBtn').style.display = running ? 'block' : 'none'; + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + const startSensorBtn = document.getElementById('startSensorBtn'); + const stopSensorBtn = document.getElementById('stopSensorBtn'); + + if (statusDot) statusDot.classList.toggle('running', running); + if (statusText) statusText.textContent = running ? 'Listening...' : 'Idle'; + if (startSensorBtn) startSensorBtn.style.display = running ? 'none' : 'block'; + if (stopSensorBtn) stopSensorBtn.style.display = running ? 'block' : 'none'; + + // Update sensor strip + const sensorStripDot = document.getElementById('sensorStripDot'); + const sensorStripStatus = document.getElementById('sensorStripStatus'); + const sensorStripStartBtn = document.getElementById('sensorStripStartBtn'); + const sensorStripStopBtn = document.getElementById('sensorStripStopBtn'); + const sensorStripPreset = document.getElementById('sensorStripPreset'); + const sensorStripGain = document.getElementById('sensorStripGain'); + + if (sensorStripDot) sensorStripDot.className = 'status-dot ' + (running ? 'listening' : 'inactive'); + if (sensorStripStatus) { + sensorStripStatus.textContent = running ? 'LISTENING' : 'STANDBY'; + sensorStripStatus.style.color = running ? 'var(--accent-green)' : ''; + } + if (sensorStripStartBtn) sensorStripStartBtn.style.display = running ? 'none' : 'inline-block'; + if (sensorStripStopBtn) sensorStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (sensorStripPreset) sensorStripPreset.disabled = running; + if (sensorStripGain) sensorStripGain.disabled = running; + } + + // Sensor strip functions + function sensorPresetChanged() { + const preset = document.getElementById('sensorStripPreset').value; + const customControl = document.getElementById('sensorStripCustomFreqControl'); + if (preset === 'custom') { + customControl.style.display = 'flex'; + } else { + customControl.style.display = 'none'; + document.getElementById('sensorFrequency').value = preset; + document.getElementById('sensorStripFreq').textContent = preset; + } + } + + function startSensorDecodingFromStrip() { + // Sync values from strip to hidden inputs + const preset = document.getElementById('sensorStripPreset').value; + const freq = preset === 'custom' ? + document.getElementById('sensorStripCustomFreq').value : + preset; + const gain = document.getElementById('sensorStripGain').value; + + document.getElementById('sensorFrequency').value = freq; + document.getElementById('sensorGain').value = gain; + document.getElementById('sensorStripFreq').textContent = freq; + + startSensorDecoding(); + } + + function updateSensorStrip(sensorCount, readingCount) { + const sensorsEl = document.getElementById('sensorStripSensors'); + const readingsEl = document.getElementById('sensorStripReadings'); + if (sensorsEl) sensorsEl.textContent = sensorCount; + if (readingsEl) readingsEl.textContent = readingCount; } function startSensorStream() { @@ -3088,18 +3794,81 @@ function setRtlamrRunning(running) { isRtlamrRunning = running; - document.getElementById('statusDot').classList.toggle('running', running); - document.getElementById('statusText').textContent = running ? 'Listening...' : 'Idle'; - document.getElementById('startRtlamrBtn').style.display = running ? 'none' : 'block'; - document.getElementById('stopRtlamrBtn').style.display = running ? 'block' : 'none'; - + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + const startRtlamrBtn = document.getElementById('startRtlamrBtn'); + const stopRtlamrBtn = document.getElementById('stopRtlamrBtn'); + const activeModeIndicator = document.getElementById('activeModeIndicator'); + const rtlamrFrequency = document.getElementById('rtlamrFrequency'); + + if (statusDot) statusDot.classList.toggle('running', running); + if (statusText) statusText.textContent = running ? 'Listening...' : 'Idle'; + if (startRtlamrBtn) startRtlamrBtn.style.display = running ? 'none' : 'block'; + if (stopRtlamrBtn) stopRtlamrBtn.style.display = running ? 'block' : 'none'; + // Update mode indicator with frequency - if (running) { - const freq = document.getElementById('rtlamrFrequency').value; - document.getElementById('activeModeIndicator').innerHTML = 'METERS @ ' + freq + ' MHz'; - } else { - document.getElementById('activeModeIndicator').innerHTML = 'METERS'; + if (activeModeIndicator) { + if (running && rtlamrFrequency) { + activeModeIndicator.innerHTML = 'METERS @ ' + rtlamrFrequency.value + ' MHz'; + } else { + activeModeIndicator.innerHTML = 'METERS'; + } } + + // Update RTLAMR strip + const rtlamrStripDot = document.getElementById('rtlamrStripDot'); + const rtlamrStripStatus = document.getElementById('rtlamrStripStatus'); + const rtlamrStripStartBtn = document.getElementById('rtlamrStripStartBtn'); + const rtlamrStripStopBtn = document.getElementById('rtlamrStripStopBtn'); + const rtlamrStripPreset = document.getElementById('rtlamrStripPreset'); + const rtlamrStripGain = document.getElementById('rtlamrStripGain'); + + if (rtlamrStripDot) rtlamrStripDot.className = 'status-dot ' + (running ? 'listening' : 'inactive'); + if (rtlamrStripStatus) { + rtlamrStripStatus.textContent = running ? 'LISTENING' : 'STANDBY'; + rtlamrStripStatus.style.color = running ? 'var(--accent-purple, #af52de)' : ''; + } + if (rtlamrStripStartBtn) rtlamrStripStartBtn.style.display = running ? 'none' : 'inline-block'; + if (rtlamrStripStopBtn) rtlamrStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (rtlamrStripPreset) rtlamrStripPreset.disabled = running; + if (rtlamrStripGain) rtlamrStripGain.disabled = running; + } + + // RTLAMR strip functions + function rtlamrPresetChanged() { + const preset = document.getElementById('rtlamrStripPreset').value; + const customControl = document.getElementById('rtlamrStripCustomFreqControl'); + if (preset === 'custom') { + customControl.style.display = 'flex'; + } else { + customControl.style.display = 'none'; + document.getElementById('rtlamrFrequency').value = preset; + document.getElementById('rtlamrStripFreq').textContent = preset; + } + } + + function startRtlamrDecodingFromStrip() { + // Sync values from strip to hidden inputs + const preset = document.getElementById('rtlamrStripPreset').value; + const freq = preset === 'custom' ? + document.getElementById('rtlamrStripCustomFreq').value : + preset; + const gain = document.getElementById('rtlamrStripGain').value; + const msgType = document.getElementById('rtlamrStripMsgType').value; + + document.getElementById('rtlamrFrequency').value = freq; + document.getElementById('rtlamrGain').value = gain; + document.getElementById('rtlamrMsgType').value = msgType; + document.getElementById('rtlamrStripFreq').textContent = freq; + + startRtlamrDecoding(); + } + + function updateRtlamrStrip(meterCount, readingCount) { + const metersEl = document.getElementById('rtlamrStripMeters'); + const readingsEl = document.getElementById('rtlamrStripReadings'); + if (metersEl) metersEl.textContent = meterCount; + if (readingsEl) readingsEl.textContent = readingCount; } function startRtlamrStream(isAgentMode = false) { @@ -3399,6 +4168,7 @@ function renderPresets() { const presets = loadPresets(); const container = document.getElementById('presetButtons'); + if (!container) return; // Preset buttons not in UI container.innerHTML = presets.map(freq => `` ).join(''); @@ -3406,6 +4176,7 @@ function addPreset() { const input = document.getElementById('newPresetFreq'); + if (!input) return; // Preset UI not present const freq = input.value.trim(); if (!freq || isNaN(parseFloat(freq))) { alert('Please enter a valid frequency'); @@ -3850,10 +4621,70 @@ function setRunning(running) { isRunning = running; - document.getElementById('statusDot').classList.toggle('running', running); - document.getElementById('statusText').textContent = running ? 'Decoding...' : 'Idle'; - document.getElementById('startBtn').style.display = running ? 'none' : 'block'; - document.getElementById('stopBtn').style.display = running ? 'block' : 'none'; + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + const startBtn = document.getElementById('startBtn'); + const stopBtn = document.getElementById('stopBtn'); + + if (statusDot) statusDot.classList.toggle('running', running); + if (statusText) statusText.textContent = running ? 'Decoding...' : 'Idle'; + if (startBtn) startBtn.style.display = running ? 'none' : 'block'; + if (stopBtn) stopBtn.style.display = running ? 'block' : 'none'; + + // Update pager strip + const pagerStripDot = document.getElementById('pagerStripDot'); + const pagerStripStatus = document.getElementById('pagerStripStatus'); + const pagerStripStartBtn = document.getElementById('pagerStripStartBtn'); + const pagerStripStopBtn = document.getElementById('pagerStripStopBtn'); + const pagerStripPreset = document.getElementById('pagerStripPreset'); + const pagerStripGain = document.getElementById('pagerStripGain'); + + if (pagerStripDot) pagerStripDot.className = 'status-dot ' + (running ? 'decoding' : 'inactive'); + if (pagerStripStatus) { + pagerStripStatus.textContent = running ? 'DECODING' : 'STANDBY'; + pagerStripStatus.style.color = running ? 'var(--accent-cyan)' : ''; + } + if (pagerStripStartBtn) pagerStripStartBtn.style.display = running ? 'none' : 'inline-block'; + if (pagerStripStopBtn) pagerStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (pagerStripPreset) pagerStripPreset.disabled = running; + if (pagerStripGain) pagerStripGain.disabled = running; + } + + // Pager strip functions + function pagerPresetChanged() { + const preset = document.getElementById('pagerStripPreset').value; + const customControl = document.getElementById('pagerStripCustomFreqControl'); + if (preset === 'custom') { + customControl.style.display = 'flex'; + } else { + customControl.style.display = 'none'; + document.getElementById('frequency').value = preset; + document.getElementById('pagerStripFreq').textContent = preset; + } + } + + function startDecodingFromStrip() { + // Sync values from strip to hidden inputs + const preset = document.getElementById('pagerStripPreset').value; + const freq = preset === 'custom' ? + document.getElementById('pagerStripCustomFreq').value : + preset; + const gain = document.getElementById('pagerStripGain').value; + + document.getElementById('frequency').value = freq; + document.getElementById('gain').value = gain; + document.getElementById('pagerStripFreq').textContent = freq; + + startDecoding(); + } + + function updatePagerStrip(messageCount, pocsagCount, flexCount) { + const msgEl = document.getElementById('pagerStripMessages'); + const pocsagEl = document.getElementById('pagerStripPocsag'); + const flexEl = document.getElementById('pagerStripFlex'); + if (msgEl) msgEl.textContent = messageCount; + if (pocsagEl) pocsagEl.textContent = pocsagCount; + if (flexEl) flexEl.textContent = flexCount; } function startStream(isAgentMode = false) { @@ -5581,10 +6412,60 @@ function setWifiRunning(running) { isWifiRunning = running; - document.getElementById('statusDot').classList.toggle('running', running); - document.getElementById('statusText').textContent = running ? 'Scanning...' : 'Idle'; - document.getElementById('startWifiBtn').style.display = running ? 'none' : 'block'; - document.getElementById('stopWifiBtn').style.display = running ? 'block' : 'none'; + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + const startWifiBtn = document.getElementById('startWifiBtn'); + const stopWifiBtn = document.getElementById('stopWifiBtn'); + + if (statusDot) statusDot.classList.toggle('running', running); + if (statusText) statusText.textContent = running ? 'Scanning...' : 'Idle'; + if (startWifiBtn) startWifiBtn.style.display = running ? 'none' : 'block'; + if (stopWifiBtn) stopWifiBtn.style.display = running ? 'block' : 'none'; + + // Update WiFi strip + const wifiStripDot = document.getElementById('wifiStripDot'); + const wifiStripStatus = document.getElementById('wifiStripStatus'); + const wifiStripQuickBtn = document.getElementById('wifiStripQuickBtn'); + const wifiStripDeepBtn = document.getElementById('wifiStripDeepBtn'); + const wifiStripStopBtn = document.getElementById('wifiStripStopBtn'); + const wifiStripInterface = document.getElementById('wifiStripInterface'); + const wifiStripBand = document.getElementById('wifiStripBand'); + + if (wifiStripDot) wifiStripDot.className = 'status-dot ' + (running ? 'scanning' : 'inactive'); + if (wifiStripStatus) { + wifiStripStatus.textContent = running ? 'SCANNING' : 'STANDBY'; + wifiStripStatus.style.color = running ? 'var(--accent-orange)' : ''; + } + if (wifiStripQuickBtn) wifiStripQuickBtn.style.display = running ? 'none' : 'inline-block'; + if (wifiStripDeepBtn) wifiStripDeepBtn.style.display = running ? 'none' : 'inline-block'; + if (wifiStripStopBtn) wifiStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (wifiStripInterface) wifiStripInterface.disabled = running; + if (wifiStripBand) wifiStripBand.disabled = running; + } + + // WiFi strip functions + function updateWifiStrip(apCount, clientCount, hiddenCount) { + const apsEl = document.getElementById('wifiStripAPs'); + const clientsEl = document.getElementById('wifiStripClients'); + const hiddenEl = document.getElementById('wifiStripHidden'); + + if (apsEl) apsEl.textContent = apCount; + if (clientsEl) clientsEl.textContent = clientCount; + if (hiddenEl) hiddenEl.textContent = hiddenCount; + } + + // Sync WiFi strip interface dropdown + function syncWifiStripInterfaces(interfaces) { + const stripSelect = document.getElementById('wifiStripInterface'); + if (!stripSelect) return; + + stripSelect.innerHTML = ''; + interfaces.forEach(iface => { + const opt = document.createElement('option'); + opt.value = iface.name || iface; + opt.textContent = iface.name || iface; + stripSelect.appendChild(opt); + }); } // Batching state for WiFi updates @@ -6859,10 +7740,84 @@ function setBtRunning(running) { isBtRunning = running; - document.getElementById('statusDot').classList.toggle('running', running); - document.getElementById('statusText').textContent = running ? 'Scanning...' : 'Idle'; - document.getElementById('startBtBtn').style.display = running ? 'none' : 'block'; - document.getElementById('stopBtBtn').style.display = running ? 'block' : 'none'; + const statusDot = document.getElementById('statusDot'); + const statusText = document.getElementById('statusText'); + const startBtBtn = document.getElementById('startBtBtn'); + const stopBtBtn = document.getElementById('stopBtBtn'); + + if (statusDot) statusDot.classList.toggle('running', running); + if (statusText) statusText.textContent = running ? 'Scanning...' : 'Idle'; + if (startBtBtn) startBtBtn.style.display = running ? 'none' : 'block'; + if (stopBtBtn) stopBtBtn.style.display = running ? 'block' : 'none'; + + // Update Bluetooth strip + const btStripDot = document.getElementById('btStripDot'); + const btStripStatus = document.getElementById('btStripStatus'); + const btStripStartBtn = document.getElementById('btStripStartBtn'); + const btStripStopBtn = document.getElementById('btStripStopBtn'); + const btStripAdapter = document.getElementById('btStripAdapter'); + const btStripTransport = document.getElementById('btStripTransport'); + + if (btStripDot) btStripDot.className = 'status-dot ' + (running ? 'scanning' : 'inactive'); + if (btStripStatus) { + btStripStatus.textContent = running ? 'SCANNING' : 'STANDBY'; + btStripStatus.style.color = running ? '#0a84ff' : ''; + } + if (btStripStartBtn) btStripStartBtn.style.display = running ? 'none' : 'inline-block'; + if (btStripStopBtn) btStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (btStripAdapter) btStripAdapter.disabled = running; + if (btStripTransport) btStripTransport.disabled = running; + } + + // Bluetooth strip functions + function btStartScanFromStrip() { + // Sync values from strip to hidden inputs + const adapter = document.getElementById('btStripAdapter').value; + const transport = document.getElementById('btStripTransport').value; + + const adapterSelect = document.getElementById('btAdapterSelect'); + const transportSelect = document.getElementById('btTransport'); + if (adapterSelect) adapterSelect.value = adapter; + if (transportSelect) transportSelect.value = transport; + + btStartScan(); + } + + function updateBtStrip(deviceCount, trackerCount, strongestRssi) { + const devicesEl = document.getElementById('btStripDevices'); + const trackersEl = document.getElementById('btStripTrackers'); + const rssiEl = document.getElementById('btStripRssi'); + const rssiStat = document.getElementById('btStripRssiStat'); + + if (devicesEl) devicesEl.textContent = deviceCount; + if (trackersEl) trackersEl.textContent = trackerCount; + if (rssiEl) rssiEl.textContent = strongestRssi !== null ? strongestRssi : '--'; + + // Color code RSSI stat + if (rssiStat && strongestRssi !== null) { + rssiStat.classList.remove('good', 'warning', 'poor'); + if (strongestRssi >= -50) { + rssiStat.classList.add('good'); + } else if (strongestRssi >= -70) { + rssiStat.classList.add('warning'); + } else { + rssiStat.classList.add('poor'); + } + } + } + + // Sync Bluetooth strip adapter dropdown when adapters are detected + function syncBtStripAdapters(adapters) { + const stripSelect = document.getElementById('btStripAdapter'); + if (!stripSelect) return; + + stripSelect.innerHTML = ''; + adapters.forEach((adapter, idx) => { + const opt = document.createElement('option'); + opt.value = adapter.path || adapter.id || idx; + opt.textContent = adapter.name || adapter.address || `Adapter ${idx}`; + stripSelect.appendChild(opt); + }); } // Batching state for Bluetooth updates @@ -7595,7 +8550,6 @@ let aprsMap = null; let aprsMarkers = {}; let aprsEventSource = null; - let isAprsRunning = false; let aprsPacketCount = 0; let aprsStationCount = 0; let aprsMeterLastUpdate = 0; @@ -7708,6 +8662,64 @@ }); } + // Check tool availability for all function strips + // Uses template data for pager/sensor/rtlamr, API calls only for endpoints that exist + function checkStripToolStatus() { + // Pager tools - use template data (no API call needed) + const pagerRtl = document.getElementById('pagerStripRtl'); + const pagerMm = document.getElementById('pagerStripMm'); + if (pagerRtl) { + pagerRtl.className = 'strip-tool' + ({{ 'true' if tools.rtl_fm else 'false' }} ? ' ok' : ''); + pagerRtl.title = 'rtl_fm: ' + ({{ 'true' if tools.rtl_fm else 'false' }} ? 'OK' : 'Missing'); + } + if (pagerMm) { + pagerMm.className = 'strip-tool' + ({{ 'true' if tools.multimon else 'false' }} ? ' ok' : ''); + pagerMm.title = 'multimon-ng: ' + ({{ 'true' if tools.multimon else 'false' }} ? 'OK' : 'Missing'); + } + + // Sensor tools - use template data + const sensorRtl = document.getElementById('sensorStripRtl'); + if (sensorRtl) { + sensorRtl.className = 'strip-tool' + ({{ 'true' if tools.rtl_433 else 'false' }} ? ' ok' : ''); + sensorRtl.title = 'rtl_433: ' + ({{ 'true' if tools.rtl_433 else 'false' }} ? 'OK' : 'Missing'); + } + + // RTLAMR tools - use template data + const rtlamrRtl = document.getElementById('rtlamrStripRtl'); + if (rtlamrRtl) { + rtlamrRtl.className = 'strip-tool' + ({{ 'true' if tools.rtl_fm else 'false' }} ? ' ok' : ''); + rtlamrRtl.title = 'rtl_tcp: ' + ({{ 'true' if tools.rtl_fm else 'false' }} ? 'OK' : 'Missing'); + } + + // Bluetooth - check capabilities via existing API + fetch('/bluetooth/capabilities') + .then(r => r.json()) + .then(data => { + const bleEl = document.getElementById('btStripBleak'); + if (bleEl) { + const hasBackend = data.has_bleak || data.has_bluez || data.available; + bleEl.className = 'strip-tool' + (hasBackend ? ' ok' : ''); + bleEl.title = 'BLE: ' + (hasBackend ? 'OK' : 'No backend'); + } + }) + .catch(() => {}); + + // Listening Post SDR - check via listening tools API + fetch('/listening/tools') + .then(r => r.json()) + .then(data => { + const sdrEl = document.getElementById('listeningStripSdr'); + if (sdrEl) { + sdrEl.className = 'strip-tool' + (data.rtl_fm ? ' ok' : ''); + sdrEl.title = 'SDR: ' + (data.rtl_fm ? 'OK' : 'Not detected'); + } + }) + .catch(() => {}); + + // Check APRS tools (this endpoint exists) + checkAprsTools(); + } + function initAprsMap() { if (aprsMap) return; @@ -9197,7 +10209,6 @@ // ============================================ // TSCM (Counter-Surveillance) Functions // ============================================ - let isTscmRunning = false; let tscmEventSource = null; let tscmThreats = []; let tscmWifiDevices = []; @@ -9400,6 +10411,7 @@ document.getElementById('startTscmBtn').style.display = 'none'; document.getElementById('stopTscmBtn').style.display = 'block'; document.getElementById('tscmProgress').style.display = 'flex'; + updateTscmStripRunning(true); // Clear and reset the signal timeline for new sweep SignalTimeline.clear(); @@ -9495,12 +10507,70 @@ document.getElementById('startTscmBtn').style.display = 'block'; document.getElementById('stopTscmBtn').style.display = 'none'; document.getElementById('tscmProgress').style.display = 'none'; + updateTscmStripRunning(false); // Show report button if we have any data const hasData = tscmWifiDevices.length > 0 || tscmBtDevices.length > 0 || tscmRfSignals.length > 0; document.getElementById('tscmReportBtn').style.display = hasData ? 'block' : 'none'; } + // TSCM strip functions + function updateTscmStripRunning(running) { + const tscmStripDot = document.getElementById('tscmStripDot'); + const tscmStripStatus = document.getElementById('tscmStripStatus'); + const tscmStripStartBtn = document.getElementById('tscmStripStartBtn'); + const tscmStripStopBtn = document.getElementById('tscmStripStopBtn'); + const tscmStripSweepType = document.getElementById('tscmStripSweepType'); + const tscmStripBaseline = document.getElementById('tscmStripBaseline'); + + if (tscmStripDot) tscmStripDot.className = 'status-dot ' + (running ? 'sweeping' : 'inactive'); + if (tscmStripStatus) { + tscmStripStatus.textContent = running ? 'SWEEPING' : 'STANDBY'; + tscmStripStatus.style.color = running ? 'var(--accent-orange)' : ''; + } + if (tscmStripStartBtn) tscmStripStartBtn.style.display = running ? 'none' : 'inline-block'; + if (tscmStripStopBtn) tscmStripStopBtn.style.display = running ? 'inline-block' : 'none'; + if (tscmStripSweepType) tscmStripSweepType.disabled = running; + if (tscmStripBaseline) tscmStripBaseline.disabled = running; + } + + function startTscmSweepFromStrip() { + // Sync values from strip to hidden selects + const sweepType = document.getElementById('tscmStripSweepType').value; + const baseline = document.getElementById('tscmStripBaseline').value; + + const sweepTypeSelect = document.getElementById('tscmSweepType'); + const baselineSelect = document.getElementById('tscmBaselineSelect'); + if (sweepTypeSelect) sweepTypeSelect.value = sweepType; + if (baselineSelect) baselineSelect.value = baseline; + + startTscmSweep(); + } + + function updateTscmStrip(threatCount, reviewCount, infoCount) { + const threatsEl = document.getElementById('tscmStripThreats'); + const reviewEl = document.getElementById('tscmStripReview'); + const infoEl = document.getElementById('tscmStripInfo'); + + if (threatsEl) threatsEl.textContent = threatCount; + if (reviewEl) reviewEl.textContent = reviewCount; + if (infoEl) infoEl.textContent = infoCount; + } + + // Sync TSCM strip baseline dropdown when baselines are loaded + function syncTscmStripBaselines(baselines) { + const stripSelect = document.getElementById('tscmStripBaseline'); + if (!stripSelect) return; + + stripSelect.innerHTML = ''; + baselines.forEach(baseline => { + const opt = document.createElement('option'); + opt.value = baseline.id || baseline.name; + opt.textContent = baseline.name; + stripSelect.appendChild(opt); + }); + } + function generateTscmReport() { // Calculate sweep duration const startTime = tscmSweepStartTime || new Date(); diff --git a/templates/partials/modes/bluetooth.html b/templates/partials/modes/bluetooth.html index fe0006c..518c53b 100644 --- a/templates/partials/modes/bluetooth.html +++ b/templates/partials/modes/bluetooth.html @@ -5,6 +5,16 @@ ++ Scan for nearby Bluetooth devices including trackers, phones, and other wireless devices. +
+