refactor: extract typed decoder registry

This commit is contained in:
sjg
2026-08-01 16:28:39 +02:00
parent 26a167cb82
commit cbca810a30
3 changed files with 164 additions and 110 deletions
@@ -72,61 +72,73 @@
}
}
// src/app.js
// src/core/decoder-registry.ts
var bridge = window;
var readyCallbacks = [];
var decoderRegistry = [];
window.decoderRegistry = decoderRegistry;
var _decoderRegistryReadyCallbacks = [];
window.onDecoderRegistryReady = function(fn) {
if (decoderRegistry.length > 0) fn();
else _decoderRegistryReadyCallbacks.push(fn);
};
(async function fetchDecoderRegistry() {
try {
const resp = await fetch("/decoders");
if (resp.ok) {
decoderRegistry = await resp.json();
window.decoderRegistry = decoderRegistry;
for (const fn of _decoderRegistryReadyCallbacks) fn();
_decoderRegistryReadyCallbacks.length = 0;
hideUnsupportedDecoderTabs();
refreshOperatorLayoutCapabilities();
}
} catch (e) {
console.error("Failed to fetch decoder registry:", e);
}
})();
function hideUnsupportedDecoderTabs() {
const knownIds = new Set(decoderRegistry.map(function(d) {
return d.id;
}));
function onDecoderRegistryReady(callback) {
if (decoderRegistry.length > 0) callback();
else readyCallbacks.push(callback);
}
function hideUnsupportedDecoderUi() {
const knownIds = new Set(decoderRegistry.map(({ id }) => id));
const alwaysShow = /* @__PURE__ */ new Set(["overview", "rds", "sat"]);
document.querySelectorAll("#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]").forEach(function(btn) {
const id = btn.dataset.subtab;
if (alwaysShow.has(id) || knownIds.has(id)) return;
btn.style.display = "none";
var panel = document.getElementById("subtab-" + id);
document.querySelectorAll(
"#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]"
).forEach((button) => {
const id = button.dataset.subtab;
if (!id || alwaysShow.has(id) || knownIds.has(id)) return;
button.style.display = "none";
const panel = document.getElementById(`subtab-${id}`);
if (panel) panel.style.display = "none";
});
document.querySelectorAll('[id^="about-dec-"]').forEach(function(el) {
var id = el.id.replace("about-dec-", "");
if (!alwaysShow.has(id) && !knownIds.has(id)) {
var row = el.closest("tr");
if (row) row.style.display = "none";
}
document.querySelectorAll('[id^="about-dec-"]').forEach((element) => {
const id = element.id.replace("about-dec-", "");
if (alwaysShow.has(id) || knownIds.has(id)) return;
const row = element.closest("tr");
if (row) row.style.display = "none";
});
document.querySelectorAll('[id^="settings-clear-"][id$="-history"]').forEach(function(el) {
var m = el.id.match(/^settings-clear-(.+)-history$/);
if (m && !alwaysShow.has(m[1]) && !knownIds.has(m[1])) {
el.style.display = "none";
}
document.querySelectorAll('[id^="settings-clear-"][id$="-history"]').forEach((element) => {
const match = /^settings-clear-(.+)-history$/.exec(element.id);
const id = match?.[1];
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
});
document.querySelectorAll("#subtab-overview .plugin-item[data-decoder]").forEach(function(el) {
var id = el.dataset.decoder;
if (!alwaysShow.has(id) && !knownIds.has(id)) {
el.style.display = "none";
}
document.querySelectorAll("#subtab-overview .plugin-item[data-decoder]").forEach((element) => {
const id = element.dataset.decoder;
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
});
}
function isDecoderDescriptor(value) {
if (typeof value !== "object" || value === null) return false;
const item = value;
return typeof item.id === "string" && typeof item.label === "string" && typeof item.activation === "string" && Array.isArray(item.active_modes) && item.active_modes.every((mode) => typeof mode === "string") && typeof item.background_decode === "boolean" && typeof item.bookmark_selectable === "boolean";
}
function decodeRegistry(value) {
if (!Array.isArray(value) || !value.every(isDecoderDescriptor)) {
throw new TypeError("The decoder registry response is malformed");
}
return value;
}
async function loadDecoderRegistry(onLoaded) {
try {
const response = await fetch("/decoders");
if (!response.ok) return;
decoderRegistry = decodeRegistry(await response.json());
bridge.decoderRegistry = decoderRegistry;
readyCallbacks.splice(0).forEach((callback) => {
callback();
});
hideUnsupportedDecoderUi();
onLoaded();
} catch (error) {
console.error("Failed to fetch decoder registry:", error);
}
}
bridge.decoderRegistry = decoderRegistry;
bridge.onDecoderRegistryReady = onDecoderRegistryReady;
// src/app.js
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
var authRole = null;
var authEnabled = true;
async function checkAuthStatus() {
@@ -11,71 +11,13 @@ import {
} from "./core/geo.js";
import { escapeHtml as escapeMapHtml } from "./core/dom.js";
import { loadSetting, saveSetting } from "./core/settings.js";
import {
decoderRegistry,
loadDecoderRegistry,
} from "./core/decoder-registry.js";
// --- Decoder registry (fetched from /decoders on load) ---
/** @type {Array<{id:string,label:string,activation:string,active_modes:string[],background_decode:boolean,bookmark_selectable:boolean}>} */
let decoderRegistry = [];
window.decoderRegistry = decoderRegistry;
/** Callbacks invoked once the decoder registry is fetched. */
const _decoderRegistryReadyCallbacks = [];
window.onDecoderRegistryReady = function (fn) {
if (decoderRegistry.length > 0) fn();
else _decoderRegistryReadyCallbacks.push(fn);
};
(async function fetchDecoderRegistry() {
try {
const resp = await fetch("/decoders");
if (resp.ok) {
decoderRegistry = await resp.json();
window.decoderRegistry = decoderRegistry;
for (const fn of _decoderRegistryReadyCallbacks) fn();
_decoderRegistryReadyCallbacks.length = 0;
hideUnsupportedDecoderTabs();
refreshOperatorLayoutCapabilities();
}
} catch (e) {
console.error("Failed to fetch decoder registry:", e);
}
})();
/** Hide decoder sub-tabs, panels, about rows, and settings buttons
* for decoders not present in the server's registry. */
function hideUnsupportedDecoderTabs() {
const knownIds = new Set(decoderRegistry.map(function (d) { return d.id; }));
// Sub-tabs that are not decoders and should always be visible.
const alwaysShow = new Set(["overview", "rds", "sat"]);
document.querySelectorAll("#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]").forEach(function (btn) {
const id = btn.dataset.subtab;
if (alwaysShow.has(id) || knownIds.has(id)) return;
btn.style.display = "none";
var panel = document.getElementById("subtab-" + id);
if (panel) panel.style.display = "none";
});
// About-tab decoder status rows: <td>LABEL</td><td id="about-dec-ID">
document.querySelectorAll('[id^="about-dec-"]').forEach(function (el) {
var id = el.id.replace("about-dec-", "");
if (!alwaysShow.has(id) && !knownIds.has(id)) {
var row = el.closest("tr");
if (row) row.style.display = "none";
}
});
// Settings clear-history buttons
document.querySelectorAll('[id^="settings-clear-"][id$="-history"]').forEach(function (el) {
var m = el.id.match(/^settings-clear-(.+)-history$/);
if (m && !alwaysShow.has(m[1]) && !knownIds.has(m[1])) {
el.style.display = "none";
}
});
// Overview decoder descriptions
document.querySelectorAll("#subtab-overview .plugin-item[data-decoder]").forEach(function (el) {
var id = el.dataset.decoder;
if (!alwaysShow.has(id) && !knownIds.has(id)) {
el.style.display = "none";
}
});
}
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
// --- Authentication ---
let authRole = null; // null (not authenticated), "rx" (read-only), or "control" (full access)
@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
export interface DecoderDescriptor {
id: string;
label: string;
activation: string;
active_modes: string[];
background_decode: boolean;
bookmark_selectable: boolean;
}
interface DecoderRegistryBridge extends Window {
decoderRegistry?: DecoderDescriptor[];
onDecoderRegistryReady?: (callback: () => void) => void;
}
const bridge = window as DecoderRegistryBridge;
const readyCallbacks: Array<() => void> = [];
export let decoderRegistry: DecoderDescriptor[] = [];
export function onDecoderRegistryReady(callback: () => void): void {
if (decoderRegistry.length > 0) callback();
else readyCallbacks.push(callback);
}
function hideUnsupportedDecoderUi(): void {
const knownIds = new Set(decoderRegistry.map(({ id }) => id));
const alwaysShow = new Set(["overview", "rds", "sat"]);
document.querySelectorAll<HTMLElement>(
"#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]",
).forEach((button) => {
const id = button.dataset.subtab;
if (!id || alwaysShow.has(id) || knownIds.has(id)) return;
button.style.display = "none";
const panel = document.getElementById(`subtab-${id}`);
if (panel) panel.style.display = "none";
});
document.querySelectorAll<HTMLElement>('[id^="about-dec-"]').forEach((element) => {
const id = element.id.replace("about-dec-", "");
if (alwaysShow.has(id) || knownIds.has(id)) return;
const row = element.closest<HTMLElement>("tr");
if (row) row.style.display = "none";
});
document.querySelectorAll<HTMLElement>('[id^="settings-clear-"][id$="-history"]')
.forEach((element) => {
const match = /^settings-clear-(.+)-history$/.exec(element.id);
const id = match?.[1];
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
});
document.querySelectorAll<HTMLElement>("#subtab-overview .plugin-item[data-decoder]")
.forEach((element) => {
const id = element.dataset.decoder;
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
});
}
function isDecoderDescriptor(value: unknown): value is DecoderDescriptor {
if (typeof value !== "object" || value === null) return false;
const item = value as Partial<DecoderDescriptor>;
return typeof item.id === "string"
&& typeof item.label === "string"
&& typeof item.activation === "string"
&& Array.isArray(item.active_modes)
&& item.active_modes.every((mode) => typeof mode === "string")
&& typeof item.background_decode === "boolean"
&& typeof item.bookmark_selectable === "boolean";
}
function decodeRegistry(value: unknown): DecoderDescriptor[] {
if (!Array.isArray(value) || !value.every(isDecoderDescriptor)) {
throw new TypeError("The decoder registry response is malformed");
}
return value;
}
export async function loadDecoderRegistry(onLoaded: () => void): Promise<void> {
try {
const response = await fetch("/decoders");
if (!response.ok) return;
decoderRegistry = decodeRegistry(await response.json());
bridge.decoderRegistry = decoderRegistry;
readyCallbacks.splice(0).forEach((callback) => { callback(); });
hideUnsupportedDecoderUi();
onLoaded();
} catch (error: unknown) {
console.error("Failed to fetch decoder registry:", error);
}
}
// Temporary compatibility boundary for lazy modules that have not yet been
// changed to receive the registry service through their plugin context.
bridge.decoderRegistry = decoderRegistry;
bridge.onDecoderRegistryReady = onDecoderRegistryReady;