feat: introduce frontend mode registry (no behaviour change)

window.INTERCEPT_MODES mirrors the existing modeCatalog, sidebar
toggles, visuals list, destroy map, and switchMode init branches.
Derivations follow in subsequent commits; a temporary console guard
verifies registry/catalog parity at runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-06-11 21:16:40 +01:00
parent cdb5285b68
commit 8813d069bc
2 changed files with 312 additions and 0 deletions
+298
View File
@@ -0,0 +1,298 @@
// Single source of truth for SPA mode wiring. Each entry drives (after the
// derivation tasks that follow):
// - modeCatalog (label/indicator/outputTitle/group)
// - sidebar active-state toggles (elementId)
// - the destroy map (destroy hook, or module.destroy?.())
// - visuals container display (visuals: true)
// - init dispatch in switchMode (init hook)
//
// Loaded in <head> before the DOM and before mode modules. init/destroy bodies
// reference globals lazily (only called later from switchMode), so nothing here
// is evaluated at load time.
window.INTERCEPT_MODES = {
pager: {
label: 'Pager', indicator: 'PAGER', outputTitle: 'Pager Decoder', group: 'signals',
elementId: 'pagerMode',
visuals: false,
destroy: () => { if (eventSource) { eventSource.close(); eventSource = null; } },
},
sensor: {
label: '433MHz', indicator: '433MHZ', outputTitle: '433MHz Sensor Monitor', group: 'signals',
elementId: 'sensorMode',
visuals: false,
destroy: () => { if (eventSource) { eventSource.close(); eventSource = null; } },
},
rtlamr: {
label: 'Meters', indicator: 'METERS', outputTitle: 'Utility Meter Monitor', group: 'signals',
elementId: 'rtlamrMode',
visuals: false,
destroy: () => { if (eventSource) { eventSource.close(); eventSource = null; } },
},
subghz: {
label: 'SubGHz', indicator: 'SUBGHZ', outputTitle: 'SubGHz Transceiver', group: 'signals',
elementId: 'subghzMode',
visuals: true,
module: 'SubGhz',
init: () => {
SubGhz.init();
},
},
aprs: {
label: 'APRS', indicator: 'APRS', outputTitle: 'APRS Tracker', group: 'tracking',
elementId: 'aprsMode',
visuals: true,
destroy: () => {
if (typeof destroyAprsMode === 'function') {
destroyAprsMode();
} else if (aprsEventSource) {
aprsEventSource.close();
aprsEventSource = null;
}
},
init: () => {
checkAprsTools();
initAprsMap();
// Fix map sizing on mobile after container becomes visible
setTimeout(() => {
if (aprsMap) aprsMap.invalidateSize();
}, 100);
},
},
gps: {
label: 'GPS', indicator: 'GPS', outputTitle: 'GPS Receiver', group: 'tracking',
elementId: 'gpsMode',
visuals: true,
module: 'GPS',
init: () => {
GPS.init();
},
},
radiosonde: {
label: 'Radiosonde', indicator: 'SONDE', outputTitle: 'Radiosonde Decoder', group: 'tracking',
elementId: 'radiosondeMode',
visuals: true,
destroy: () => { if (radiosondeEventSource) { radiosondeEventSource.close(); radiosondeEventSource = null; } },
init: () => {
initRadiosondeWaveform();
initRadiosondeMap();
setTimeout(() => {
if (radiosondeMap) radiosondeMap.invalidateSize();
}, 100);
},
},
satellite: {
label: 'Satellite', indicator: 'SATELLITE', outputTitle: 'Satellite Monitor', group: 'space',
elementId: 'satelliteMode',
visuals: true,
init: () => {
initPolarPlot();
initSatelliteList();
},
},
sstv: {
label: 'ISS SSTV', indicator: 'ISS SSTV', outputTitle: 'ISS SSTV Decoder', group: 'space',
elementId: 'sstvMode',
visuals: true,
module: 'SSTV',
init: () => {
SSTV.init();
setTimeout(() => {
if (typeof SSTV !== 'undefined' && SSTV.invalidateMap) SSTV.invalidateMap();
}, 120);
},
},
weathersat: {
label: 'Weather Sat', indicator: 'WEATHER SAT', outputTitle: 'Weather Satellite Decoder', group: 'space',
elementId: 'weatherSatMode',
visuals: true,
module: 'WeatherSat',
init: () => {
WeatherSat.init();
setTimeout(() => {
WeatherSat.invalidateMap();
}, 100);
},
},
sstv_general: {
label: 'HF SSTV', indicator: 'HF SSTV', outputTitle: 'HF SSTV Decoder', group: 'space',
elementId: 'sstvGeneralMode',
visuals: true,
module: 'SSTVGeneral',
init: () => {
SSTVGeneral.init();
},
},
wefax: {
label: 'WeFax', indicator: 'WEFAX', outputTitle: 'Weather Fax Decoder', group: 'space',
elementId: 'wefaxMode',
visuals: true,
module: 'WeFax',
init: () => {
WeFax.init();
},
},
spaceweather: {
label: 'Space Weather', indicator: 'SPACE WX', outputTitle: 'Space Weather Monitor', group: 'space',
elementId: 'spaceWeatherMode',
visuals: true,
module: 'SpaceWeather',
init: () => {
SpaceWeather.init();
},
},
meteor: {
label: 'Meteor', indicator: 'METEOR', outputTitle: 'Meteor Scatter Monitor', group: 'space',
elementId: 'meteorMode',
visuals: true,
module: 'MeteorScatter',
init: () => {
MeteorScatter.init();
},
},
wifi: {
label: 'WiFi', indicator: 'WIFI', outputTitle: 'WiFi Scanner', group: 'wireless',
elementId: 'wifiMode',
visuals: true,
module: 'WiFiMode',
init: () => {
refreshWifiInterfaces();
initRadar();
initWatchList();
// Initialize v2 WiFi components
if (typeof WiFiMode !== 'undefined') {
WiFiMode.init();
}
},
},
bluetooth: {
label: 'Bluetooth', indicator: 'BLUETOOTH', outputTitle: 'Bluetooth Scanner', group: 'wireless',
elementId: 'bluetoothMode',
visuals: true,
module: 'BluetoothMode',
init: () => {
refreshBtInterfaces();
initBtRadar();
},
},
bt_locate: {
label: 'BT Locate', indicator: 'BT LOCATE', outputTitle: 'BT Locate — SAR Tracker', group: 'wireless',
elementId: 'btLocateMode',
visuals: true,
module: 'BtLocate',
init: () => {
BtLocate.init();
setTimeout(() => {
if (typeof BtLocate !== 'undefined' && BtLocate.invalidateMap) BtLocate.invalidateMap();
}, 100);
setTimeout(() => {
if (typeof BtLocate !== 'undefined' && BtLocate.invalidateMap) BtLocate.invalidateMap();
}, 320);
},
},
wifi_locate: {
label: 'WiFi Locate', indicator: 'WF LOCATE', outputTitle: 'WiFi Locate', group: 'wireless',
elementId: 'wflMode',
visuals: true,
module: 'WiFiLocate',
init: () => {
WiFiLocate.init();
},
},
meshtastic: {
label: 'Meshtastic', indicator: 'MESHTASTIC', outputTitle: 'Meshtastic Mesh Monitor', group: 'wireless',
elementId: 'meshtasticMode',
visuals: true,
module: 'Meshtastic',
init: () => {
Meshtastic.init();
// Fix map sizing after container becomes visible
setTimeout(() => {
Meshtastic.invalidateMap();
}, 100);
},
},
meshcore: {
label: 'Meshcore', indicator: 'MESHCORE', outputTitle: 'Meshcore Mesh Monitor', group: 'wireless',
elementId: 'meshcoreMode',
visuals: true,
module: 'MeshCore',
init: () => {
MeshCore.init();
setTimeout(() => {
MeshCore.invalidateMap();
}, 100);
},
},
tscm: {
label: 'TSCM', indicator: 'TSCM', outputTitle: 'TSCM Counter-Surveillance', group: 'intel',
elementId: 'tscmMode',
visuals: true,
destroy: () => { if (tscmEventSource) { tscmEventSource.close(); tscmEventSource = null; } },
},
drone: {
label: 'Drone Intel', indicator: 'DRONE', outputTitle: 'Drone Intelligence', group: 'intel',
elementId: 'droneMode',
visuals: true,
module: 'DroneMode',
init: () => {
if (typeof DroneMode !== 'undefined') {
DroneMode.init();
setTimeout(() => { DroneMode.invalidateMap?.(); }, 100);
}
},
},
spystations: {
label: 'Spy Stations', indicator: 'SPY STATIONS', outputTitle: 'Spy Stations', group: 'intel',
elementId: 'spystationsMode',
visuals: true,
module: 'SpyStations',
init: () => {
SpyStations.init();
},
},
websdr: {
label: 'WebSDR', indicator: 'WEBSDR', outputTitle: 'HF/Shortwave WebSDR', group: 'intel',
elementId: 'websdrMode',
visuals: true,
module: 'WebSDR',
init: () => {
if (typeof initWebSDR === 'function') initWebSDR();
},
},
waterfall: {
label: 'Waterfall', indicator: 'WATERFALL', outputTitle: 'Spectrum Waterfall', group: 'signals',
elementId: 'waterfallMode',
visuals: true,
module: 'Waterfall',
init: () => {
if (typeof Waterfall !== 'undefined') Waterfall.init();
},
},
morse: {
label: 'Morse', indicator: 'MORSE', outputTitle: 'CW/Morse Decoder', group: 'signals',
elementId: 'morseMode',
visuals: true,
module: 'MorseMode',
init: () => {
MorseMode.init();
},
},
system: {
label: 'System', indicator: 'SYSTEM', outputTitle: 'System Health Monitor', group: 'system',
elementId: 'systemMode',
visuals: true,
module: 'SystemHealth',
init: () => {
SystemHealth.init();
},
},
ook: {
label: 'OOK Decoder', indicator: 'OOK', outputTitle: 'OOK Signal Decoder', group: 'signals',
elementId: 'ookMode',
visuals: true,
module: 'OokMode',
init: () => {
OokMode.init();
},
},
};
+14
View File
@@ -3684,6 +3684,7 @@
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
<script src="{{ url_for('static', filename='js/mode-registry.js') }}?v={{ version }}"></script>
<script>
// ============================================
@@ -3848,6 +3849,19 @@
const validModes = new Set(Object.keys(modeCatalog));
window.interceptModeCatalog = Object.assign({}, modeCatalog);
// TEMP migration guard (removed in Task 3.2): registry must mirror modeCatalog
(function checkRegistry() {
const a = Object.keys(window.INTERCEPT_MODES).sort().join(',');
const b = Object.keys(modeCatalog).sort().join(',');
if (a !== b) console.error('mode-registry drift:', a, 'vs', b);
for (const [m, def] of Object.entries(window.INTERCEPT_MODES)) {
const c = modeCatalog[m] || {};
for (const f of ['label', 'indicator', 'outputTitle', 'group']) {
if (def[f] !== c[f]) console.error(`mode-registry drift: ${m}.${f}`, def[f], 'vs', c[f]);
}
}
})();
function getModeFromQuery() {
const params = new URLSearchParams(window.location.search);
const requestedMode = params.get('mode');