From e52c4839c9e38b95b967e64672074a6ddad5e640 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Thu, 6 Aug 2026 23:23:48 +0200 Subject: [PATCH] [fix](trx-frontend-http): give satellite passes their own page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass predictions were a third view inside the Weather Satellite Decoder card, under Digital modes — a planning tool filed behind a decoder toggle, beside the FT8 and WEFAX panels it has nothing to do with. Nothing about knowing when a bird comes over belongs there. Move them to /satellites, reached from Tools alongside Statistics, Recorder, Settings and About: occasional destinations that live behind that menu rather than taking a slot in the operating strip. Adding a sixth strip button wrapped the phone nav onto two rows and cost the desktop strip its labels at 1280px, so the tab is hidden from the strip exactly the way its four peers already are — the nav is byte-for-byte what it was. The prediction code moves out of sat.ts into its own plugin that loads with the page, so the decoder card no longer carries it. Countdowns stop when the page is hidden and each visit reloads, since passes go stale while it is closed. The server grows a /satellites index route so a deep link or a refresh serves the SPA shell rather than a 404. Closes #47 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams --- .../assets/web/generated/app.js | 14 +- .../assets/web/generated/sat.js | 234 +------------- .../web/generated/satellite-predictions.js | 228 ++++++++++++++ .../trx-frontend-http/assets/web/index.html | 95 +++--- .../trx-frontend-http/assets/web/style.css | 12 +- .../trx-frontend-http/frontend/build.mjs | 1 + .../trx-frontend-http/frontend/package.json | 2 +- .../trx-frontend-http/frontend/src/app.ts | 14 +- .../src/features/navigation/routes.ts | 3 +- .../frontend/src/plugin-loader.ts | 5 +- .../frontend/src/plugins/sat.ts | 281 +---------------- .../src/plugins/satellite-predictions.ts | 290 ++++++++++++++++++ .../trx-frontend-http/frontend/src/ui-core.ts | 2 +- .../frontend/tests/satellite-predictions.mjs | 122 ++++++++ .../frontend/tests/web-fixture.mjs | 2 + .../trx-frontend-http/src/api/assets.rs | 6 + .../trx-frontend-http/src/api/mod.rs | 1 + 17 files changed, 744 insertions(+), 568 deletions(-) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/satellite-predictions.js create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/satellite-predictions.ts create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/satellite-predictions.mjs diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js index c9dd4208..6f891962 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js @@ -885,7 +885,7 @@ function elementById(id) { if (restoreFocus) more.focus(); }; api.closeMobileOverlays = closeMore; - ["statistics", "recorder", "settings", "about"].forEach((tabName) => { + ["satellites", "statistics", "recorder", "settings", "about"].forEach((tabName) => { const source = nav.querySelector(`[data-tab="${tabName}"]`); if (!source) return; const item = document.createElement("button"); @@ -1529,6 +1529,7 @@ var TAB_ORDER = [ "bookmarks", "digital-modes", "map", + "satellites", "statistics", "recorder", "settings", @@ -1539,6 +1540,7 @@ var TAB_PATHS = { bookmarks: "/bookmarks", "digital-modes": "/digital-modes", map: "/map", + satellites: "/satellites", statistics: "/statistics", recorder: "/recorder", settings: "/settings", @@ -1756,6 +1758,7 @@ var pluginGroups = { ], "map-data": ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js"], map: ["/map-core.js", "/ais.js", "/vdes.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js"], + satellites: ["/satellite-predictions.js"], statistics: ["/map-core.js"], bookmarks: ["/bookmarks.js"], recorder: [], @@ -5844,6 +5847,7 @@ function _initMapWhenReady() { } function navigateToTab(name, options = {}) { window.trxUi?.closeMobileOverlays?.(); + const leavingSatellites = _activeTab === "satellites" && name !== "satellites"; const { updateHistory = true, replaceHistory = false } = options; if (authEnabled && !authRole && name !== "main") { showAuthGate(false); @@ -5873,7 +5877,10 @@ function navigateToTab(name, options = {}) { updateTabHistory(name, replaceHistory); } scheduleSpectrumLayout(); - void loadPluginsForTab(name).catch((error) => { + if (leavingSatellites) window.clearSatPredictionDom?.(); + void loadPluginsForTab(name).then(() => { + if (name === "satellites") window.refreshSatPredictions?.(); + }).catch((error) => { console.error(error); }); if (name === "map") { @@ -6320,9 +6327,6 @@ function _wireSubTabBar(bar) { if (window.refreshCwTonePicker) window.refreshCwTonePicker(); }); } - if (btn.dataset.subtab !== "sat" && typeof window.clearSatPredictionDom === "function") { - window.clearSatPredictionDom(); - } }); } document.querySelectorAll(".sub-tab-bar").forEach(_wireSubTabBar); 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 44bd3940..0c9ca257 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 @@ -8,7 +8,6 @@ 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"), @@ -17,29 +16,12 @@ var satDom = { 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") + viewHistoryBtn: document.getElementById("sat-view-history") }; 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); @@ -48,37 +30,19 @@ function scheduleSatUi(key, job) { 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(); - } + if (view === "history") renderSatHistoryTable(); } -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; @@ -275,200 +239,6 @@ document.getElementById("settings-clear-sat-history")?.addEventListener("click", } })(); }); -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"); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/satellite-predictions.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/satellite-predictions.js new file mode 100644 index 00000000..0c497d88 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/satellite-predictions.js @@ -0,0 +1,228 @@ +// src/plugins/satellite-predictions.ts +var predWindow = window; +var dom = { + page: document.getElementById("tab-satellites"), + filter: document.getElementById("sat-pred-filter"), + minElevation: document.getElementById("sat-pred-min-el"), + category: document.getElementById("sat-pred-category"), + currentList: document.getElementById("sat-pred-current-list"), + upcomingList: document.getElementById("sat-pred-list"), + currentSection: document.getElementById("sat-pred-current-section"), + upcomingSection: document.getElementById("sat-pred-upcoming-section"), + status: document.getElementById("sat-pred-status") +}; +var PAGE_SIZE = 50; +var predShowAll = false; +var predData = []; +var predFilterText = ""; +var predMinEl = 0; +var predCategory = "all"; +var predSatCount = 0; +var predCountdownTimer = null; +function isPageVisible() { + return !!dom.page && dom.page.style.display !== "none"; +} +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 (predCountdownTimer) { + clearInterval(predCountdownTimer); + predCountdownTimer = null; + } +} +function startCountdownTimer(container) { + const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? []; + if (countdownEls.length === 0) return; + predCountdownTimer = setInterval(() => { + if (!isPageVisible()) { + 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(); + render(filtered()); + } + }, 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 filtered() { + let items = predData; + if (predCategory !== "all") items = items.filter((p) => p.category === predCategory); + if (predMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= predMinEl); + if (predFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(predFilterText)); + return items; +} +function applyFilters() { + render(filtered()); +} +var filterInput = dom.filter; +filterInput?.addEventListener("input", () => { + predFilterText = filterInput.value.trim().toUpperCase(); + applyFilters(); +}); +var minElevationSelect = dom.minElevation; +minElevationSelect?.addEventListener("change", () => { + predMinEl = Number.parseInt(minElevationSelect.value, 10) || 0; + applyFilters(); +}); +var categorySelect = dom.category; +categorySelect?.addEventListener("change", () => { + predCategory = categorySelect.value; + applyFilters(); +}); +function render(passes, error) { + stopCountdownTimer(); + if (error) { + if (dom.currentList) dom.currentList.innerHTML = ""; + if (dom.upcomingList) dom.upcomingList.innerHTML = ""; + if (dom.currentSection) dom.currentSection.style.display = "none"; + if (dom.upcomingSection) dom.upcomingSection.style.display = "none"; + if (dom.status) dom.status.textContent = error; + return; + } + if (!Array.isArray(passes) || passes.length === 0) { + if (dom.currentList) dom.currentList.innerHTML = ""; + if (dom.upcomingList) dom.upcomingList.innerHTML = ""; + if (dom.currentSection) dom.currentSection.style.display = "none"; + if (dom.upcomingSection) dom.upcomingSection.style.display = "none"; + if (dom.status) dom.status.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 (dom.currentSection) dom.currentSection.style.display = current.length > 0 ? "" : "none"; + if (dom.currentList) { + if (current.length === 0) { + dom.currentList.innerHTML = ""; + } else { + const frag = document.createDocumentFragment(); + for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now)); + dom.currentList.replaceChildren(frag); + } + } + const upcomingLimit = predShowAll ? upcoming.length : PAGE_SIZE; + const visibleUpcoming = upcoming.slice(0, upcomingLimit); + const hiddenCount = upcoming.length - visibleUpcoming.length; + if (dom.upcomingSection) dom.upcomingSection.style.display = upcoming.length > 0 ? "" : "none"; + if (dom.upcomingList) { + 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", () => { + predShowAll = true; + render(filtered()); + }); + frag.appendChild(moreRow); + } + dom.upcomingList.replaceChildren(frag); + } + if (dom.status) { + let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`; + if (predSatCount > 0) text += ` · ${predSatCount} satellites tracked`; + dom.status.textContent = text; + } + if (current.length > 0 && isPageVisible()) { + startCountdownTimer(dom.currentList); + } +} +async function load() { + if (dom.status) dom.status.textContent = "Loading predictions…"; + if (dom.currentList) dom.currentList.innerHTML = ""; + if (dom.upcomingList) dom.upcomingList.innerHTML = ""; + try { + const resp = await fetch("/sat_passes"); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + predSatCount = data.satellite_count || 0; + if (data.error) { + predData = []; + render([], data.error); + } else { + predData = data.passes || []; + render(filtered()); + } + } catch (error) { + render([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`); + } +} +function clearPredictionDom() { + stopCountdownTimer(); + if (dom.currentList) dom.currentList.innerHTML = ""; + if (dom.upcomingList) dom.upcomingList.innerHTML = ""; +} +predWindow.clearSatPredictionDom = clearPredictionDom; +predWindow.refreshSatPredictions = function() { + predShowAll = false; + void load(); +}; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 45bc6bc1..ec936b08 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -24,6 +24,7 @@ SPDX-License-Identifier: GPL-2.0-or-later + @@ -63,6 +64,10 @@ SPDX-License-Identifier: GPL-2.0-or-later Map + -
@@ -960,50 +964,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
No images yet
- - + +