diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
index 1607e28d..0547d1a1 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
@@ -10,7 +10,7 @@ const pluginGroups = {
};
const loaded = /* @__PURE__ */ new Set();
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"]);
+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"]);
function loadLegacyScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
index 6d7e98bc..35800f51 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
@@ -1,447 +1,472 @@
"use strict";
-const satDom = {
- status: document.getElementById("sat-status"),
- liveView: document.getElementById("sat-live-view"),
- historyView: document.getElementById("sat-history-view"),
- predictionsView: document.getElementById("sat-predictions-view"),
- liveLatest: document.getElementById("sat-live-latest"),
- historyList: document.getElementById("sat-history-list"),
- historyCount: document.getElementById("sat-history-count"),
- filterInput: document.getElementById("sat-filter"),
- sortSelect: document.getElementById("sat-sort"),
- typeFilter: document.getElementById("sat-type-filter"),
- lrptState: document.getElementById("sat-lrpt-state"),
- viewLiveBtn: document.getElementById("sat-view-live"),
- viewHistoryBtn: document.getElementById("sat-view-history"),
- viewPredBtn: document.getElementById("sat-view-predictions"),
- predFilter: document.getElementById("sat-pred-filter"),
- predMinEl: document.getElementById("sat-pred-min-el"),
- predCategory: document.getElementById("sat-pred-category"),
- predCurrentList: document.getElementById("sat-pred-current-list"),
- predUpcomingList: document.getElementById("sat-pred-list"),
- predCurrentSec: document.getElementById("sat-pred-current-section"),
- predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
- predStatus: document.getElementById("sat-pred-status")
-};
-let satImageHistory = [];
-const SAT_MAX_IMAGES = 100;
-const SAT_PRED_PAGE_SIZE = 50;
-let satPredShowAll = false;
-let satFilterText = "";
-let satActiveView = "live";
-let satPredData = [];
-let satPredFilterText = "";
-let satPredMinEl = 0;
-let satPredCategory = "all";
-let satPredSatCount = 0;
-let satPredCountdownTimer = null;
-function scheduleSatUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
- return;
- }
- job();
-}
-function switchSatView(view) {
- const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
- satActiveView = view;
- if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
- if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
- if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
- if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
- if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
- if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
- if (leavingPredictions) clearPredictionDom();
- if (view === "history") {
- renderSatHistoryTable();
- } else if (view === "predictions") {
- satPredShowAll = false;
- loadSatPredictions();
- }
-}
-function clearPredictionDom() {
- stopCountdownTimer();
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
-}
-window.clearSatPredictionDom = clearPredictionDom;
-satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
-satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
-satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
-let _lastSatLrptOn = null;
-window.updateSatLiveState = function(update) {
- if (!satDom.lrptState) return;
- const lrptOn = !!update.lrpt_decode_enabled;
- if (lrptOn !== _lastSatLrptOn) {
- _lastSatLrptOn = lrptOn;
- satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
- satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
- if (satDom.status) {
- if (lrptOn) {
- satDom.status.textContent = "Decoder active — waiting for signal";
- } else {
- satDom.status.textContent = "Decoder idle";
- }
- }
- }
-};
-function renderSatLatestCard() {
- if (!satDom.liveLatest) return;
- if (satImageHistory.length === 0) {
- satDom.liveLatest.innerHTML = '
No images decoded yet. Enable a decoder and wait for a satellite pass.
';
- return;
- }
- const img = satImageHistory[0];
- const decoder = img._decoder || "unknown";
- const typeName = "Meteor LRPT";
- const satellite = img.satellite || "";
- const channels = img.channels || img.channel_a || "";
- const lines = img.mcu_count || img.line_count || 0;
- const unit = "MCU rows";
- const ts = img._ts || "--";
- const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
- let meta = [typeName];
- if (satellite) meta.push(satellite);
- if (channels) meta.push(channels);
- meta.push(`${lines} ${unit}`);
- meta.push(`${date} ${ts}`);
- let html = ``;
- html += `
Latest decoded image
`;
- html += `
${meta.join(" · ")}
`;
- if (img.path) {
- html += `
Download PNG`;
- }
- if (img.geo_bounds) {
- html += `
`;
- }
- html += `
`;
- satDom.liveLatest.innerHTML = html;
-}
-function getSatFilteredHistory() {
- let items = satImageHistory;
- const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
- if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
- if (satFilterText) {
- items = items.filter((i) => {
- const haystack = [
- "meteor lrpt",
- i.satellite || "",
- i.channels || "",
- i.channel_a || "",
- i.channel_b || ""
- ].join(" ").toUpperCase();
- return haystack.includes(satFilterText);
- });
- }
- const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
- if (sortVal === "oldest") items = items.slice().reverse();
- return items;
-}
-function renderSatHistoryRow(img) {
- const row = document.createElement("div");
- row.className = "sat-history-row";
- const decoder = img._decoder || "unknown";
- const typeName = "Meteor LRPT";
- const typeClass = "sat-type-lrpt";
- const ts = img._ts || "--";
- const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
- const satellite = img.satellite || "--";
- const channels = img.channels || "--";
- const lines = img.mcu_count || img.line_count || 0;
- const unit = "MCU";
- let link = img.path ? `PNG` : "--";
- if (img.geo_bounds) {
- link += ` Map`;
- }
- row.innerHTML = [
- `${date} ${ts}`,
- `${typeName}`,
- `${satellite}`,
- `${channels}`,
- `${lines} ${unit}`,
- `${link}`
- ].join("");
- return row;
-}
-function renderSatHistoryTable() {
- if (!satDom.historyList) return;
- const items = getSatFilteredHistory();
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < items.length; i += 1) {
- fragment.appendChild(renderSatHistoryRow(items[i]));
- }
- satDom.historyList.replaceChildren(fragment);
- if (satDom.historyCount) {
- const total = satImageHistory.length;
- const shown = items.length;
- satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
- }
-}
-function addSatImage(img, decoder) {
- const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
- img._tsMs = tsMs;
- img._ts = new Date(tsMs).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- img._decoder = decoder;
- satImageHistory.unshift(img);
- if (satImageHistory.length > SAT_MAX_IMAGES) {
- satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
- }
- scheduleSatUi("sat-latest", () => renderSatLatestCard());
- if (satActiveView === "history") {
- scheduleSatUi("sat-history", () => renderSatHistoryTable());
- }
-}
-window.onServerLrptProgress = function(msg) {
- if (satDom.status && msg.mcu_count > 0) {
- satDom.status.textContent = "Receiving — " + msg.mcu_count + " MCU rows decoded";
- }
-};
-window.onServerLrptImage = function(msg) {
- if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
- addSatImage(msg, "lrpt");
- if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
- window.addSatMapOverlay(msg);
- }
-};
-window.resetSatHistoryView = function() {
- satImageHistory = [];
- if (satDom.historyList) satDom.historyList.innerHTML = "";
- renderSatLatestCard();
- renderSatHistoryTable();
- if (window.clearSatMapOverlays) window.clearSatMapOverlays();
-};
-window.pruneSatHistoryView = function() {
- renderSatHistoryTable();
- renderSatLatestCard();
-};
-const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
-lrptDecodeToggleBtn?.addEventListener("click", async () => {
- try {
- await window.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
- await postPath("/toggle_lrpt_decode");
- } catch (e) {
- console.error("LRPT toggle failed", e);
- }
-});
-satDom.filterInput?.addEventListener("input", () => {
- satFilterText = satDom.filterInput.value.trim().toUpperCase();
- renderSatHistoryTable();
-});
-satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
-satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
-document.getElementById("settings-clear-sat-history")?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await postPath("/clear_lrpt_decode");
- window.resetSatHistoryView();
- } catch (e) {
- console.error("Weather satellite history clear failed", e);
- }
-});
-function azToCardinal(deg) {
- const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
- return dirs[Math.round(deg / 45) % 8];
-}
-function formatPredTime(ms) {
- const d = new Date(ms);
- const now = /* @__PURE__ */ new Date();
- const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
- const day = d.getUTCDay() !== now.getUTCDay() ? dayNames[d.getUTCDay()] + " " : "";
- const hh = String(d.getUTCHours()).padStart(2, "0");
- const mm = String(d.getUTCMinutes()).padStart(2, "0");
- return `${day}${hh}:${mm}`;
-}
-function formatPredDuration(s) {
- if (s >= 60) return `${Math.round(s / 60)} min`;
- return `${s}s`;
-}
-function formatCountdown(ms) {
- const totalSec = Math.max(0, Math.floor(ms / 1e3));
- const m = Math.floor(totalSec / 60);
- const s = totalSec % 60;
- return `${m}:${String(s).padStart(2, "0")}`;
-}
-function elevationClass(deg) {
- if (deg >= 45) return "sat-pred-el-high";
- if (deg >= 10) return "sat-pred-el-mid";
- return "sat-pred-el-low";
-}
-function stopCountdownTimer() {
- if (satPredCountdownTimer) {
- clearInterval(satPredCountdownTimer);
- satPredCountdownTimer = null;
- }
-}
-function startCountdownTimer(container) {
- const countdownEls = container ? container.querySelectorAll(".sat-pred-col-countdown") : [];
- if (countdownEls.length === 0) return;
- satPredCountdownTimer = setInterval(() => {
- if (satActiveView !== "predictions") {
- stopCountdownTimer();
+(() => {
+ // src/plugins/sat.ts
+ var satWindow = window;
+ var satDom = {
+ status: document.getElementById("sat-status"),
+ liveView: document.getElementById("sat-live-view"),
+ historyView: document.getElementById("sat-history-view"),
+ predictionsView: document.getElementById("sat-predictions-view"),
+ liveLatest: document.getElementById("sat-live-latest"),
+ historyList: document.getElementById("sat-history-list"),
+ historyCount: document.getElementById("sat-history-count"),
+ filterInput: document.getElementById("sat-filter"),
+ sortSelect: document.getElementById("sat-sort"),
+ typeFilter: document.getElementById("sat-type-filter"),
+ lrptState: document.getElementById("sat-lrpt-state"),
+ viewLiveBtn: document.getElementById("sat-view-live"),
+ viewHistoryBtn: document.getElementById("sat-view-history"),
+ viewPredBtn: document.getElementById("sat-view-predictions"),
+ predFilter: document.getElementById("sat-pred-filter"),
+ predMinEl: document.getElementById("sat-pred-min-el"),
+ predCategory: document.getElementById("sat-pred-category"),
+ predCurrentList: document.getElementById("sat-pred-current-list"),
+ predUpcomingList: document.getElementById("sat-pred-list"),
+ predCurrentSec: document.getElementById("sat-pred-current-section"),
+ predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
+ predStatus: document.getElementById("sat-pred-status")
+ };
+ var satImageHistory = [];
+ var SAT_MAX_IMAGES = 100;
+ var SAT_PRED_PAGE_SIZE = 50;
+ var satPredShowAll = false;
+ var satFilterText = "";
+ var satActiveView = "live";
+ var satPredData = [];
+ var satPredFilterText = "";
+ var satPredMinEl = 0;
+ var satPredCategory = "all";
+ var satPredSatCount = 0;
+ var satPredCountdownTimer = null;
+ function scheduleSatUi(key, job) {
+ if (typeof satWindow.trxScheduleUiFrameJob === "function") {
+ satWindow.trxScheduleUiFrameJob(key, job);
return;
}
- const n = Date.now();
- let anyActive = false;
- for (const el of countdownEls) {
- const los = parseInt(el.dataset.los, 10);
- const rem = los - n;
- if (rem > 0) {
- el.textContent = formatCountdown(rem);
- anyActive = true;
- } else {
- el.textContent = "0:00";
+ job();
+ }
+ function switchSatView(view) {
+ const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
+ satActiveView = view;
+ if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
+ if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
+ if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
+ if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
+ if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
+ if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
+ if (leavingPredictions) clearPredictionDom();
+ if (view === "history") {
+ renderSatHistoryTable();
+ } else if (view === "predictions") {
+ satPredShowAll = false;
+ void loadSatPredictions();
+ }
+ }
+ function clearPredictionDom() {
+ stopCountdownTimer();
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ }
+ satWindow.clearSatPredictionDom = clearPredictionDom;
+ satDom.viewLiveBtn?.addEventListener("click", () => {
+ switchSatView("live");
+ });
+ satDom.viewHistoryBtn?.addEventListener("click", () => {
+ switchSatView("history");
+ });
+ satDom.viewPredBtn?.addEventListener("click", () => {
+ switchSatView("predictions");
+ });
+ var lastSatLrptOn = null;
+ satWindow.updateSatLiveState = function(update) {
+ if (!satDom.lrptState) return;
+ const lrptOn = !!update.lrpt_decode_enabled;
+ if (lrptOn !== lastSatLrptOn) {
+ lastSatLrptOn = lrptOn;
+ satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
+ satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
+ if (satDom.status) {
+ if (lrptOn) {
+ satDom.status.textContent = "Decoder active — waiting for signal";
+ } else {
+ satDom.status.textContent = "Decoder idle";
+ }
}
}
- if (!anyActive) {
- stopCountdownTimer();
- renderSatPredictions(getFilteredPredictions());
+ };
+ function renderSatLatestCard() {
+ if (!satDom.liveLatest) return;
+ if (satImageHistory.length === 0) {
+ satDom.liveLatest.innerHTML = 'No images decoded yet. Enable a decoder and wait for a satellite pass.
';
+ return;
}
- }, 1e3);
-}
-function buildCurrentPassRow(pass, now) {
- const row = document.createElement("div");
- row.className = "sat-pred-row-current";
- const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
- const remaining = Math.max(0, pass.los_ms - now);
- row.innerHTML = [
- `${pass.satellite}`,
- `${pass.max_elevation_deg.toFixed(1)}°`,
- `${formatPredTime(pass.aos_ms)}`,
- `${formatPredTime(pass.los_ms)}`,
- `${formatCountdown(remaining)}`,
- `${dir}`
- ].join("");
- return row;
-}
-function buildUpcomingPassRow(pass) {
- const row = document.createElement("div");
- row.className = "sat-pred-row";
- const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
- row.innerHTML = [
- `${formatPredTime(pass.aos_ms)}`,
- `${pass.satellite}`,
- `${pass.max_elevation_deg.toFixed(1)}°`,
- `${formatPredDuration(pass.duration_s)}`,
- `${dir}`
- ].join("");
- return row;
-}
-function getFilteredPredictions() {
- let items = satPredData;
- if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
- if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
- if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
- return items;
-}
-function applyPredFilters() {
- renderSatPredictions(getFilteredPredictions());
-}
-satDom.predFilter?.addEventListener("input", () => {
- satPredFilterText = satDom.predFilter.value.trim().toUpperCase();
- applyPredFilters();
-});
-satDom.predMinEl?.addEventListener("change", () => {
- satPredMinEl = parseInt(satDom.predMinEl.value, 10) || 0;
- applyPredFilters();
-});
-satDom.predCategory?.addEventListener("change", () => {
- satPredCategory = satDom.predCategory.value;
- applyPredFilters();
-});
-function renderSatPredictions(passes, error) {
- stopCountdownTimer();
- if (error) {
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
- if (satDom.predStatus) satDom.predStatus.textContent = error;
- return;
- }
- if (!Array.isArray(passes) || passes.length === 0) {
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
- if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
- return;
- }
- const now = Date.now();
- const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
- const upcoming = passes.filter((p) => p.aos_ms > now);
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
- if (satDom.predCurrentList) {
- if (current.length === 0) {
- satDom.predCurrentList.innerHTML = "";
- } else {
- const frag = document.createDocumentFragment();
- for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
- satDom.predCurrentList.replaceChildren(frag);
+ const img = satImageHistory[0];
+ if (!img) return;
+ const typeName = "Meteor LRPT";
+ const satellite = img.satellite || "";
+ const channels = img.channels || img.channel_a || "";
+ const lines = img.mcu_count || img.line_count || 0;
+ const unit = "MCU rows";
+ const ts = img._ts || "--";
+ const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
+ const meta = [typeName];
+ if (satellite) meta.push(satellite);
+ if (channels) meta.push(channels);
+ meta.push(`${lines} ${unit}`);
+ meta.push(`${date} ${ts}`);
+ let html = ``;
+ html += `
Latest decoded image
`;
+ html += `
${meta.join(" · ")}
`;
+ if (img.path) {
+ html += `
Download PNG`;
}
+ if (img.geo_bounds) {
+ html += `
`;
+ }
+ html += `
`;
+ satDom.liveLatest.innerHTML = html;
}
- const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
- const visibleUpcoming = upcoming.slice(0, upcomingLimit);
- const hiddenCount = upcoming.length - visibleUpcoming.length;
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
- if (satDom.predUpcomingList) {
- const frag = document.createDocumentFragment();
- for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
- if (hiddenCount > 0) {
- const moreRow = document.createElement("div");
- moreRow.className = "sat-pred-row";
- moreRow.style.cursor = "pointer";
- moreRow.style.textAlign = "center";
- moreRow.innerHTML = `Show ${hiddenCount} more passes…`;
- moreRow.addEventListener("click", () => {
- satPredShowAll = true;
- renderSatPredictions(getFilteredPredictions());
+ function getSatFilteredHistory() {
+ let items = satImageHistory;
+ const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
+ if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
+ if (satFilterText) {
+ items = items.filter((i) => {
+ const haystack = [
+ "meteor lrpt",
+ i.satellite || "",
+ i.channels || "",
+ i.channel_a || "",
+ i.channel_b || ""
+ ].join(" ").toUpperCase();
+ return haystack.includes(satFilterText);
});
- frag.appendChild(moreRow);
}
- satDom.predUpcomingList.replaceChildren(frag);
+ const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
+ if (sortVal === "oldest") items = items.slice().reverse();
+ return items;
}
- if (satDom.predStatus) {
- let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
- if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
- satDom.predStatus.textContent = text;
- }
- if (current.length > 0 && satActiveView === "predictions") {
- startCountdownTimer(satDom.predCurrentList);
- }
-}
-async function loadSatPredictions() {
- if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- try {
- const resp = await fetch("/sat_passes");
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
- satPredSatCount = data.satellite_count || 0;
- if (data.error) {
- satPredData = [];
- renderSatPredictions([], data.error);
- } else {
- satPredData = data.passes || [];
- renderSatPredictions(getFilteredPredictions());
+ function renderSatHistoryRow(img) {
+ const row = document.createElement("div");
+ row.className = "sat-history-row";
+ const typeName = "Meteor LRPT";
+ const typeClass = "sat-type-lrpt";
+ const ts = img._ts || "--";
+ const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
+ const satellite = img.satellite || "--";
+ const channels = img.channels || "--";
+ const lines = img.mcu_count || img.line_count || 0;
+ const unit = "MCU";
+ let link = img.path ? `PNG` : "--";
+ if (img.geo_bounds) {
+ link += ` Map`;
}
- } catch (e) {
- renderSatPredictions([], `Failed to load predictions: ${e.message}`);
+ row.innerHTML = [
+ `${date} ${ts}`,
+ `${typeName}`,
+ `${satellite}`,
+ `${channels}`,
+ `${lines} ${unit}`,
+ `${link}`
+ ].join("");
+ return row;
}
-}
-window.satShowOnMap = function(south, west, north, east) {
- if (typeof window.enableMapSourceFilter === "function") {
- window.enableMapSourceFilter("sat");
+ function renderSatHistoryTable() {
+ if (!satDom.historyList) return;
+ const items = getSatFilteredHistory();
+ const fragment = document.createDocumentFragment();
+ for (const image of items) {
+ fragment.appendChild(renderSatHistoryRow(image));
+ }
+ satDom.historyList.replaceChildren(fragment);
+ if (satDom.historyCount) {
+ const total = satImageHistory.length;
+ const shown = items.length;
+ satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
+ }
}
- const lat = (south + north) / 2;
- const lon = (west + east) / 2;
- if (window.navigateToAprsMap) {
- window.navigateToAprsMap(lat, lon);
+ function addSatImage(img, decoder) {
+ const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
+ img._tsMs = tsMs;
+ img._ts = new Date(tsMs).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ img._decoder = decoder;
+ satImageHistory.unshift(img);
+ if (satImageHistory.length > SAT_MAX_IMAGES) {
+ satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
+ }
+ scheduleSatUi("sat-latest", () => {
+ renderSatLatestCard();
+ });
+ if (satActiveView === "history") {
+ scheduleSatUi("sat-history", () => {
+ renderSatHistoryTable();
+ });
+ }
}
-};
-renderSatLatestCard();
-renderSatHistoryTable();
+ satWindow.onServerLrptProgress = function(msg) {
+ if (satDom.status && (msg.mcu_count ?? 0) > 0) {
+ satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
+ }
+ };
+ satWindow.onServerLrptImage = function(msg) {
+ if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
+ addSatImage(msg, "lrpt");
+ if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
+ satWindow.addSatMapOverlay(msg);
+ }
+ };
+ satWindow.resetSatHistoryView = function() {
+ satImageHistory = [];
+ if (satDom.historyList) satDom.historyList.innerHTML = "";
+ renderSatLatestCard();
+ renderSatHistoryTable();
+ satWindow.clearSatMapOverlays?.();
+ };
+ satWindow.pruneSatHistoryView = function() {
+ renderSatHistoryTable();
+ renderSatLatestCard();
+ };
+ var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
+ lrptDecodeToggleBtn?.addEventListener("click", () => {
+ void (async () => {
+ try {
+ await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
+ await satWindow.postPath?.("/toggle_lrpt_decode");
+ } catch (e) {
+ console.error("LRPT toggle failed", e);
+ }
+ })();
+ });
+ var satFilterInput = satDom.filterInput;
+ satFilterInput?.addEventListener("input", () => {
+ satFilterText = satFilterInput.value.trim().toUpperCase();
+ renderSatHistoryTable();
+ });
+ satDom.sortSelect?.addEventListener("change", () => {
+ renderSatHistoryTable();
+ });
+ satDom.typeFilter?.addEventListener("change", () => {
+ renderSatHistoryTable();
+ });
+ document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await satWindow.postPath?.("/clear_lrpt_decode");
+ satWindow.resetSatHistoryView?.();
+ } catch (e) {
+ console.error("Weather satellite history clear failed", e);
+ }
+ })();
+ });
+ function azToCardinal(deg) {
+ const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
+ return dirs[Math.round(deg / 45) % 8] ?? "N";
+ }
+ function formatPredTime(ms) {
+ const d = new Date(ms);
+ const now = /* @__PURE__ */ new Date();
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
+ const hh = String(d.getUTCHours()).padStart(2, "0");
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
+ return `${day}${hh}:${mm}`;
+ }
+ function formatPredDuration(s) {
+ if (s >= 60) return `${Math.round(s / 60)} min`;
+ return `${s}s`;
+ }
+ function formatCountdown(ms) {
+ const totalSec = Math.max(0, Math.floor(ms / 1e3));
+ const m = Math.floor(totalSec / 60);
+ const s = totalSec % 60;
+ return `${m}:${String(s).padStart(2, "0")}`;
+ }
+ function elevationClass(deg) {
+ if (deg >= 45) return "sat-pred-el-high";
+ if (deg >= 10) return "sat-pred-el-mid";
+ return "sat-pred-el-low";
+ }
+ function stopCountdownTimer() {
+ if (satPredCountdownTimer) {
+ clearInterval(satPredCountdownTimer);
+ satPredCountdownTimer = null;
+ }
+ }
+ function startCountdownTimer(container) {
+ const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
+ if (countdownEls.length === 0) return;
+ satPredCountdownTimer = setInterval(() => {
+ if (satActiveView !== "predictions") {
+ stopCountdownTimer();
+ return;
+ }
+ const n = Date.now();
+ let anyActive = false;
+ for (const el of countdownEls) {
+ const los = Number.parseInt(el.dataset.los ?? "0", 10);
+ const rem = los - n;
+ if (rem > 0) {
+ el.textContent = formatCountdown(rem);
+ anyActive = true;
+ } else {
+ el.textContent = "0:00";
+ }
+ }
+ if (!anyActive) {
+ stopCountdownTimer();
+ renderSatPredictions(getFilteredPredictions());
+ }
+ }, 1e3);
+ }
+ function buildCurrentPassRow(pass, now) {
+ const row = document.createElement("div");
+ row.className = "sat-pred-row-current";
+ const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
+ const remaining = Math.max(0, pass.los_ms - now);
+ row.innerHTML = [
+ `${pass.satellite}`,
+ `${pass.max_elevation_deg.toFixed(1)}°`,
+ `${formatPredTime(pass.aos_ms)}`,
+ `${formatPredTime(pass.los_ms)}`,
+ `${formatCountdown(remaining)}`,
+ `${dir}`
+ ].join("");
+ return row;
+ }
+ function buildUpcomingPassRow(pass) {
+ const row = document.createElement("div");
+ row.className = "sat-pred-row";
+ const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
+ row.innerHTML = [
+ `${formatPredTime(pass.aos_ms)}`,
+ `${pass.satellite}`,
+ `${pass.max_elevation_deg.toFixed(1)}°`,
+ `${formatPredDuration(pass.duration_s)}`,
+ `${dir}`
+ ].join("");
+ return row;
+ }
+ function getFilteredPredictions() {
+ let items = satPredData;
+ if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
+ if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
+ if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
+ return items;
+ }
+ function applyPredFilters() {
+ renderSatPredictions(getFilteredPredictions());
+ }
+ var satPredictionFilter = satDom.predFilter;
+ satPredictionFilter?.addEventListener("input", () => {
+ satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
+ applyPredFilters();
+ });
+ var satPredictionMinElevation = satDom.predMinEl;
+ satPredictionMinElevation?.addEventListener("change", () => {
+ satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
+ applyPredFilters();
+ });
+ var satPredictionCategory = satDom.predCategory;
+ satPredictionCategory?.addEventListener("change", () => {
+ satPredCategory = satPredictionCategory.value;
+ applyPredFilters();
+ });
+ function renderSatPredictions(passes, error) {
+ stopCountdownTimer();
+ if (error) {
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
+ if (satDom.predStatus) satDom.predStatus.textContent = error;
+ return;
+ }
+ if (!Array.isArray(passes) || passes.length === 0) {
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
+ if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
+ return;
+ }
+ const now = Date.now();
+ const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
+ const upcoming = passes.filter((p) => p.aos_ms > now);
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
+ if (satDom.predCurrentList) {
+ if (current.length === 0) {
+ satDom.predCurrentList.innerHTML = "";
+ } else {
+ const frag = document.createDocumentFragment();
+ for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
+ satDom.predCurrentList.replaceChildren(frag);
+ }
+ }
+ const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
+ const visibleUpcoming = upcoming.slice(0, upcomingLimit);
+ const hiddenCount = upcoming.length - visibleUpcoming.length;
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
+ if (satDom.predUpcomingList) {
+ const frag = document.createDocumentFragment();
+ for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
+ if (hiddenCount > 0) {
+ const moreRow = document.createElement("div");
+ moreRow.className = "sat-pred-row";
+ moreRow.style.cursor = "pointer";
+ moreRow.style.textAlign = "center";
+ moreRow.innerHTML = `Show ${hiddenCount} more passes…`;
+ moreRow.addEventListener("click", () => {
+ satPredShowAll = true;
+ renderSatPredictions(getFilteredPredictions());
+ });
+ frag.appendChild(moreRow);
+ }
+ satDom.predUpcomingList.replaceChildren(frag);
+ }
+ if (satDom.predStatus) {
+ let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
+ if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
+ satDom.predStatus.textContent = text;
+ }
+ if (current.length > 0 && satActiveView === "predictions") {
+ startCountdownTimer(satDom.predCurrentList);
+ }
+ }
+ async function loadSatPredictions() {
+ if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ try {
+ const resp = await fetch("/sat_passes");
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ const data = await resp.json();
+ satPredSatCount = data.satellite_count || 0;
+ if (data.error) {
+ satPredData = [];
+ renderSatPredictions([], data.error);
+ } else {
+ satPredData = data.passes || [];
+ renderSatPredictions(getFilteredPredictions());
+ }
+ } catch (error) {
+ renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ satWindow.satShowOnMap = function(south, west, north, east) {
+ if (typeof satWindow.enableMapSourceFilter === "function") {
+ satWindow.enableMapSourceFilter("sat");
+ }
+ const lat = (south + north) / 2;
+ const lon = (west + east) / 2;
+ if (satWindow.navigateToAprsMap) {
+ satWindow.navigateToAprsMap(lat, lon);
+ }
+ };
+ renderSatLatestCard();
+ renderSatHistoryTable();
+})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
index 81cd6014..e64d8b7f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
@@ -23,7 +23,6 @@ await build({
screenshot: path.join(sourceDir, "screenshot.ts"),
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
- sat: path.join(sourceDir, "plugins", "sat.js"),
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
vchan: path.join(sourceDir, "plugins", "vchan.js"),
@@ -51,6 +50,7 @@ await build({
ais: path.join(sourceDir, "plugins", "ais.ts"),
aprs: path.join(sourceDir, "plugins", "aprs.ts"),
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.ts"),
+ sat: path.join(sourceDir, "plugins", "sat.ts"),
},
outdir: outputDir,
bundle: true,
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
index fe93c2d6..1c839079 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
@@ -16,7 +16,7 @@ const pluginGroups: Readonly> = {
const loaded = new Set();
const loading = new Map>();
-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"]);
+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"]);
function loadLegacyScript(path: string): Promise {
return new Promise((resolve, reject) => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.ts
similarity index 74%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.js
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.ts
index 6c7e39fc..fd09030e 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat.ts
@@ -2,6 +2,28 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
+import type { LrptProgress, SatelliteImage, SatelliteLiveUpdate, SatellitePass, SatellitePassResponse } from "./satellite-types";
+
+type SatelliteView = "live" | "history" | "predictions";
+interface SatelliteBridge {
+ trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
+ clearSatPredictionDom?: () => void;
+ updateSatLiveState?: (update: SatelliteLiveUpdate) => void;
+ onServerLrptProgress?: (message: LrptProgress) => void;
+ onServerLrptImage?: (message: SatelliteImage) => void;
+ addSatMapOverlay?: (image: SatelliteImage) => void;
+ resetSatHistoryView?: () => void;
+ pruneSatHistoryView?: () => void;
+ clearSatMapOverlays?: () => void;
+ takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise;
+ postPath?: (path: string) => Promise;
+ trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise };
+ satShowOnMap?: (south: number, west: number, north: number, east: number) => void;
+ enableMapSourceFilter?: (source: string) => void;
+ navigateToAprsMap?: (lat: number, lon: number) => void;
+}
+const satWindow = window as unknown as SatelliteBridge;
+
// --- SAT Plugin ---
// Live view: decoder state, latest image card
// History view: filterable table of all decoded images
@@ -16,16 +38,16 @@ const satDom = {
liveLatest: document.getElementById("sat-live-latest"),
historyList: document.getElementById("sat-history-list"),
historyCount: document.getElementById("sat-history-count"),
- filterInput: document.getElementById("sat-filter"),
- sortSelect: document.getElementById("sat-sort"),
- typeFilter: document.getElementById("sat-type-filter"),
+ filterInput: document.getElementById("sat-filter") as HTMLInputElement | null,
+ sortSelect: document.getElementById("sat-sort") as HTMLSelectElement | null,
+ typeFilter: document.getElementById("sat-type-filter") as HTMLSelectElement | null,
lrptState: document.getElementById("sat-lrpt-state"),
viewLiveBtn: document.getElementById("sat-view-live"),
viewHistoryBtn: document.getElementById("sat-view-history"),
viewPredBtn: document.getElementById("sat-view-predictions"),
- predFilter: document.getElementById("sat-pred-filter"),
- predMinEl: document.getElementById("sat-pred-min-el"),
- predCategory: document.getElementById("sat-pred-category"),
+ predFilter: document.getElementById("sat-pred-filter") as HTMLInputElement | null,
+ predMinEl: document.getElementById("sat-pred-min-el") as HTMLSelectElement | null,
+ predCategory: document.getElementById("sat-pred-category") as HTMLSelectElement | null,
predCurrentList: document.getElementById("sat-pred-current-list"),
predUpcomingList: document.getElementById("sat-pred-list"),
predCurrentSec: document.getElementById("sat-pred-current-section"),
@@ -34,30 +56,30 @@ const satDom = {
};
// ── State ───────────────────────────────────────────────────────────
-let satImageHistory = [];
+let satImageHistory: SatelliteImage[] = [];
const SAT_MAX_IMAGES = 100;
const SAT_PRED_PAGE_SIZE = 50;
let satPredShowAll = false;
let satFilterText = "";
-let satActiveView = "live"; // "live" | "history" | "predictions"
-let satPredData = [];
+let satActiveView: SatelliteView = "live";
+let satPredData: SatellitePass[] = [];
let satPredFilterText = "";
let satPredMinEl = 0;
let satPredCategory = "all";
let satPredSatCount = 0;
-let satPredCountdownTimer = null;
+let satPredCountdownTimer: ReturnType | null = null;
// ── UI scheduler helper ─────────────────────────────────────────────
-function scheduleSatUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
+function scheduleSatUi(key: string, job: () => void): void {
+ if (typeof satWindow.trxScheduleUiFrameJob === "function") {
+ satWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
// ── View switching ──────────────────────────────────────────────────
-function switchSatView(view) {
+function switchSatView(view: SatelliteView): void {
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
satActiveView = view;
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
@@ -71,7 +93,7 @@ function switchSatView(view) {
renderSatHistoryTable();
} else if (view === "predictions") {
satPredShowAll = false;
- loadSatPredictions();
+ void loadSatPredictions();
}
}
@@ -80,19 +102,19 @@ function clearPredictionDom() {
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
}
-window.clearSatPredictionDom = clearPredictionDom;
+satWindow.clearSatPredictionDom = clearPredictionDom;
-satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
-satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
-satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
+satDom.viewLiveBtn?.addEventListener("click", () => { switchSatView("live"); });
+satDom.viewHistoryBtn?.addEventListener("click", () => { switchSatView("history"); });
+satDom.viewPredBtn?.addEventListener("click", () => { switchSatView("predictions"); });
// ── Live view: decoder state ────────────────────────────────────────
-let _lastSatLrptOn = null;
-window.updateSatLiveState = function (update) {
+let lastSatLrptOn: boolean | null = null;
+satWindow.updateSatLiveState = function (update: SatelliteLiveUpdate) {
if (!satDom.lrptState) return;
const lrptOn = !!update.lrpt_decode_enabled;
- if (lrptOn !== _lastSatLrptOn) {
- _lastSatLrptOn = lrptOn;
+ if (lrptOn !== lastSatLrptOn) {
+ lastSatLrptOn = lrptOn;
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
if (satDom.status) {
@@ -114,7 +136,7 @@ function renderSatLatestCard() {
}
const img = satImageHistory[0];
- const decoder = img._decoder || "unknown";
+ if (!img) return;
const typeName = "Meteor LRPT";
const satellite = img.satellite || "";
const channels = img.channels || img.channel_a || "";
@@ -123,7 +145,7 @@ function renderSatLatestCard() {
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
- let meta = [typeName];
+ const meta = [typeName];
if (satellite) meta.push(satellite);
if (channels) meta.push(channels);
meta.push(`${lines} ${unit}`);
@@ -143,7 +165,7 @@ function renderSatLatestCard() {
}
// ── History view: table ─────────────────────────────────────────────
-function getSatFilteredHistory() {
+function getSatFilteredHistory(): SatelliteImage[] {
let items = satImageHistory;
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
@@ -168,11 +190,10 @@ function getSatFilteredHistory() {
return items;
}
-function renderSatHistoryRow(img) {
+function renderSatHistoryRow(img: SatelliteImage): HTMLElement {
const row = document.createElement("div");
row.className = "sat-history-row";
- const decoder = img._decoder || "unknown";
const typeName = "Meteor LRPT";
const typeClass = "sat-type-lrpt";
const ts = img._ts || "--";
@@ -204,8 +225,8 @@ function renderSatHistoryTable() {
if (!satDom.historyList) return;
const items = getSatFilteredHistory();
const fragment = document.createDocumentFragment();
- for (let i = 0; i < items.length; i += 1) {
- fragment.appendChild(renderSatHistoryRow(items[i]));
+ for (const image of items) {
+ fragment.appendChild(renderSatHistoryRow(image));
}
satDom.historyList.replaceChildren(fragment);
@@ -222,7 +243,7 @@ function renderSatHistoryTable() {
}
// ── Add image to history ────────────────────────────────────────────
-function addSatImage(img, decoder) {
+function addSatImage(img: SatelliteImage, decoder: "lrpt"): void {
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
img._tsMs = tsMs;
img._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -237,102 +258,103 @@ function addSatImage(img, decoder) {
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
}
- scheduleSatUi("sat-latest", () => renderSatLatestCard());
+ scheduleSatUi("sat-latest", () => { renderSatLatestCard(); });
if (satActiveView === "history") {
- scheduleSatUi("sat-history", () => renderSatHistoryTable());
+ scheduleSatUi("sat-history", () => { renderSatHistoryTable(); });
}
}
// ── Server callbacks ────────────────────────────────────────────────
-window.onServerLrptProgress = function (msg) {
- if (satDom.status && msg.mcu_count > 0) {
- satDom.status.textContent = "Receiving \u2014 " + msg.mcu_count + " MCU rows decoded";
+satWindow.onServerLrptProgress = function (msg: LrptProgress) {
+ if (satDom.status && (msg.mcu_count ?? 0) > 0) {
+ satDom.status.textContent = `Receiving \u2014 ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
}
};
-window.onServerLrptImage = function (msg) {
+satWindow.onServerLrptImage = function (msg: SatelliteImage) {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt");
- if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
- window.addSatMapOverlay(msg);
+ if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
+ satWindow.addSatMapOverlay(msg);
}
};
-window.resetSatHistoryView = function () {
+satWindow.resetSatHistoryView = function () {
satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard();
renderSatHistoryTable();
- if (window.clearSatMapOverlays) window.clearSatMapOverlays();
+ satWindow.clearSatMapOverlays?.();
};
-window.pruneSatHistoryView = function () {
+satWindow.pruneSatHistoryView = function () {
renderSatHistoryTable();
renderSatLatestCard();
};
// ── Toggle buttons ──────────────────────────────────────────────────
const lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
-lrptDecodeToggleBtn?.addEventListener("click", async () => {
+lrptDecodeToggleBtn?.addEventListener("click", () => { void (async () => {
try {
- await window.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
- await postPath("/toggle_lrpt_decode");
+ await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
+ await satWindow.postPath?.("/toggle_lrpt_decode");
} catch (e) {
console.error("LRPT toggle failed", e);
}
-});
+})(); });
// ── Filter / sort event listeners ───────────────────────────────────
-satDom.filterInput?.addEventListener("input", () => {
- satFilterText = satDom.filterInput.value.trim().toUpperCase();
+const satFilterInput = satDom.filterInput;
+satFilterInput?.addEventListener("input", () => {
+ satFilterText = satFilterInput.value.trim().toUpperCase();
renderSatHistoryTable();
});
-satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
-satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
+satDom.sortSelect?.addEventListener("change", () => { renderSatHistoryTable(); });
+satDom.typeFilter?.addEventListener("change", () => { renderSatHistoryTable(); });
// ── Settings: clear history ─────────────────────────────────────────
document
.getElementById("settings-clear-sat-history")
- ?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ ?.addEventListener("click", () => { void (async () => {
+ if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
- await postPath("/clear_lrpt_decode");
- window.resetSatHistoryView();
+ await satWindow.postPath?.("/clear_lrpt_decode");
+ satWindow.resetSatHistoryView?.();
} catch (e) {
console.error("Weather satellite history clear failed", e);
}
- });
+ })(); });
// ── Predictions: helpers ────────────────────────────────────────────
-function azToCardinal(deg) {
+function azToCardinal(deg: number): string {
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
- return dirs[Math.round(deg / 45) % 8];
+ return dirs[Math.round(deg / 45) % 8] ?? "N";
}
-function formatPredTime(ms) {
+function formatPredTime(ms: number): string {
const d = new Date(ms);
const now = new Date();
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
- const day = d.getUTCDay() !== now.getUTCDay() ? dayNames[d.getUTCDay()] + " " : "";
+ const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${day}${hh}:${mm}`;
}
-function formatPredDuration(s) {
+function formatPredDuration(s: number): string {
if (s >= 60) return `${Math.round(s / 60)} min`;
return `${s}s`;
}
-function formatCountdown(ms) {
+function formatCountdown(ms: number): string {
const totalSec = Math.max(0, Math.floor(ms / 1000));
const m = Math.floor(totalSec / 60);
const s = totalSec % 60;
return `${m}:${String(s).padStart(2, "0")}`;
}
-function elevationClass(deg) {
+function elevationClass(deg: number): string {
if (deg >= 45) return "sat-pred-el-high";
if (deg >= 10) return "sat-pred-el-mid";
return "sat-pred-el-low";
@@ -346,8 +368,8 @@ function stopCountdownTimer() {
}
}
-function startCountdownTimer(container) {
- const countdownEls = container ? container.querySelectorAll(".sat-pred-col-countdown") : [];
+function startCountdownTimer(container: HTMLElement | null): void {
+ const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
if (countdownEls.length === 0) return;
satPredCountdownTimer = setInterval(() => {
@@ -358,7 +380,7 @@ function startCountdownTimer(container) {
const n = Date.now();
let anyActive = false;
for (const el of countdownEls) {
- const los = parseInt(el.dataset.los, 10);
+ const los = Number.parseInt(el.dataset.los ?? "0", 10);
const rem = los - n;
if (rem > 0) {
el.textContent = formatCountdown(rem);
@@ -375,7 +397,7 @@ function startCountdownTimer(container) {
}
// ── Predictions: row builders ───────────────────────────────────────
-function buildCurrentPassRow(pass, now) {
+function buildCurrentPassRow(pass: SatellitePass, now: number): HTMLElement {
const row = document.createElement("div");
row.className = "sat-pred-row-current";
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
@@ -391,7 +413,7 @@ function buildCurrentPassRow(pass, now) {
return row;
}
-function buildUpcomingPassRow(pass) {
+function buildUpcomingPassRow(pass: SatellitePass): HTMLElement {
const row = document.createElement("div");
row.className = "sat-pred-row";
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
@@ -406,7 +428,7 @@ function buildUpcomingPassRow(pass) {
}
// ── Predictions: filter state ───────────────────────────────────────
-function getFilteredPredictions() {
+function getFilteredPredictions(): SatellitePass[] {
let items = satPredData;
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
@@ -418,23 +440,26 @@ function applyPredFilters() {
renderSatPredictions(getFilteredPredictions());
}
-satDom.predFilter?.addEventListener("input", () => {
- satPredFilterText = satDom.predFilter.value.trim().toUpperCase();
+const satPredictionFilter = satDom.predFilter;
+satPredictionFilter?.addEventListener("input", () => {
+ satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
applyPredFilters();
});
-satDom.predMinEl?.addEventListener("change", () => {
- satPredMinEl = parseInt(satDom.predMinEl.value, 10) || 0;
+const satPredictionMinElevation = satDom.predMinEl;
+satPredictionMinElevation?.addEventListener("change", () => {
+ satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
applyPredFilters();
});
-satDom.predCategory?.addEventListener("change", () => {
- satPredCategory = satDom.predCategory.value;
+const satPredictionCategory = satDom.predCategory;
+satPredictionCategory?.addEventListener("change", () => {
+ satPredCategory = satPredictionCategory.value;
applyPredFilters();
});
// ── Predictions: main render ────────────────────────────────────────
-function renderSatPredictions(passes, error) {
+function renderSatPredictions(passes: SatellitePass[], error?: string): void {
stopCountdownTimer();
if (error) {
@@ -508,14 +533,14 @@ function renderSatPredictions(passes, error) {
}
// ── Predictions: data loading ───────────────────────────────────────
-async function loadSatPredictions() {
+async function loadSatPredictions(): Promise {
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions\u2026";
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
try {
const resp = await fetch("/sat_passes");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
+ const data = await resp.json() as SatellitePassResponse;
satPredSatCount = data.satellite_count || 0;
if (data.error) {
satPredData = [];
@@ -524,20 +549,20 @@ async function loadSatPredictions() {
satPredData = data.passes || [];
renderSatPredictions(getFilteredPredictions());
}
- } catch (e) {
- renderSatPredictions([], `Failed to load predictions: ${e.message}`);
+ } catch (error: unknown) {
+ renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
}
}
// ── Navigate to map centered on satellite image bounds ──────────────
-window.satShowOnMap = function (south, west, north, east) {
- if (typeof window.enableMapSourceFilter === "function") {
- window.enableMapSourceFilter("sat");
+satWindow.satShowOnMap = function (south: number, west: number, north: number, east: number) {
+ if (typeof satWindow.enableMapSourceFilter === "function") {
+ satWindow.enableMapSourceFilter("sat");
}
const lat = (south + north) / 2;
const lon = (west + east) / 2;
- if (window.navigateToAprsMap) {
- window.navigateToAprsMap(lat, lon);
+ if (satWindow.navigateToAprsMap) {
+ satWindow.navigateToAprsMap(lat, lon);
}
};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/satellite-types.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/satellite-types.ts
new file mode 100644
index 00000000..ad069e63
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/satellite-types.ts
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+export interface SatelliteImage {
+ ts_ms?: number | null;
+ satellite?: string;
+ channels?: string;
+ channel_a?: string;
+ channel_b?: string;
+ mcu_count?: number;
+ line_count?: number;
+ path?: string;
+ geo_bounds?: [number, number, number, number];
+ _tsMs?: number;
+ _ts?: string;
+ _decoder?: "lrpt";
+}
+
+export interface LrptProgress { mcu_count?: number }
+export interface SatelliteLiveUpdate { lrpt_decode_enabled?: boolean }
+
+export interface SatellitePass {
+ satellite: string;
+ category: string;
+ aos_ms: number;
+ los_ms: number;
+ duration_s: number;
+ azimuth_aos_deg: number;
+ azimuth_los_deg: number;
+ max_elevation_deg: number;
+}
+
+export interface SatellitePassResponse {
+ satellite_count?: number;
+ error?: string | null;
+ passes?: SatellitePass[];
+}
+
+export interface SatelliteScheduleEntry {
+ id: string;
+ satellite: string;
+ norad_id: number;
+ bookmark_id: string;
+ min_elevation_deg: number;
+ priority: number;
+ center_hz: number | null;
+ bookmark_ids: string[];
+}
+
+export interface SatelliteScheduleConfig {
+ enabled: boolean;
+ pretune_secs: number;
+ entries: SatelliteScheduleEntry[];
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/sat.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/sat.test.mjs
new file mode 100644
index 00000000..6c2de006
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/sat.test.mjs
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// 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 entry registers lifecycle callbacks and forwards georeferenced images", async () => {
+ const overlays = [];
+ const window = {
+ trxUi: { confirm: async () => true },
+ addSatMapOverlay: (image) => { overlays.push(image); },
+ };
+ const context = vm.createContext({
+ window,
+ document: { getElementById: () => null, createDocumentFragment: () => ({ appendChild() {} }) },
+ fetch: async () => ({ ok: true, json: async () => ({ satellite_count: 0, passes: [] }) }),
+ setInterval: () => 1,
+ clearInterval() {},
+ Date,
+ Number,
+ String,
+ Array,
+ Error,
+ console,
+ });
+ const source = await readFile(new URL("../../assets/web/generated/sat.js", import.meta.url), "utf8");
+ new vm.Script(source).runInContext(context);
+
+ window.onServerLrptImage({ path: "/images/lrpt.png", geo_bounds: [50, 10, 55, 20], mcu_count: 100 });
+ assert.equal(overlays.length, 1);
+ assert.deepEqual(overlays[0].geo_bounds, [50, 10, 55, 20]);
+ assert.equal(typeof window.updateSatLiveState, "function");
+ assert.equal(typeof window.resetSatHistoryView, "function");
+});