mirror of
https://github.com/smittix/intercept.git
synced 2026-07-03 23:33:38 -07:00
feat: add SignalIdModal IIFE component with scored results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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, '"')
|
||||
.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 = [
|
||||
'<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">×</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};
|
||||
})();
|
||||
@@ -3678,6 +3678,7 @@
|
||||
<script src="{{ url_for('static', filename='js/components/proximity-radar.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/timeline-heatmap.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/signal-waveform.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/components/signal-id-modal.js') }}?v={{ version }}"></script>
|
||||
<!-- Mode scripts are lazy-loaded via ensureModeScript() in switchMode() -->
|
||||
<!-- WiFi v2 components (eagerly loaded — shared component) -->
|
||||
<script src="{{ url_for('static', filename='js/components/channel-chart.js') }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user