refactor: convert satellite scheduler to TypeScript

This commit is contained in:
sjg
2026-08-01 12:54:03 +02:00
parent 4fb65971e4
commit fc2f55bab9
7 changed files with 408 additions and 332 deletions
@@ -10,7 +10,7 @@ const pluginGroups = {
}; };
const loaded = /* @__PURE__ */ new Set(); const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map(); const loading = /* @__PURE__ */ new Map();
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js"]); const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js"]);
function loadLegacyScript(path) { function loadLegacyScript(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
@@ -1,5 +1,8 @@
"use strict"; "use strict";
(function() { (() => {
// src/plugins/sat-scheduler.ts
var satSchedulerWindow = window;
(function() {
"use strict"; "use strict";
const dom = { const dom = {
enabled: document.getElementById("scheduler-sat-enabled"), enabled: document.getElementById("scheduler-sat-enabled"),
@@ -22,52 +25,50 @@
}; };
let editIdx = null; let editIdx = null;
function getBridge() { function getBridge() {
return window.schedulerBridge || {}; return satSchedulerWindow.schedulerBridge ?? null;
} }
function getConfig() { function getConfig() {
const b = getBridge(); const b = getBridge();
return typeof b.getConfig === "function" ? b.getConfig() : null; return b?.getConfig() ?? null;
} }
function getStatus() { function getStatus() {
const b = getBridge(); const b = getBridge();
return typeof b.getStatus === "function" ? b.getStatus() : null; return b?.getStatus() ?? null;
} }
function getBookmarks() { function getBookmarks() {
const b = getBridge(); const b = getBridge();
return typeof b.getBookmarks === "function" ? b.getBookmarks() : []; return b?.getBookmarks() ?? [];
} }
function markDirty() { function markDirty() {
var b = getBridge(); getBridge()?.markDirty();
if (typeof b.markDirty === "function") b.markDirty();
} }
function bmName(id) { function bmName(id) {
const bm = getBookmarks().find(function(b) { const bm = getBookmarks().find(function(b) {
return b.id === id; return b.id === id;
}); });
return bm ? bm.name : String(id || ""); return bm ? bm.name : id;
} }
function escHtml(s) { function escHtml(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
} }
function formatFreq(hz) { function formatFreq(hz) {
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz"; if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz"; if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
return hz + " Hz"; return `${String(hz)} Hz`;
} }
function getSatelliteEntries() { function getSatelliteEntries() {
var config = getConfig(); const config = getConfig();
return config && config.satellites && Array.isArray(config.satellites.entries) ? config.satellites.entries : []; return config && config.satellites && Array.isArray(config.satellites.entries) ? config.satellites.entries : [];
} }
function ensureSatelliteConfig() { function ensureSatelliteConfig() {
var config = getConfig(); const config = getConfig();
if (!config) return { enabled: false, pretune_secs: 60, entries: [] }; if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] }; if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites.entries) config.satellites.entries = [];
return config.satellites; return config.satellites;
} }
function collectSatelliteConfig() { function collectSatelliteConfig() {
var enabled = dom.enabled ? dom.enabled.checked : false; const enabled = dom.enabled ? dom.enabled.checked : false;
var pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60; const pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
return { return {
enabled, enabled,
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune, pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
@@ -75,38 +76,38 @@
}; };
} }
function renderSection() { function renderSection() {
var config = getConfig(); const config = getConfig();
var satCfg = config && config.satellites || {}; const satCfg = config?.satellites;
var enabled = !!satCfg.enabled; const enabled = satCfg?.enabled ?? false;
if (dom.enabled) dom.enabled.checked = enabled; if (dom.enabled) dom.enabled.checked = enabled;
if (dom.pretune) dom.pretune.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60; if (dom.pretune) dom.pretune.value = String(satCfg?.pretune_secs ?? 60);
if (dom.body) dom.body.style.display = enabled ? "" : "none"; if (dom.body) dom.body.style.display = enabled ? "" : "none";
renderEntries(); renderEntries();
renderPassStatus(); renderPassStatus();
} }
function renderEntries() { function renderEntries() {
if (!dom.tbody) return; if (!dom.tbody) return;
var entries = getSatelliteEntries(); const entries = getSatelliteEntries();
var frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
entries.forEach(function(entry, idx) { entries.forEach(function(entry, idx) {
var tr = document.createElement("tr"); const tr = document.createElement("tr");
var tdSat = document.createElement("td"); const tdSat = document.createElement("td");
tdSat.textContent = entry.satellite || ""; tdSat.textContent = entry.satellite || "";
tr.appendChild(tdSat); tr.appendChild(tdSat);
var tdNorad = document.createElement("td"); const tdNorad = document.createElement("td");
tdNorad.textContent = entry.norad_id || ""; tdNorad.textContent = String(entry.norad_id || "");
tr.appendChild(tdNorad); tr.appendChild(tdNorad);
var tdBm = document.createElement("td"); const tdBm = document.createElement("td");
tdBm.textContent = bmName(entry.bookmark_id); tdBm.textContent = bmName(entry.bookmark_id);
tr.appendChild(tdBm); tr.appendChild(tdBm);
var tdEl = document.createElement("td"); const tdEl = document.createElement("td");
tdEl.textContent = entry.min_elevation_deg != null ? entry.min_elevation_deg + "°" : "5°"; tdEl.textContent = `${String(entry.min_elevation_deg)}°`;
tr.appendChild(tdEl); tr.appendChild(tdEl);
var tdPrio = document.createElement("td"); const tdPrio = document.createElement("td");
tdPrio.textContent = entry.priority || 0; tdPrio.textContent = String(entry.priority || 0);
tr.appendChild(tdPrio); tr.appendChild(tdPrio);
var tdActions = document.createElement("td"); const tdActions = document.createElement("td");
var editBtn = document.createElement("button"); const editBtn = document.createElement("button");
editBtn.className = "sch-write"; editBtn.className = "sch-write";
editBtn.type = "button"; editBtn.type = "button";
editBtn.textContent = "Edit"; editBtn.textContent = "Edit";
@@ -114,7 +115,7 @@
openForm(entry, idx); openForm(entry, idx);
}); });
tdActions.appendChild(editBtn); tdActions.appendChild(editBtn);
var removeBtn = document.createElement("button"); const removeBtn = document.createElement("button");
removeBtn.className = "sch-write"; removeBtn.className = "sch-write";
removeBtn.type = "button"; removeBtn.type = "button";
removeBtn.textContent = "Remove"; removeBtn.textContent = "Remove";
@@ -129,12 +130,12 @@
} }
function renderPassStatus() { function renderPassStatus() {
if (!dom.passStatus) return; if (!dom.passStatus) return;
var entries = getSatelliteEntries(); const entries = getSatelliteEntries();
if (entries.length === 0) { if (entries.length === 0) {
dom.passStatus.innerHTML = ""; dom.passStatus.innerHTML = "";
return; return;
} }
var status = getStatus(); const status = getStatus();
if (status && status.active_satellite) { if (status && status.active_satellite) {
dom.passStatus.innerHTML = '<span class="sch-sat-active-badge">PASS ACTIVE: ' + escHtml(status.active_satellite) + "</span>"; dom.passStatus.innerHTML = '<span class="sch-sat-active-badge">PASS ACTIVE: ' + escHtml(status.active_satellite) + "</span>";
} else { } else {
@@ -142,18 +143,19 @@
} }
} }
function renderBookmarkSelect(selectedId) { function renderBookmarkSelect(selectedId) {
if (!dom.bookmark) return; const bookmarkSelect = dom.bookmark;
dom.bookmark.innerHTML = '<option value="">— none —</option>'; if (!bookmarkSelect) return;
bookmarkSelect.innerHTML = '<option value="">— none —</option>';
getBookmarks().forEach(function(bm) { getBookmarks().forEach(function(bm) {
var opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = bm.id; opt.value = bm.id;
opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")"; opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
if (bm.id === selectedId) opt.selected = true; if (bm.id === selectedId) opt.selected = true;
dom.bookmark.appendChild(opt); bookmarkSelect.appendChild(opt);
}); });
} }
function removeEntry(idx) { function removeEntry(idx) {
var sat = ensureSatelliteConfig(); const sat = ensureSatelliteConfig();
sat.entries.splice(idx, 1); sat.entries.splice(idx, 1);
renderEntries(); renderEntries();
markDirty(); markDirty();
@@ -163,10 +165,10 @@
if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite"; if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
if (dom.preset) dom.preset.value = ""; if (dom.preset) dom.preset.value = "";
if (dom.name) dom.name.value = entry ? entry.satellite || "" : ""; if (dom.name) dom.name.value = entry ? entry.satellite || "" : "";
if (dom.norad) dom.norad.value = entry ? entry.norad_id || "" : ""; if (dom.norad) dom.norad.value = entry ? String(entry.norad_id || "") : "";
if (dom.minEl) dom.minEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5; if (dom.minEl) dom.minEl.value = String(entry?.min_elevation_deg ?? 5);
if (dom.priority) dom.priority.value = entry && entry.priority != null ? entry.priority : 0; if (dom.priority) dom.priority.value = String(entry?.priority ?? 0);
if (dom.centerHz) dom.centerHz.value = entry && entry.center_hz ? entry.center_hz : ""; if (dom.centerHz) dom.centerHz.value = entry?.center_hz ? String(entry.center_hz) : "";
renderBookmarkSelect(entry ? entry.bookmark_id : null); renderBookmarkSelect(entry ? entry.bookmark_id : null);
if (dom.formWrap) { if (dom.formWrap) {
dom.formWrap.style.display = "flex"; dom.formWrap.style.display = "flex";
@@ -179,29 +181,31 @@
} }
function onFormSubmit(e) { function onFormSubmit(e) {
e.preventDefault(); e.preventDefault();
var satellite = dom.name ? dom.name.value.trim() : ""; const satellite = dom.name ? dom.name.value.trim() : "";
var noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN; const noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
var bmId = dom.bookmark ? dom.bookmark.value : ""; const bmId = dom.bookmark ? dom.bookmark.value : "";
if (!satellite) { if (!satellite) {
window.trxUi?.notify("Enter a satellite name.", { kind: "error" }); satSchedulerWindow.trxUi?.notify("Enter a satellite name.", { kind: "error" });
document.getElementById("scheduler-sat-name")?.focus(); dom.name?.focus();
return; return;
} }
if (isNaN(noradId) || noradId <= 0) { if (isNaN(noradId) || noradId <= 0) {
window.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" }); satSchedulerWindow.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" });
document.getElementById("scheduler-sat-norad")?.focus(); dom.norad?.focus();
return; return;
} }
if (!bmId) { if (!bmId) {
window.trxUi?.notify("Select a bookmark.", { kind: "error" }); satSchedulerWindow.trxUi?.notify("Select a bookmark.", { kind: "error" });
document.getElementById("scheduler-sat-bookmark")?.focus(); dom.bookmark?.focus();
return; return;
} }
var minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5; const minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
var prio = dom.priority ? parseInt(dom.priority.value, 10) : 0; const prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
var centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN; const centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
var sat = ensureSatelliteConfig(); const sat = ensureSatelliteConfig();
var entryData = { const existing = editIdx !== null ? sat.entries[editIdx] : void 0;
const entryData = {
id: existing?.id ?? `sat_${Date.now().toString(36)}`,
satellite, satellite,
norad_id: noradId, norad_id: noradId,
bookmark_id: bmId, bookmark_id: bmId,
@@ -211,11 +215,8 @@
bookmark_ids: [] bookmark_ids: []
}; };
if (editIdx !== null) { if (editIdx !== null) {
var existing = sat.entries[editIdx];
entryData.id = existing ? existing.id : "sat_" + Date.now().toString(36);
sat.entries[editIdx] = entryData; sat.entries[editIdx] = entryData;
} else { } else {
entryData.id = "sat_" + Date.now().toString(36);
sat.entries.push(entryData); sat.entries.push(entryData);
} }
closeForm(); closeForm();
@@ -224,14 +225,15 @@
} }
function onPresetChange() { function onPresetChange() {
if (!dom.preset || !dom.preset.value) return; if (!dom.preset || !dom.preset.value) return;
var parts = dom.preset.value.split("|"); const parts = dom.preset.value.split("|");
if (dom.name) dom.name.value = parts[0] || ""; if (dom.name) dom.name.value = parts[0] || "";
if (dom.norad) dom.norad.value = parts[1] || ""; if (dom.norad) dom.norad.value = parts[1] || "";
} }
function wireEvents() { function wireEvents() {
if (dom.enabled) { if (dom.enabled) {
const enabledInput = dom.enabled;
dom.enabled.addEventListener("change", function() { dom.enabled.addEventListener("change", function() {
if (dom.body) dom.body.style.display = dom.enabled.checked ? "" : "none"; if (dom.body) dom.body.style.display = enabledInput.checked ? "" : "none";
markDirty(); markDirty();
}); });
} }
@@ -247,10 +249,11 @@
if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm); if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm);
if (dom.preset) dom.preset.addEventListener("change", onPresetChange); if (dom.preset) dom.preset.addEventListener("change", onPresetChange);
} }
window.satScheduler = { satSchedulerWindow.satScheduler = {
wireEvents, wireEvents,
renderSection, renderSection,
renderPassStatus, renderPassStatus,
collectSatelliteConfig collectSatelliteConfig
}; };
})();
})(); })();
@@ -23,7 +23,6 @@ await build({
screenshot: path.join(sourceDir, "screenshot.ts"), screenshot: path.join(sourceDir, "screenshot.ts"),
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"), "webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"), bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
scheduler: path.join(sourceDir, "plugins", "scheduler.js"), scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
vchan: path.join(sourceDir, "plugins", "vchan.js"), vchan: path.join(sourceDir, "plugins", "vchan.js"),
}, },
@@ -51,6 +50,7 @@ await build({
aprs: path.join(sourceDir, "plugins", "aprs.ts"), aprs: path.join(sourceDir, "plugins", "aprs.ts"),
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.ts"), "hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.ts"),
sat: path.join(sourceDir, "plugins", "sat.ts"), sat: path.join(sourceDir, "plugins", "sat.ts"),
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.ts"),
}, },
outdir: outputDir, outdir: outputDir,
bundle: true, bundle: true,
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
const loaded = new Set<string>(); const loaded = new Set<string>();
const loading = new Map<string, Promise<void>>(); const loading = new Map<string, Promise<void>>();
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js"]); const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js"]);
function loadLegacyScript(path: string): Promise<void> { function loadLegacyScript(path: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -2,6 +2,23 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import type {
SatelliteBookmark,
SatelliteScheduleConfig,
SatelliteScheduleEntry,
SatelliteSchedulerApi,
SatelliteSchedulerBridge,
SatelliteSchedulerStatus,
SchedulerConfigWithSatellites,
} from "./satellite-types";
interface SatelliteSchedulerWindow {
schedulerBridge?: SatelliteSchedulerBridge;
satScheduler?: SatelliteSchedulerApi;
trxUi?: { notify(message: string, options: { kind: "error" }): void };
}
const satSchedulerWindow = window as unknown as SatelliteSchedulerWindow;
// Satellite Pass Scheduling UI // Satellite Pass Scheduling UI
// Manages the satellite overlay section within the background decoding scheduler. // Manages the satellite overlay section within the background decoding scheduler.
// Communicates with scheduler.js via a thin window API for shared state access. // Communicates with scheduler.js via a thin window API for shared state access.
@@ -11,93 +28,91 @@
// ── DOM references (cached once) ────────────────────────────────── // ── DOM references (cached once) ──────────────────────────────────
const dom = { const dom = {
enabled: document.getElementById("scheduler-sat-enabled"), enabled: document.getElementById("scheduler-sat-enabled") as HTMLInputElement | null,
pretune: document.getElementById("scheduler-sat-pretune"), pretune: document.getElementById("scheduler-sat-pretune") as HTMLInputElement | null,
body: document.getElementById("scheduler-sat-body"), body: document.getElementById("scheduler-sat-body"),
tbody: document.getElementById("scheduler-sat-tbody"), tbody: document.getElementById("scheduler-sat-tbody"),
addBtn: document.getElementById("scheduler-sat-add-btn"), addBtn: document.getElementById("scheduler-sat-add-btn"),
passStatus: document.getElementById("scheduler-sat-pass-status"), passStatus: document.getElementById("scheduler-sat-pass-status"),
formWrap: document.getElementById("sch-sat-form-wrap"), formWrap: document.getElementById("sch-sat-form-wrap"),
formTitle: document.getElementById("sch-sat-form-title"), formTitle: document.getElementById("sch-sat-form-title"),
form: document.getElementById("sch-sat-form"), form: document.getElementById("sch-sat-form") as HTMLFormElement | null,
formCancel: document.getElementById("sch-sat-form-cancel"), formCancel: document.getElementById("sch-sat-form-cancel"),
preset: document.getElementById("scheduler-sat-preset"), preset: document.getElementById("scheduler-sat-preset") as HTMLSelectElement | null,
name: document.getElementById("scheduler-sat-name"), name: document.getElementById("scheduler-sat-name") as HTMLInputElement | null,
norad: document.getElementById("scheduler-sat-norad"), norad: document.getElementById("scheduler-sat-norad") as HTMLInputElement | null,
bookmark: document.getElementById("scheduler-sat-bookmark"), bookmark: document.getElementById("scheduler-sat-bookmark") as HTMLSelectElement | null,
minEl: document.getElementById("scheduler-sat-min-el"), minEl: document.getElementById("scheduler-sat-min-el") as HTMLInputElement | null,
priority: document.getElementById("scheduler-sat-priority"), priority: document.getElementById("scheduler-sat-priority") as HTMLInputElement | null,
centerHz: document.getElementById("scheduler-sat-center-hz"), centerHz: document.getElementById("scheduler-sat-center-hz") as HTMLInputElement | null,
}; };
// ── Local state ─────────────────────────────────────────────────── // ── Local state ───────────────────────────────────────────────────
let editIdx = null; // null = adding, number = editing let editIdx: number | null = null;
// ── Scheduler bridge ────────────────────────────────────────────── // ── Scheduler bridge ──────────────────────────────────────────────
// These accessors call into scheduler.js via window.schedulerBridge, // These accessors call into scheduler.js via window.schedulerBridge,
// which is set up by scheduler.js after it initializes. // which is set up by scheduler.js after it initializes.
function getBridge() { function getBridge(): SatelliteSchedulerBridge | null {
return window.schedulerBridge || {}; return satSchedulerWindow.schedulerBridge ?? null;
} }
function getConfig() { function getConfig(): SchedulerConfigWithSatellites | null {
const b = getBridge(); const b = getBridge();
return typeof b.getConfig === "function" ? b.getConfig() : null; return b?.getConfig() ?? null;
} }
function getStatus() { function getStatus(): SatelliteSchedulerStatus | null {
const b = getBridge(); const b = getBridge();
return typeof b.getStatus === "function" ? b.getStatus() : null; return b?.getStatus() ?? null;
} }
function getBookmarks() { function getBookmarks(): SatelliteBookmark[] {
const b = getBridge(); const b = getBridge();
return typeof b.getBookmarks === "function" ? b.getBookmarks() : []; return b?.getBookmarks() ?? [];
} }
function markDirty() { function markDirty() {
var b = getBridge(); getBridge()?.markDirty();
if (typeof b.markDirty === "function") b.markDirty();
} }
function bmName(id) { function bmName(id: string): string {
const bm = getBookmarks().find(function (b) { return b.id === id; }); const bm = getBookmarks().find(function (b) { return b.id === id; });
return bm ? bm.name : String(id || ""); return bm ? bm.name : id;
} }
function escHtml(s) { function escHtml(s: string): string {
return String(s) return s
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;"); .replace(/"/g, "&quot;");
} }
function formatFreq(hz) { function formatFreq(hz: number): string {
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz"; if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz"; if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
return hz + " Hz"; return `${String(hz)} Hz`;
} }
// ── Satellite config helpers ────────────────────────────────────── // ── Satellite config helpers ──────────────────────────────────────
function getSatelliteEntries() { function getSatelliteEntries(): SatelliteScheduleEntry[] {
var config = getConfig(); const config = getConfig();
return (config && config.satellites && Array.isArray(config.satellites.entries)) return (config && config.satellites && Array.isArray(config.satellites.entries))
? config.satellites.entries ? config.satellites.entries
: []; : [];
} }
function ensureSatelliteConfig() { function ensureSatelliteConfig(): SatelliteScheduleConfig {
var config = getConfig(); const config = getConfig();
if (!config) return { enabled: false, pretune_secs: 60, entries: [] }; if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] }; if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites.entries) config.satellites.entries = [];
return config.satellites; return config.satellites;
} }
function collectSatelliteConfig() { function collectSatelliteConfig(): SatelliteScheduleConfig {
var enabled = dom.enabled ? dom.enabled.checked : false; const enabled = dom.enabled ? dom.enabled.checked : false;
var pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60; const pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
return { return {
enabled: enabled, enabled: enabled,
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune, pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
@@ -107,12 +122,12 @@
// ── Render: section ─────────────────────────────────────────────── // ── Render: section ───────────────────────────────────────────────
function renderSection() { function renderSection() {
var config = getConfig(); const config = getConfig();
var satCfg = (config && config.satellites) || {}; const satCfg = config?.satellites;
var enabled = !!satCfg.enabled; const enabled = satCfg?.enabled ?? false;
if (dom.enabled) dom.enabled.checked = enabled; if (dom.enabled) dom.enabled.checked = enabled;
if (dom.pretune) dom.pretune.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60; if (dom.pretune) dom.pretune.value = String(satCfg?.pretune_secs ?? 60);
if (dom.body) dom.body.style.display = enabled ? "" : "none"; if (dom.body) dom.body.style.display = enabled ? "" : "none";
renderEntries(); renderEntries();
@@ -122,35 +137,35 @@
// ── Render: entries table ───────────────────────────────────────── // ── Render: entries table ─────────────────────────────────────────
function renderEntries() { function renderEntries() {
if (!dom.tbody) return; if (!dom.tbody) return;
var entries = getSatelliteEntries(); const entries = getSatelliteEntries();
var frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
entries.forEach(function (entry, idx) { entries.forEach(function (entry, idx) {
var tr = document.createElement("tr"); const tr = document.createElement("tr");
var tdSat = document.createElement("td"); const tdSat = document.createElement("td");
tdSat.textContent = entry.satellite || ""; tdSat.textContent = entry.satellite || "";
tr.appendChild(tdSat); tr.appendChild(tdSat);
var tdNorad = document.createElement("td"); const tdNorad = document.createElement("td");
tdNorad.textContent = entry.norad_id || ""; tdNorad.textContent = String(entry.norad_id || "");
tr.appendChild(tdNorad); tr.appendChild(tdNorad);
var tdBm = document.createElement("td"); const tdBm = document.createElement("td");
tdBm.textContent = bmName(entry.bookmark_id); tdBm.textContent = bmName(entry.bookmark_id);
tr.appendChild(tdBm); tr.appendChild(tdBm);
var tdEl = document.createElement("td"); const tdEl = document.createElement("td");
tdEl.textContent = (entry.min_elevation_deg != null ? entry.min_elevation_deg + "\u00B0" : "5\u00B0"); tdEl.textContent = `${String(entry.min_elevation_deg)}\u00B0`;
tr.appendChild(tdEl); tr.appendChild(tdEl);
var tdPrio = document.createElement("td"); const tdPrio = document.createElement("td");
tdPrio.textContent = entry.priority || 0; tdPrio.textContent = String(entry.priority || 0);
tr.appendChild(tdPrio); tr.appendChild(tdPrio);
var tdActions = document.createElement("td"); const tdActions = document.createElement("td");
var editBtn = document.createElement("button"); const editBtn = document.createElement("button");
editBtn.className = "sch-write"; editBtn.className = "sch-write";
editBtn.type = "button"; editBtn.type = "button";
editBtn.textContent = "Edit"; editBtn.textContent = "Edit";
@@ -159,7 +174,7 @@
}); });
tdActions.appendChild(editBtn); tdActions.appendChild(editBtn);
var removeBtn = document.createElement("button"); const removeBtn = document.createElement("button");
removeBtn.className = "sch-write"; removeBtn.className = "sch-write";
removeBtn.type = "button"; removeBtn.type = "button";
removeBtn.textContent = "Remove"; removeBtn.textContent = "Remove";
@@ -178,12 +193,12 @@
// ── Render: pass status ─────────────────────────────────────────── // ── Render: pass status ───────────────────────────────────────────
function renderPassStatus() { function renderPassStatus() {
if (!dom.passStatus) return; if (!dom.passStatus) return;
var entries = getSatelliteEntries(); const entries = getSatelliteEntries();
if (entries.length === 0) { if (entries.length === 0) {
dom.passStatus.innerHTML = ""; dom.passStatus.innerHTML = "";
return; return;
} }
var status = getStatus(); const status = getStatus();
if (status && status.active_satellite) { if (status && status.active_satellite) {
dom.passStatus.innerHTML = dom.passStatus.innerHTML =
'<span class="sch-sat-active-badge">PASS ACTIVE: ' + '<span class="sch-sat-active-badge">PASS ACTIVE: ' +
@@ -196,37 +211,38 @@
} }
// ── Render: bookmark dropdown ───────────────────────────────────── // ── Render: bookmark dropdown ─────────────────────────────────────
function renderBookmarkSelect(selectedId) { function renderBookmarkSelect(selectedId: string | null): void {
if (!dom.bookmark) return; const bookmarkSelect = dom.bookmark;
dom.bookmark.innerHTML = '<option value="">— none —</option>'; if (!bookmarkSelect) return;
bookmarkSelect.innerHTML = '<option value="">— none —</option>';
getBookmarks().forEach(function (bm) { getBookmarks().forEach(function (bm) {
var opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = bm.id; opt.value = bm.id;
opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")"; opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
if (bm.id === selectedId) opt.selected = true; if (bm.id === selectedId) opt.selected = true;
dom.bookmark.appendChild(opt); bookmarkSelect.appendChild(opt);
}); });
} }
// ── Entry management ────────────────────────────────────────────── // ── Entry management ──────────────────────────────────────────────
function removeEntry(idx) { function removeEntry(idx: number): void {
var sat = ensureSatelliteConfig(); const sat = ensureSatelliteConfig();
sat.entries.splice(idx, 1); sat.entries.splice(idx, 1);
renderEntries(); renderEntries();
markDirty(); markDirty();
} }
// ── Form: open ──────────────────────────────────────────────────── // ── Form: open ────────────────────────────────────────────────────
function openForm(entry, idx) { function openForm(entry: SatelliteScheduleEntry | null, idx: number | null): void {
editIdx = (idx != null) ? idx : null; editIdx = (idx != null) ? idx : null;
if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite"; if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
if (dom.preset) dom.preset.value = ""; if (dom.preset) dom.preset.value = "";
if (dom.name) dom.name.value = entry ? (entry.satellite || "") : ""; if (dom.name) dom.name.value = entry ? (entry.satellite || "") : "";
if (dom.norad) dom.norad.value = entry ? (entry.norad_id || "") : ""; if (dom.norad) dom.norad.value = entry ? String(entry.norad_id || "") : "";
if (dom.minEl) dom.minEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5; if (dom.minEl) dom.minEl.value = String(entry?.min_elevation_deg ?? 5);
if (dom.priority) dom.priority.value = entry && entry.priority != null ? entry.priority : 0; if (dom.priority) dom.priority.value = String(entry?.priority ?? 0);
if (dom.centerHz) dom.centerHz.value = entry && entry.center_hz ? entry.center_hz : ""; if (dom.centerHz) dom.centerHz.value = entry?.center_hz ? String(entry.center_hz) : "";
renderBookmarkSelect(entry ? entry.bookmark_id : null); renderBookmarkSelect(entry ? entry.bookmark_id : null);
@@ -243,24 +259,26 @@
} }
// ── Form: submit ────────────────────────────────────────────────── // ── Form: submit ──────────────────────────────────────────────────
function onFormSubmit(e) { function onFormSubmit(e: SubmitEvent): void {
e.preventDefault(); e.preventDefault();
var satellite = dom.name ? dom.name.value.trim() : ""; const satellite = dom.name ? dom.name.value.trim() : "";
var noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN; const noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
var bmId = dom.bookmark ? dom.bookmark.value : ""; const bmId = dom.bookmark ? dom.bookmark.value : "";
if (!satellite) { window.trxUi?.notify("Enter a satellite name.", { kind: "error" }); document.getElementById("scheduler-sat-name")?.focus(); return; } if (!satellite) { satSchedulerWindow.trxUi?.notify("Enter a satellite name.", { kind: "error" }); dom.name?.focus(); return; }
if (isNaN(noradId) || noradId <= 0) { window.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" }); document.getElementById("scheduler-sat-norad")?.focus(); return; } if (isNaN(noradId) || noradId <= 0) { satSchedulerWindow.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" }); dom.norad?.focus(); return; }
if (!bmId) { window.trxUi?.notify("Select a bookmark.", { kind: "error" }); document.getElementById("scheduler-sat-bookmark")?.focus(); return; } if (!bmId) { satSchedulerWindow.trxUi?.notify("Select a bookmark.", { kind: "error" }); dom.bookmark?.focus(); return; }
var minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5; const minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
var prio = dom.priority ? parseInt(dom.priority.value, 10) : 0; const prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
var centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN; const centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
var sat = ensureSatelliteConfig(); const sat = ensureSatelliteConfig();
var entryData = { const existing = editIdx !== null ? sat.entries[editIdx] : undefined;
const entryData: SatelliteScheduleEntry = {
id: existing?.id ?? `sat_${Date.now().toString(36)}`,
satellite: satellite, satellite: satellite,
norad_id: noradId, norad_id: noradId,
bookmark_id: bmId, bookmark_id: bmId,
@@ -271,11 +289,8 @@
}; };
if (editIdx !== null) { if (editIdx !== null) {
var existing = sat.entries[editIdx];
entryData.id = existing ? existing.id : ("sat_" + Date.now().toString(36));
sat.entries[editIdx] = entryData; sat.entries[editIdx] = entryData;
} else { } else {
entryData.id = "sat_" + Date.now().toString(36);
sat.entries.push(entryData); sat.entries.push(entryData);
} }
@@ -287,7 +302,7 @@
// ── Preset change handler ───────────────────────────────────────── // ── Preset change handler ─────────────────────────────────────────
function onPresetChange() { function onPresetChange() {
if (!dom.preset || !dom.preset.value) return; if (!dom.preset || !dom.preset.value) return;
var parts = dom.preset.value.split("|"); const parts = dom.preset.value.split("|");
if (dom.name) dom.name.value = parts[0] || ""; if (dom.name) dom.name.value = parts[0] || "";
if (dom.norad) dom.norad.value = parts[1] || ""; if (dom.norad) dom.norad.value = parts[1] || "";
} }
@@ -295,8 +310,9 @@
// ── Wire all events ─────────────────────────────────────────────── // ── Wire all events ───────────────────────────────────────────────
function wireEvents() { function wireEvents() {
if (dom.enabled) { if (dom.enabled) {
const enabledInput = dom.enabled;
dom.enabled.addEventListener("change", function () { dom.enabled.addEventListener("change", function () {
if (dom.body) dom.body.style.display = dom.enabled.checked ? "" : "none"; if (dom.body) dom.body.style.display = enabledInput.checked ? "" : "none";
markDirty(); markDirty();
}); });
} }
@@ -312,7 +328,7 @@
} }
// ── Public API ──────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────
window.satScheduler = { satSchedulerWindow.satScheduler = {
wireEvents: wireEvents, wireEvents: wireEvents,
renderSection: renderSection, renderSection: renderSection,
renderPassStatus: renderPassStatus, renderPassStatus: renderPassStatus,
@@ -53,3 +53,31 @@ export interface SatelliteScheduleConfig {
pretune_secs: number; pretune_secs: number;
entries: SatelliteScheduleEntry[]; entries: SatelliteScheduleEntry[];
} }
export interface SatelliteBookmark {
id: string;
name: string;
freq_hz: number;
mode: string;
}
export interface SchedulerConfigWithSatellites {
satellites?: SatelliteScheduleConfig;
[key: string]: unknown;
}
export interface SatelliteSchedulerStatus { active_satellite?: string | null }
export interface SatelliteSchedulerBridge {
getConfig(): SchedulerConfigWithSatellites | null;
getStatus(): SatelliteSchedulerStatus | null;
getBookmarks(): SatelliteBookmark[];
markDirty(): void;
}
export interface SatelliteSchedulerApi {
wireEvents(): void;
renderSection(): void;
renderPassStatus(): void;
collectSatelliteConfig(): SatelliteScheduleConfig;
}
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
test("satellite scheduler exposes a typed config adapter with safe defaults", async () => {
const window = {};
const context = vm.createContext({
window,
document: { getElementById: () => null, createDocumentFragment: () => ({ appendChild() {} }) },
Date,
Number,
String,
Array,
console,
});
const source = await readFile(new URL("../../assets/web/generated/sat-scheduler.js", import.meta.url), "utf8");
new vm.Script(source).runInContext(context);
assert.equal(typeof window.satScheduler.wireEvents, "function");
assert.deepEqual(
JSON.parse(JSON.stringify(window.satScheduler.collectSatelliteConfig())),
{ enabled: false, pretune_secs: 60, entries: [] },
);
});