refactor: extract typed decoder registry
This commit is contained in:
@@ -72,61 +72,73 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/app.js
|
// src/core/decoder-registry.ts
|
||||||
|
var bridge = window;
|
||||||
|
var readyCallbacks = [];
|
||||||
var decoderRegistry = [];
|
var decoderRegistry = [];
|
||||||
window.decoderRegistry = decoderRegistry;
|
function onDecoderRegistryReady(callback) {
|
||||||
var _decoderRegistryReadyCallbacks = [];
|
if (decoderRegistry.length > 0) callback();
|
||||||
window.onDecoderRegistryReady = function(fn) {
|
else readyCallbacks.push(callback);
|
||||||
if (decoderRegistry.length > 0) fn();
|
}
|
||||||
else _decoderRegistryReadyCallbacks.push(fn);
|
function hideUnsupportedDecoderUi() {
|
||||||
};
|
const knownIds = new Set(decoderRegistry.map(({ id }) => id));
|
||||||
(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;
|
|
||||||
}));
|
|
||||||
const alwaysShow = /* @__PURE__ */ new Set(["overview", "rds", "sat"]);
|
const alwaysShow = /* @__PURE__ */ new Set(["overview", "rds", "sat"]);
|
||||||
document.querySelectorAll("#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]").forEach(function(btn) {
|
document.querySelectorAll(
|
||||||
const id = btn.dataset.subtab;
|
"#tab-digital-modes > .sub-tab-bar > .sub-tab[data-subtab]"
|
||||||
if (alwaysShow.has(id) || knownIds.has(id)) return;
|
).forEach((button) => {
|
||||||
btn.style.display = "none";
|
const id = button.dataset.subtab;
|
||||||
var panel = document.getElementById("subtab-" + id);
|
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";
|
if (panel) panel.style.display = "none";
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[id^="about-dec-"]').forEach(function(el) {
|
document.querySelectorAll('[id^="about-dec-"]').forEach((element) => {
|
||||||
var id = el.id.replace("about-dec-", "");
|
const id = element.id.replace("about-dec-", "");
|
||||||
if (!alwaysShow.has(id) && !knownIds.has(id)) {
|
if (alwaysShow.has(id) || knownIds.has(id)) return;
|
||||||
var row = el.closest("tr");
|
const row = element.closest("tr");
|
||||||
if (row) row.style.display = "none";
|
if (row) row.style.display = "none";
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[id^="settings-clear-"][id$="-history"]').forEach(function(el) {
|
document.querySelectorAll('[id^="settings-clear-"][id$="-history"]').forEach((element) => {
|
||||||
var m = el.id.match(/^settings-clear-(.+)-history$/);
|
const match = /^settings-clear-(.+)-history$/.exec(element.id);
|
||||||
if (m && !alwaysShow.has(m[1]) && !knownIds.has(m[1])) {
|
const id = match?.[1];
|
||||||
el.style.display = "none";
|
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.querySelectorAll("#subtab-overview .plugin-item[data-decoder]").forEach(function(el) {
|
document.querySelectorAll("#subtab-overview .plugin-item[data-decoder]").forEach((element) => {
|
||||||
var id = el.dataset.decoder;
|
const id = element.dataset.decoder;
|
||||||
if (!alwaysShow.has(id) && !knownIds.has(id)) {
|
if (id && !alwaysShow.has(id) && !knownIds.has(id)) element.style.display = "none";
|
||||||
el.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 authRole = null;
|
||||||
var authEnabled = true;
|
var authEnabled = true;
|
||||||
async function checkAuthStatus() {
|
async function checkAuthStatus() {
|
||||||
|
|||||||
@@ -11,71 +11,13 @@ import {
|
|||||||
} from "./core/geo.js";
|
} from "./core/geo.js";
|
||||||
import { escapeHtml as escapeMapHtml } from "./core/dom.js";
|
import { escapeHtml as escapeMapHtml } from "./core/dom.js";
|
||||||
import { loadSetting, saveSetting } from "./core/settings.js";
|
import { loadSetting, saveSetting } from "./core/settings.js";
|
||||||
|
import {
|
||||||
|
decoderRegistry,
|
||||||
|
loadDecoderRegistry,
|
||||||
|
} from "./core/decoder-registry.js";
|
||||||
|
|
||||||
// --- Decoder registry (fetched from /decoders on load) ---
|
// --- Decoder registry (fetched from /decoders on load) ---
|
||||||
/** @type {Array<{id:string,label:string,activation:string,active_modes:string[],background_decode:boolean,bookmark_selectable:boolean}>} */
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
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";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Authentication ---
|
// --- Authentication ---
|
||||||
let authRole = null; // null (not authenticated), "rx" (read-only), or "control" (full access)
|
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;
|
||||||
Reference in New Issue
Block a user