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"); +});