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 4f3c3b7..a11cca1 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 @@ -866,6 +866,12 @@
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 77fde64..0039984 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 @@ -292,12 +292,18 @@ document let satPredData = []; let satPredFilterText = ""; let satPredMinEl = 0; +let satPredCategory = "all"; let satPredSatCount = 0; +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"); 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); } @@ -307,14 +313,23 @@ function getFilteredPredictions() { return items; } +function applyPredFilters() { + renderSatPredictions(getFilteredPredictions()); +} + satPredFilterInput?.addEventListener("input", () => { satPredFilterText = satPredFilterInput.value.trim().toUpperCase(); - renderSatPredictions(getFilteredPredictions()); + applyPredFilters(); }); satPredMinElSelect?.addEventListener("change", () => { satPredMinEl = parseInt(satPredMinElSelect.value, 10) || 0; - renderSatPredictions(getFilteredPredictions()); + applyPredFilters(); +}); + +satPredCategorySelect?.addEventListener("change", () => { + satPredCategory = satPredCategorySelect.value; + applyPredFilters(); }); function azToCardinal(deg) { @@ -339,55 +354,142 @@ function formatPredDuration(s) { return `${s}s`; } +function formatCountdown(ms) { + 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 renderSatPredictions(passes, error) { - const list = document.getElementById("sat-pred-list"); + 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"); - if (!list) return; + + // Stop any previous countdown timer + if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } if (error) { - list.innerHTML = ""; + if (currentList) currentList.innerHTML = ""; + if (upcomingList) upcomingList.innerHTML = ""; + if (currentSection) currentSection.style.display = "none"; + if (upcomingSection) upcomingSection.style.display = "none"; if (status) status.textContent = error; return; } if (!Array.isArray(passes) || passes.length === 0) { - list.innerHTML = ""; + if (currentList) currentList.innerHTML = ""; + if (upcomingList) upcomingList.innerHTML = ""; + if (currentSection) currentSection.style.display = "none"; + if (upcomingSection) upcomingSection.style.display = "none"; if (status) status.textContent = "No passes found in the next 24 hours."; return; } - const fragment = document.createDocumentFragment(); - for (const pass of passes) { - const row = document.createElement("div"); - row.className = "sat-pred-row"; - const elClass = pass.max_elevation_deg >= 45 - ? "sat-pred-el-high" - : pass.max_elevation_deg >= 10 - ? "sat-pred-el-mid" - : "sat-pred-el-low"; - 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(""); - fragment.appendChild(row); + 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); + + // ── Current passes ── + if (currentSection) currentSection.style.display = current.length > 0 ? "" : "none"; + if (currentList) { + if (current.length === 0) { + currentList.innerHTML = ""; + } else { + const frag = document.createDocumentFragment(); + for (const pass of current) { + const row = document.createElement("div"); + row.className = "sat-pred-row-current"; + const elClass = pass.max_elevation_deg >= 45 + ? "sat-pred-el-high" + : pass.max_elevation_deg >= 10 + ? "sat-pred-el-mid" + : "sat-pred-el-low"; + 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(""); + frag.appendChild(row); + } + currentList.replaceChildren(frag); + } } - list.replaceChildren(fragment); + + // ── Upcoming passes ── + if (upcomingSection) upcomingSection.style.display = upcoming.length > 0 ? "" : "none"; + if (upcomingList) { + const frag = document.createDocumentFragment(); + for (const pass of upcoming) { + const row = document.createElement("div"); + row.className = "sat-pred-row"; + const elClass = pass.max_elevation_deg >= 45 + ? "sat-pred-el-high" + : pass.max_elevation_deg >= 10 + ? "sat-pred-el-mid" + : "sat-pred-el-low"; + 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(""); + frag.appendChild(row); + } + upcomingList.replaceChildren(frag); + } + + // ── Status ── if (status) { - let text = `${passes.length} pass${passes.length === 1 ? "" : "es"} in the next 24 h · times in UTC`; + 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"; + } + } + if (!anyActive) { + // All current passes ended — re-render to move them out + clearInterval(satPredCountdownTimer); + satPredCountdownTimer = null; + renderSatPredictions(getFilteredPredictions()); + } + }, 1000); + } } async function loadSatPredictions() { const status = document.getElementById("sat-pred-status"); - const list = document.getElementById("sat-pred-list"); + const currentList = document.getElementById("sat-pred-current-list"); + const upcomingList = document.getElementById("sat-pred-list"); if (status) status.textContent = "Loading predictions\u2026"; - if (list) list.innerHTML = ""; + if (currentList) currentList.innerHTML = ""; + if (upcomingList) upcomingList.innerHTML = ""; try { const resp = await fetch("/sat_passes"); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 5c6ae8d..a396a25 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -4572,9 +4572,16 @@ button:focus-visible, input:focus-visible, select:focus-visible { .sat-pred-el-high { color: var(--accent-green); font-weight: 600; } .sat-pred-el-mid { color: #f0a020; } .sat-pred-el-low { color: var(--text-muted); } +.sat-pred-section-title { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding: 0.5rem 0.4rem 0.15rem; } +.sat-pred-header-current { grid-template-columns: 1fr 4.5rem 6rem 6rem 5.5rem 6rem; } +.sat-pred-row-current { display: grid; grid-template-columns: 1fr 4.5rem 6rem 6rem 5.5rem 6rem; gap: 0.25rem; padding: 0.35rem 0.4rem; font-size: 0.82rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.04)); } +.sat-pred-row-current:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); } +.sat-pred-col-countdown { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-green); } +.sat-pred-current-empty { padding: 0.5rem 0.4rem; font-size: 0.8rem; color: var(--text-muted); } @media (max-width: 600px) { .sat-live-grid { grid-template-columns: 1fr; } .sat-history-header, .sat-history-row { grid-template-columns: 5rem 4rem 6rem 4rem 3.5rem 1fr; font-size: 0.75rem; } .sat-pred-header, .sat-pred-row { grid-template-columns: 5.5rem 1fr 4rem 4.5rem; font-size: 0.75rem; } + .sat-pred-header-current, .sat-pred-row-current { grid-template-columns: 1fr 4rem 5rem 5rem 4.5rem; font-size: 0.75rem; } .sat-pred-col-dir { display: none; } } diff --git a/src/trx-core/src/geo.rs b/src/trx-core/src/geo.rs index 4e27095..591c058 100644 --- a/src/trx-core/src/geo.rs +++ b/src/trx-core/src/geo.rs @@ -15,7 +15,7 @@ use std::sync::RwLock; use std::time::Duration; /// Result of computing upcoming passes, including metadata about TLE source. -#[derive(Debug, Clone, serde::Serialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PassPredictionResult { /// Predicted passes sorted by AOS time. pub passes: Vec