[fix](trx-frontend-http): fix SAT prediction page degrading whole-page rendering

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 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 17:50:49 +00:00
committed by Stan Grams
parent 842ee6f076
commit 9920094008
2 changed files with 78 additions and 35 deletions
@@ -7680,6 +7680,10 @@ document.querySelectorAll(".sub-tab-bar").forEach((bar) => {
if (window.refreshCwTonePicker) window.refreshCwTonePicker(); 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();
}
}); });
}); });
@@ -20,6 +20,8 @@ const satLrptState = document.getElementById("sat-lrpt-state");
// ── State ─────────────────────────────────────────────────────────── // ── State ───────────────────────────────────────────────────────────
let satImageHistory = []; let satImageHistory = [];
const SAT_MAX_IMAGES = 100; const SAT_MAX_IMAGES = 100;
const SAT_PRED_PAGE_SIZE = 50; // max rows before "Show more"
let satPredShowAll = false;
let satFilterText = ""; let satFilterText = "";
let satActiveView = "live"; // "live" | "history" | "predictions" let satActiveView = "live"; // "live" | "history" | "predictions"
@@ -38,6 +40,7 @@ const satViewHistoryBtn = document.getElementById("sat-view-history");
const satViewPredictionsBtn = document.getElementById("sat-view-predictions"); const satViewPredictionsBtn = document.getElementById("sat-view-predictions");
function switchSatView(view) { function switchSatView(view) {
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
satActiveView = view; satActiveView = view;
if (satLiveView) satLiveView.style.display = view === "live" ? "" : "none"; if (satLiveView) satLiveView.style.display = view === "live" ? "" : "none";
if (satHistoryView) satHistoryView.style.display = view === "history" ? "" : "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 (satViewLiveBtn) satViewLiveBtn.classList.toggle("sat-view-active", view === "live");
if (satViewHistoryBtn) satViewHistoryBtn.classList.toggle("sat-view-active", view === "history"); if (satViewHistoryBtn) satViewHistoryBtn.classList.toggle("sat-view-active", view === "history");
if (satViewPredictionsBtn) satViewPredictionsBtn.classList.toggle("sat-view-active", view === "predictions"); 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") { if (view === "history") {
renderSatHistoryTable(); renderSatHistoryTable();
} else if (view === "predictions") { } else if (view === "predictions") {
satPredShowAll = false;
loadSatPredictions(); 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")); satViewLiveBtn?.addEventListener("click", () => switchSatView("live"));
satViewHistoryBtn?.addEventListener("click", () => switchSatView("history")); satViewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
satViewPredictionsBtn?.addEventListener("click", () => switchSatView("predictions")); satViewPredictionsBtn?.addEventListener("click", () => switchSatView("predictions"));
@@ -298,6 +313,11 @@ let satPredCountdownTimer = null;
const satPredFilterInput = document.getElementById("sat-pred-filter"); const satPredFilterInput = document.getElementById("sat-pred-filter");
const satPredMinElSelect = document.getElementById("sat-pred-min-el"); const satPredMinElSelect = document.getElementById("sat-pred-min-el");
const satPredCategorySelect = document.getElementById("sat-pred-category"); 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() { function getFilteredPredictions() {
let items = satPredData; let items = satPredData;
@@ -362,11 +382,11 @@ function formatCountdown(ms) {
} }
function renderSatPredictions(passes, error) { function renderSatPredictions(passes, error) {
const currentList = document.getElementById("sat-pred-current-list"); const currentList = satPredCurrentList;
const upcomingList = document.getElementById("sat-pred-list"); const upcomingList = satPredUpcomingList;
const currentSection = document.getElementById("sat-pred-current-section"); const currentSection = satPredCurrentSection;
const upcomingSection = document.getElementById("sat-pred-upcoming-section"); const upcomingSection = satPredUpcomingSection;
const status = document.getElementById("sat-pred-status"); const status = satPredStatus;
// Stop any previous countdown timer // Stop any previous countdown timer
if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } 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 (upcomingSection) upcomingSection.style.display = upcoming.length > 0 ? "" : "none";
if (upcomingList) { if (upcomingList) {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
for (const pass of upcoming) { for (const pass of visibleUpcoming) {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "sat-pred-row"; row.className = "sat-pred-row";
const elClass = pass.max_elevation_deg >= 45 const elClass = pass.max_elevation_deg >= 45
@@ -446,24 +469,43 @@ function renderSatPredictions(passes, error) {
].join(""); ].join("");
frag.appendChild(row); 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 = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes\u2026</span>`;
moreRow.addEventListener("click", () => {
satPredShowAll = true;
renderSatPredictions(getFilteredPredictions());
});
frag.appendChild(moreRow);
}
upcomingList.replaceChildren(frag); upcomingList.replaceChildren(frag);
} }
// ── Status ── // ── Status ──
if (status) { if (status) {
const totalAll = getFilteredPredictions().length;
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`; let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`; if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
status.textContent = text; status.textContent = text;
} }
// ── Countdown timer: update "time left" every second ── // ── Countdown timer: update "time left" every second ──
if (current.length > 0) { // 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(() => { satPredCountdownTimer = setInterval(() => {
// Pause timer if predictions view was hidden (e.g. switched tabs).
if (satActiveView !== "predictions") {
clearInterval(satPredCountdownTimer);
satPredCountdownTimer = null;
return;
}
const n = Date.now(); const n = Date.now();
const els = document.querySelectorAll("#sat-pred-current-list .sat-pred-col-countdown");
let anyActive = false; let anyActive = false;
for (const el of els) { for (const el of countdownEls) {
const los = parseInt(el.dataset.los, 10); const los = parseInt(el.dataset.los, 10);
const rem = los - n; const rem = los - n;
if (rem > 0) { if (rem > 0) {
@@ -474,7 +516,6 @@ function renderSatPredictions(passes, error) {
} }
} }
if (!anyActive) { if (!anyActive) {
// All current passes ended — re-render to move them out
clearInterval(satPredCountdownTimer); clearInterval(satPredCountdownTimer);
satPredCountdownTimer = null; satPredCountdownTimer = null;
renderSatPredictions(getFilteredPredictions()); renderSatPredictions(getFilteredPredictions());
@@ -482,14 +523,12 @@ function renderSatPredictions(passes, error) {
}, 1000); }, 1000);
} }
} }
}
async function loadSatPredictions() { async function loadSatPredictions() {
const status = document.getElementById("sat-pred-status"); if (satPredStatus) satPredStatus.textContent = "Loading predictions\u2026";
const currentList = document.getElementById("sat-pred-current-list"); if (satPredCurrentList) satPredCurrentList.innerHTML = "";
const upcomingList = document.getElementById("sat-pred-list"); if (satPredUpcomingList) satPredUpcomingList.innerHTML = "";
if (status) status.textContent = "Loading predictions\u2026";
if (currentList) currentList.innerHTML = "";
if (upcomingList) upcomingList.innerHTML = "";
try { try {
const resp = await fetch("/sat_passes"); const resp = await fetch("/sat_passes");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`);