From 76814a51b300acbe6aeae736979348d67a72d4ed Mon Sep 17 00:00:00 2001 From: James Smith Date: Fri, 3 Jul 2026 08:49:55 +0100 Subject: [PATCH] feat: add SignalIdModal IIFE component with scored results Co-Authored-By: Claude Sonnet 4.6 --- static/js/components/signal-id-modal.js | 295 ++++++++++++++++++++++++ templates/index.html | 1 + 2 files changed, 296 insertions(+) create mode 100644 static/js/components/signal-id-modal.js diff --git a/static/js/components/signal-id-modal.js b/static/js/components/signal-id-modal.js new file mode 100644 index 0000000..f680272 --- /dev/null +++ b/static/js/components/signal-id-modal.js @@ -0,0 +1,295 @@ +/* Signal identification modal — standalone IIFE component. + * Usage: SignalIdModal.open({ frequency_mhz: 98.5, modulation: 'WFM' }) + * SignalIdModal.open({}) // blank fields + * SignalIdModal.close() + */ +window.SignalIdModal = (function () { + 'use strict'; + + var _modal = null; + var _backdrop = null; + var _lastFreq = null; + var _lastMod = null; + + function _esc(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function _safeSigIdUrl(url) { + try { + var p = new URL(String(url || '')); + if (p.protocol === 'https:' && p.hostname.endsWith('sigidwiki.com')) return p.toString(); + } catch (_) {} + return null; + } + + function _build() { + if (_modal) return; + + _backdrop = document.createElement('div'); + _backdrop.id = 'sigIdBackdrop'; + _backdrop.style.cssText = [ + 'position:fixed', 'inset:0', 'z-index:9998', + 'background:rgba(0,0,0,0.65)', 'display:none', + ].join(';'); + _backdrop.addEventListener('click', close); + + _modal = document.createElement('div'); + _modal.id = 'sigIdModal'; + _modal.style.cssText = [ + 'position:fixed', 'top:50%', 'left:50%', + 'transform:translate(-50%,-50%)', + 'z-index:9999', + 'width:min(500px,95vw)', 'max-height:85vh', + 'overflow-y:auto', + 'background:var(--bg-card,#1a1a2e)', + 'border:1px solid rgba(255,255,255,0.12)', + 'border-radius:8px', 'padding:16px', + 'display:none', + 'font-family:var(--font-mono,monospace)', + 'color:var(--text-primary,#e0e0e0)', + 'box-sizing:border-box', + ].join(';'); + + _modal.innerHTML = [ + '
', + '
Signal Identification
', + ' ', + '
', + '
', + '
', + ' ', + ' ', + '
', + '
', + ' ', + ' ', + '
', + '
', + '
', + '
', + ' ', + ' ', + '
', + ' ', + '
', + '
', + '
', + ].join(''); + + document.body.appendChild(_backdrop); + document.body.appendChild(_modal); + + document.getElementById('sigIdClose').addEventListener('click', close); + document.getElementById('sigIdSearch').addEventListener('click', search); + document.getElementById('sigIdFreq').addEventListener('input', _validateFreq); + } + + function _validateFreq() { + var freq = document.getElementById('sigIdFreq'); + var btn = document.getElementById('sigIdSearch'); + if (!freq || !btn) return; + var val = parseFloat(freq.value); + var valid = isFinite(val) && val > 0; + btn.disabled = !valid; + freq.style.borderColor = (freq.value === '' || valid) + ? 'rgba(255,255,255,0.15)' + : 'var(--accent-red,#ff4444)'; + } + + function _setStatus(text, isError) { + var el = document.getElementById('sigIdStatus'); + if (!el) return; + el.textContent = text || ''; + el.style.color = isError ? 'var(--accent-red,#ff4444)' : 'var(--text-muted,#888)'; + } + + function _renderResults(matches, freqMhz) { + var el = document.getElementById('sigIdResults'); + if (!el) return; + if (!matches.length) { + el.innerHTML = '
' + + 'No signals match ' + freqMhz.toFixed(4) + ' MHz' + + ' — try adjusting the frequency or leaving bandwidth blank.
'; + return; + } + el.innerHTML = matches.map(function (m, i) { + var dot = i === 0 ? '●' : '○'; + var score = Number(m.score) || 0; + var name = _esc(m.name || 'Unknown'); + var desc = _esc(m.description || ''); + var cats = Array.isArray(m.categories) ? m.categories : []; + var reasons = Array.isArray(m.match_reasons) ? m.match_reasons : []; + var safeUrl = _safeSigIdUrl(m.sigidwiki_url); + var ranges = Array.isArray(m.frequency_ranges) ? m.frequency_ranges : []; + var mods = Array.isArray(m.modulations) ? m.modulations : []; + var bw = m.bandwidth_range; + + var freqStr = ranges.length + ? (ranges[0].min_hz / 1e6).toFixed(3) + '–' + (ranges[0].max_hz / 1e6).toFixed(3) + ' MHz' + : ''; + var bwStr = bw + ? (bw.min_hz >= 1000 ? (bw.min_hz / 1000).toFixed(0) + 'k' : bw.min_hz) + + '–' + + (bw.max_hz >= 1000 ? (bw.max_hz / 1000).toFixed(0) + 'k' : bw.max_hz) + + ' Hz' + : ''; + var modStr = mods.join(', '); + var meta = [freqStr, modStr, bwStr].filter(Boolean).join(' · '); + + var catHtml = cats.slice(0, 5).map(function (c) { + return '' + + _esc(c) + ''; + }).join(''); + + return '
' + + '
' + + '
' + dot + ' ' + name + '
' + + '
' + + '
' + + '
' + + '
' + score + '
' + + '
' + + (meta ? '
' + _esc(meta) + '
' : '') + + (desc ? '
' + desc + '
' : '') + + (catHtml ? '
' + catHtml + '
' : '') + + (reasons.length ? '
' + + reasons.map(_esc).join(' · ') + '
' : '') + + (safeUrl ? '
' + + '↗ View on SigID Wiki
' : '') + + '
'; + }).join(''); + } + + function open(opts) { + _build(); + opts = opts || {}; + + var freqEl = document.getElementById('sigIdFreq'); + var modEl = document.getElementById('sigIdMod'); + var bwEl = document.getElementById('sigIdBw'); + var resultsEl = document.getElementById('sigIdResults'); + + if (opts.frequency_mhz != null) { + freqEl.value = Number(opts.frequency_mhz).toFixed(4); + _lastFreq = Number(opts.frequency_mhz); + } else { + freqEl.value = ''; + _lastFreq = null; + } + + var modVal = (opts.modulation || '').toUpperCase(); + modEl.value = modVal || ''; + + bwEl.value = ''; + resultsEl.innerHTML = ''; + _setStatus(''); + _validateFreq(); + + _backdrop.style.display = 'block'; + _modal.style.display = 'block'; + + if (!opts.frequency_mhz) { + setTimeout(function () { freqEl.focus(); }, 50); + } + } + + function close() { + if (!_modal) return; + _modal.style.display = 'none'; + _backdrop.style.display = 'none'; + } + + function search() { + var freqEl = document.getElementById('sigIdFreq'); + var modEl = document.getElementById('sigIdMod'); + var bwEl = document.getElementById('sigIdBw'); + var searchBtn = document.getElementById('sigIdSearch'); + + var freq = parseFloat(freqEl.value); + if (!isFinite(freq) || freq <= 0) return; + + var bwKhz = parseFloat(bwEl.value); + var bwHz = (isFinite(bwKhz) && bwKhz > 0) ? Math.round(bwKhz * 1000) : null; + var mod = (modEl.value || '').trim().toUpperCase() || null; + + _setStatus('Searching ' + freq.toFixed(4) + ' MHz…'); + document.getElementById('sigIdResults').innerHTML = ''; + searchBtn.disabled = true; + + fetch('/signalid/match', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + frequency_mhz: freq, + bandwidth_hz: bwHz, + modulation: mod, + limit: 8, + }), + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + searchBtn.disabled = false; + if (!data || data.status !== 'ok') { + _setStatus('Search failed', true); + return; + } + var matches = data.matches || []; + if (matches.length) { + _setStatus(matches.length + ' match' + (matches.length !== 1 ? 'es' : '') + + ' for ' + freq.toFixed(4) + ' MHz'); + } + _renderResults(matches, freq); + }) + .catch(function () { + searchBtn.disabled = false; + _setStatus('Search failed — check connection', true); + document.getElementById('sigIdResults').innerHTML = + '
' + + 'Network error —
'; + }); + } + + return {open: open, close: close, search: search}; +})(); diff --git a/templates/index.html b/templates/index.html index 0239041..75252b6 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3678,6 +3678,7 @@ +