From 9920094008d4da8f925658ec01ad8c027634f64d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 17:50:49 +0000 Subject: [PATCH] [fix](trx-frontend-http): fix SAT prediction page degrading whole-page rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues in the satellite predictions view caused page-wide rendering performance degradation: 1. Unbounded DOM nodes: All satellite passes (200+ satellites × multiple passes = 500-1000 rows with 5 spans each) were rendered at once, creating thousands of DOM nodes that slowed style recalculation and layout across the entire page. Now caps at 50 visible rows with a "Show more" button. 2. No DOM cleanup on view switch: Prediction rows persisted in the DOM when navigating away from the predictions view or the SAT tab, bloating the page DOM indefinitely. Now clears prediction DOM when leaving the predictions view or switching decoder tabs. 3. Countdown timer never paused: The 1-second setInterval with querySelectorAll kept running even when the predictions view was hidden, wasting CPU on invisible DOM queries. Now only runs when predictions view is active, caches element references instead of querying the DOM each tick, and auto-pauses when the view is hidden. Also caches prediction DOM element references at module init instead of calling getElementById on every render invocation. https://claude.ai/code/session_01G6wuNCkckbHHsU7w5zCtW2 Signed-off-by: Claude --- .../trx-frontend-http/assets/web/app.js | 4 + .../assets/web/plugins/sat.js | 109 ++++++++++++------ 2 files changed, 78 insertions(+), 35 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 5190288..f322b9e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -7680,6 +7680,10 @@ document.querySelectorAll(".sub-tab-bar").forEach((bar) => { if (window.refreshCwTonePicker) window.refreshCwTonePicker(); }); } + // Clear SAT prediction DOM when leaving the SAT tab to reduce node count. + if (btn.dataset.subtab !== "sat" && typeof window.clearSatPredictionDom === "function") { + window.clearSatPredictionDom(); + } }); }); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js index 0039984..3ca1197 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js @@ -20,6 +20,8 @@ const satLrptState = document.getElementById("sat-lrpt-state"); // ── State ─────────────────────────────────────────────────────────── let satImageHistory = []; const SAT_MAX_IMAGES = 100; +const SAT_PRED_PAGE_SIZE = 50; // max rows before "Show more" +let satPredShowAll = false; let satFilterText = ""; let satActiveView = "live"; // "live" | "history" | "predictions" @@ -38,6 +40,7 @@ const satViewHistoryBtn = document.getElementById("sat-view-history"); const satViewPredictionsBtn = document.getElementById("sat-view-predictions"); function switchSatView(view) { + const leavingPredictions = satActiveView === "predictions" && view !== "predictions"; satActiveView = view; if (satLiveView) satLiveView.style.display = view === "live" ? "" : "none"; if (satHistoryView) satHistoryView.style.display = view === "history" ? "" : "none"; @@ -45,13 +48,25 @@ function switchSatView(view) { if (satViewLiveBtn) satViewLiveBtn.classList.toggle("sat-view-active", view === "live"); if (satViewHistoryBtn) satViewHistoryBtn.classList.toggle("sat-view-active", view === "history"); if (satViewPredictionsBtn) satViewPredictionsBtn.classList.toggle("sat-view-active", view === "predictions"); + // Clear prediction DOM when leaving to reduce node count. + if (leavingPredictions) { + clearPredictionDom(); + } if (view === "history") { renderSatHistoryTable(); } else if (view === "predictions") { + satPredShowAll = false; loadSatPredictions(); } } +function clearPredictionDom() { + if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } + if (satPredCurrentList) satPredCurrentList.innerHTML = ""; + if (satPredUpcomingList) satPredUpcomingList.innerHTML = ""; +} +window.clearSatPredictionDom = clearPredictionDom; + satViewLiveBtn?.addEventListener("click", () => switchSatView("live")); satViewHistoryBtn?.addEventListener("click", () => switchSatView("history")); satViewPredictionsBtn?.addEventListener("click", () => switchSatView("predictions")); @@ -298,6 +313,11 @@ let satPredCountdownTimer = null; const satPredFilterInput = document.getElementById("sat-pred-filter"); const satPredMinElSelect = document.getElementById("sat-pred-min-el"); const satPredCategorySelect = document.getElementById("sat-pred-category"); +const satPredCurrentList = document.getElementById("sat-pred-current-list"); +const satPredUpcomingList = document.getElementById("sat-pred-list"); +const satPredCurrentSection = document.getElementById("sat-pred-current-section"); +const satPredUpcomingSection = document.getElementById("sat-pred-upcoming-section"); +const satPredStatus = document.getElementById("sat-pred-status"); function getFilteredPredictions() { let items = satPredData; @@ -362,11 +382,11 @@ function formatCountdown(ms) { } function renderSatPredictions(passes, error) { - const currentList = document.getElementById("sat-pred-current-list"); - const upcomingList = document.getElementById("sat-pred-list"); - const currentSection = document.getElementById("sat-pred-current-section"); - const upcomingSection = document.getElementById("sat-pred-upcoming-section"); - const status = document.getElementById("sat-pred-status"); + const currentList = satPredCurrentList; + const upcomingList = satPredUpcomingList; + const currentSection = satPredCurrentSection; + const upcomingSection = satPredUpcomingSection; + const status = satPredStatus; // Stop any previous countdown timer if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } @@ -424,11 +444,14 @@ function renderSatPredictions(passes, error) { } } - // ── Upcoming passes ── + // ── Upcoming passes (capped to reduce DOM node count) ── + const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE; + const visibleUpcoming = upcoming.slice(0, upcomingLimit); + const hiddenCount = upcoming.length - visibleUpcoming.length; if (upcomingSection) upcomingSection.style.display = upcoming.length > 0 ? "" : "none"; if (upcomingList) { const frag = document.createDocumentFragment(); - for (const pass of upcoming) { + for (const pass of visibleUpcoming) { const row = document.createElement("div"); row.className = "sat-pred-row"; const elClass = pass.max_elevation_deg >= 45 @@ -446,50 +469,66 @@ function renderSatPredictions(passes, error) { ].join(""); frag.appendChild(row); } + 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\u2026`; + moreRow.addEventListener("click", () => { + satPredShowAll = true; + renderSatPredictions(getFilteredPredictions()); + }); + frag.appendChild(moreRow); + } upcomingList.replaceChildren(frag); } // ── Status ── if (status) { - const totalAll = getFilteredPredictions().length; let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`; if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`; status.textContent = text; } // ── Countdown timer: update "time left" every second ── - if (current.length > 0) { - satPredCountdownTimer = setInterval(() => { - const n = Date.now(); - const els = document.querySelectorAll("#sat-pred-current-list .sat-pred-col-countdown"); - let anyActive = false; - for (const el of els) { - 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"; + // Only run when predictions view is actually visible. + if (current.length > 0 && satActiveView === "predictions") { + const countdownEls = currentList ? currentList.querySelectorAll(".sat-pred-col-countdown") : []; + if (countdownEls.length > 0) { + satPredCountdownTimer = setInterval(() => { + // Pause timer if predictions view was hidden (e.g. switched tabs). + if (satActiveView !== "predictions") { + clearInterval(satPredCountdownTimer); + satPredCountdownTimer = null; + return; } - } - if (!anyActive) { - // All current passes ended — re-render to move them out - clearInterval(satPredCountdownTimer); - satPredCountdownTimer = null; - renderSatPredictions(getFilteredPredictions()); - } - }, 1000); + 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"; + } + } + if (!anyActive) { + clearInterval(satPredCountdownTimer); + satPredCountdownTimer = null; + renderSatPredictions(getFilteredPredictions()); + } + }, 1000); + } } } async function loadSatPredictions() { - const status = document.getElementById("sat-pred-status"); - const currentList = document.getElementById("sat-pred-current-list"); - const upcomingList = document.getElementById("sat-pred-list"); - if (status) status.textContent = "Loading predictions\u2026"; - if (currentList) currentList.innerHTML = ""; - if (upcomingList) upcomingList.innerHTML = ""; + if (satPredStatus) satPredStatus.textContent = "Loading predictions\u2026"; + if (satPredCurrentList) satPredCurrentList.innerHTML = ""; + if (satPredUpcomingList) satPredUpcomingList.innerHTML = ""; try { const resp = await fetch("/sat_passes"); if (!resp.ok) throw new Error(`HTTP ${resp.status}`);