diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index 49ace0b9..09a3e166 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -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() { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js index 7387afcc..7e11ca16 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js @@ -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: LABEL - 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) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts new file mode 100644 index 00000000..763c1c65 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/decoder-registry.ts @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// 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( + "#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((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((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((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; + 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 { + 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;