Files
intercept/static/js/components/signal-id-modal.js
T
2026-07-03 08:49:55 +01:00

296 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 = [
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">',
' <div style="font-size:13px;font-weight:700;">Signal Identification</div>',
' <button id="sigIdClose" style="background:none;border:none;color:var(--text-muted,#888);',
' cursor:pointer;font-size:20px;line-height:1;padding:0 4px;" aria-label="Close">&times;</button>',
'</div>',
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">',
' <div>',
' <label for="sigIdFreq" style="font-size:10px;color:var(--text-muted,#888);display:block;margin-bottom:3px;">',
' Frequency (MHz)</label>',
' <input id="sigIdFreq" type="number" step="0.0001" min="0.001" max="6000"',
' style="width:100%;background:var(--bg-input,rgba(255,255,255,0.07));',
' border:1px solid rgba(255,255,255,0.15);border-radius:4px;',
' color:var(--text-primary,#e0e0e0);padding:5px 8px;',
' font-family:var(--font-mono,monospace);font-size:11px;box-sizing:border-box;">',
' </div>',
' <div>',
' <label for="sigIdBw" style="font-size:10px;color:var(--text-muted,#888);display:block;margin-bottom:3px;">',
' Bandwidth (kHz)</label>',
' <input id="sigIdBw" type="number" step="0.1" min="0.1" placeholder="optional"',
' style="width:100%;background:var(--bg-input,rgba(255,255,255,0.07));',
' border:1px solid rgba(255,255,255,0.15);border-radius:4px;',
' color:var(--text-primary,#e0e0e0);padding:5px 8px;',
' font-family:var(--font-mono,monospace);font-size:11px;box-sizing:border-box;">',
' </div>',
'</div>',
'<div style="display:flex;gap:8px;margin-bottom:12px;align-items:flex-end;">',
' <div style="flex:1;">',
' <label for="sigIdMod" style="font-size:10px;color:var(--text-muted,#888);display:block;margin-bottom:3px;">',
' Modulation</label>',
' <select id="sigIdMod" style="width:100%;background:var(--bg-input,rgba(255,255,255,0.07));',
' border:1px solid rgba(255,255,255,0.15);border-radius:4px;',
' color:var(--text-primary,#e0e0e0);padding:5px 8px;',
' font-family:var(--font-mono,monospace);font-size:11px;box-sizing:border-box;">',
' <option value="">Auto</option>',
' <option value="WFM">WFM</option>',
' <option value="NFM">NFM</option>',
' <option value="FM">FM</option>',
' <option value="AM">AM</option>',
' <option value="USB">USB</option>',
' <option value="LSB">LSB</option>',
' <option value="FSK">FSK</option>',
' <option value="OOK">OOK</option>',
' <option value="PSK">PSK</option>',
' </select>',
' </div>',
' <button id="sigIdSearch" style="background:var(--accent-cyan,#00c8ff);color:#000;',
' border:none;border-radius:4px;padding:6px 18px;font-size:11px;font-weight:700;',
' cursor:pointer;font-family:var(--font-mono,monospace);white-space:nowrap;">',
' Search</button>',
'</div>',
'<div id="sigIdStatus" style="font-size:10px;color:var(--text-muted,#888);min-height:14px;margin-bottom:8px;"></div>',
'<div id="sigIdResults"></div>',
].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 = '<div style="font-size:10px;color:var(--text-muted,#888);">'
+ 'No signals match ' + freqMhz.toFixed(4) + ' MHz'
+ ' — try adjusting the frequency or leaving bandwidth blank.</div>';
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 '<span style="background:rgba(0,200,255,0.1);color:var(--accent-cyan,#00c8ff);'
+ 'padding:1px 5px;border-radius:3px;font-size:9px;margin-right:3px;">'
+ _esc(c) + '</span>';
}).join('');
return '<div style="border:1px solid rgba(255,255,255,0.08);border-radius:5px;'
+ 'padding:8px;margin-bottom:6px;">'
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;'
+ 'gap:8px;margin-bottom:4px;">'
+ '<div style="font-size:11px;font-weight:700;">' + dot + ' ' + name + '</div>'
+ '<div style="display:flex;align-items:center;gap:4px;flex-shrink:0;">'
+ '<div style="width:56px;height:5px;background:rgba(255,255,255,0.1);border-radius:3px;overflow:hidden;">'
+ '<div style="width:' + score + '%;height:100%;background:var(--accent-cyan,#00c8ff);"></div>'
+ '</div><div style="font-size:9px;color:var(--text-muted,#888);">' + score + '</div>'
+ '</div></div>'
+ (meta ? '<div style="font-size:9px;color:var(--text-muted,#888);margin-bottom:4px;">' + _esc(meta) + '</div>' : '')
+ (desc ? '<div style="font-size:10px;color:var(--text-secondary,#b0b0b0);line-height:1.4;margin-bottom:5px;">' + desc + '</div>' : '')
+ (catHtml ? '<div style="margin-bottom:5px;">' + catHtml + '</div>' : '')
+ (reasons.length ? '<div style="font-size:9px;color:var(--text-dim,#666);">'
+ reasons.map(_esc).join(' · ') + '</div>' : '')
+ (safeUrl ? '<div style="margin-top:5px;"><a href="' + _esc(safeUrl) + '"'
+ ' target="_blank" rel="noopener noreferrer"'
+ ' style="font-size:9px;color:var(--accent-cyan,#00c8ff);text-decoration:none;">'
+ '↗ View on SigID Wiki</a></div>' : '')
+ '</div>';
}).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 =
'<div style="font-size:10px;color:var(--accent-red,#ff4444);">'
+ 'Network error — <button onclick="window.SignalIdModal.search()" '
+ 'style="background:none;border:none;color:var(--accent-cyan,#00c8ff);'
+ 'cursor:pointer;font-size:10px;padding:0;">Retry</button></div>';
});
}
return {open: open, close: close, search: search};
})();