From 1e5bc0054d33ba8e53f5c9d0611fe38e8f8e0206 Mon Sep 17 00:00:00 2001 From: mitchross Date: Wed, 25 Mar 2026 01:13:37 -0400 Subject: [PATCH] Enhance weather satellite UX with pass geometry, guides, and wider predictions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass prediction improvements: - Widen prediction window to 48h at 5° min elevation (was 24h/15°) - Add AOS/TCA/LOS pass geometry detail panel with times and bearings - Fix duration display (was showing seconds labeled as minutes) - Enhanced pass cards with AOS/LOS times, bearings, and directions - Add REFRESH button in passes panel header - Better empty state with clear "set your location" prompt and icon Countdown and visual: - Pulse animation on countdown when pass is imminent or active - Countdown numbers scale up and change color for urgency Sidebar getting started guide: - New "Getting Started" section explaining what Meteor satellites are, polar orbits, 4-8 passes/day, step-by-step workflow - "When to look" tips (elevation, day vs night, pass direction) - "What you need" equipment table with costs - Collapsed antenna guide by default to reduce initial overwhelm - Improved offline decode section with clear instructions on where to get IQ recordings Co-Authored-By: Claude Opus 4.6 (1M context) --- static/css/modes/weather-satellite.css | 81 ++++++++++++++++ static/js/modes/weather-satellite.js | 86 +++++++++++++--- templates/index.html | 28 +++++- .../partials/modes/weather-satellite.html | 97 +++++++++++++++++-- 4 files changed, 268 insertions(+), 24 deletions(-) diff --git a/static/css/modes/weather-satellite.css b/static/css/modes/weather-satellite.css index bbdc3b8..00b078d 100644 --- a/static/css/modes/weather-satellite.css +++ b/static/css/modes/weather-satellite.css @@ -408,6 +408,87 @@ margin: 0 2px; } +/* ===== Pass Geometry Detail ===== */ +.wxsat-pass-geometry { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 14px; + background: var(--bg-primary, #0d1117); + border-bottom: 1px solid var(--border-color, #2a3040); + font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; +} + +.wxsat-geom-event { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + min-width: 60px; +} + +.wxsat-geom-event.wxsat-geom-tca { + color: var(--neon-green); +} + +.wxsat-geom-label { + font-size: 9px; + font-weight: 600; + color: var(--text-dim, #666); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.wxsat-geom-tca .wxsat-geom-label { + color: var(--neon-green); +} + +.wxsat-geom-time { + font-size: 12px; + font-weight: 600; + color: var(--text-primary, #e0e0e0); +} + +.wxsat-geom-tca .wxsat-geom-time { + color: var(--neon-green); +} + +.wxsat-geom-az { + font-size: 10px; + color: var(--text-dim, #666); +} + +.wxsat-geom-arrow { + font-size: 14px; + color: var(--text-dim, #444); +} + +.wxsat-geom-meta { + font-size: 10px; + color: var(--text-dim, #666); + margin-left: 8px; + padding-left: 8px; + border-left: 1px solid var(--border-color, #2a3040); + white-space: nowrap; +} + +/* ===== Countdown Pulse Animation ===== */ +.wxsat-countdown-box.imminent .wxsat-cd-value { + animation: wxsat-count-pulse 1s ease-in-out infinite; + color: var(--accent-yellow); +} + +.wxsat-countdown-box.active .wxsat-cd-value { + animation: wxsat-count-pulse 1s ease-in-out infinite; + color: var(--neon-green); +} + +@keyframes wxsat-count-pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.15); opacity: 0.8; } +} + /* ===== Pass Predictions Panel ===== */ .wxsat-passes-panel { flex: 0 0 280px; diff --git a/static/js/modes/weather-satellite.js b/static/js/modes/weather-satellite.js index 4fc3db5..44f47cf 100644 --- a/static/js/modes/weather-satellite.js +++ b/static/js/modes/weather-satellite.js @@ -751,7 +751,7 @@ const WeatherSat = (function() { } try { - const url = `/weather-sat/passes?latitude=${storedLat}&longitude=${storedLon}&hours=24&min_elevation=15&trajectory=true&ground_track=true`; + const url = `/weather-sat/passes?latitude=${storedLat}&longitude=${storedLon}&hours=48&min_elevation=5&trajectory=true&ground_track=true`; const response = await fetch(url); const data = await response.json(); @@ -815,6 +815,42 @@ const WeatherSat = (function() { // Update polar panel subtitle const polarSat = document.getElementById('wxsatPolarSat'); if (polarSat) polarSat.textContent = `${pass.name} ${pass.maxEl}\u00b0`; + + // Update pass geometry detail panel + updatePassGeometry(pass); + } + + /** + * Update the AOS/TCA/LOS pass geometry detail panel. + */ + function updatePassGeometry(pass) { + const panel = document.getElementById('wxsatPassGeometry'); + if (!panel) return; + + if (!pass) { + panel.style.display = 'none'; + return; + } + panel.style.display = 'flex'; + + const aosTime = document.getElementById('wxsatGeomAosTime'); + const aosAz = document.getElementById('wxsatGeomAosAz'); + const tcaEl = document.getElementById('wxsatGeomTcaEl'); + const tcaAz = document.getElementById('wxsatGeomTcaAz'); + const losTime = document.getElementById('wxsatGeomLosTime'); + const losAz = document.getElementById('wxsatGeomLosAz'); + const meta = document.getElementById('wxsatGeomMeta'); + + const tzLabel = getTZLabel(); + if (aosTime) aosTime.textContent = formatShortTime(pass.startTimeISO) + tzLabel; + if (aosAz) aosAz.textContent = `${Math.round(pass.riseAz || 0)}\u00b0 ${azToDir(pass.riseAz)}`; + if (tcaEl) tcaEl.textContent = `${pass.maxEl}\u00b0 el`; + if (tcaAz) tcaAz.textContent = `${Math.round(pass.maxElAz || pass.tcaAz || 0)}\u00b0 ${azToDir(pass.maxElAz || pass.tcaAz)}`; + if (losTime) losTime.textContent = formatShortTime(pass.endTimeISO) + tzLabel; + if (losAz) losAz.textContent = `${Math.round(pass.setAz || 0)}\u00b0 ${azToDir(pass.setAz)}`; + + const durMin = Math.round((pass.duration || 0) / 60); + if (meta) meta.textContent = `${durMin} min / ${pass.quality}`; } /** @@ -829,12 +865,28 @@ const WeatherSat = (function() { if (!container) return; if (passList.length === 0) { - const hasLocation = localStorage.getItem('observerLat') !== null; + const hasLocation = localStorage.getItem('observerLat') !== null || + (window.ObserverLocation && ObserverLocation.isSharedEnabled() && ObserverLocation.getShared()?.lat); container.innerHTML = ` `; + // Hide geometry panel when no passes + const geom = document.getElementById('wxsatPassGeometry'); + if (geom) geom.style.display = 'none'; return; } @@ -866,6 +918,10 @@ const WeatherSat = (function() { const riseDir = azToDir(pass.riseAz); const setDir = azToDir(pass.setAz); const bestBadge = isBest ? 'BEST' : ''; + const durMin = Math.round((pass.duration || 0) / 60); + const aosStr = formatShortTime(pass.startTimeISO); + const losStr = formatShortTime(pass.endTimeISO); + const tzLabel = getTZLabel(); return `
@@ -874,13 +930,13 @@ const WeatherSat = (function() { ${escapeHtml(pass.mode)}
- Time - ${escapeHtml(timeStr)} - Max El - ${pass.maxEl}° - Duration - ${pass.duration} min - Direction + AOS + ${escapeHtml(aosStr)}${escapeHtml(tzLabel)} · ${Math.round(pass.riseAz || 0)}° ${riseDir} + LOS + ${escapeHtml(losStr)}${escapeHtml(tzLabel)} · ${Math.round(pass.setAz || 0)}° ${setDir} + Peak + ${pass.maxEl}° el · ${durMin} min + Track ${riseDir} ${setDir}
@@ -1456,10 +1512,11 @@ const WeatherSat = (function() { detailEl.textContent = `ACTIVE - ${nextPass.maxEl}\u00b0 max el`; } else { const bestPass = findBestPass(filtered); + const durMin = Math.round((nextPass.duration || 0) / 60); const bestNote = bestPass && bestPass.startTimeISO !== nextPass.startTimeISO ? ` | Best: ${bestPass.name} ${formatShortTime(bestPass.startTimeISO)}${getTZLabel()} (${bestPass.maxEl}\u00b0)` : ''; - detailEl.textContent = `${passTimeStr} / ${nextPass.maxEl}\u00b0 max el / ${nextPass.duration} min${bestNote}`; + detailEl.textContent = `${passTimeStr} / ${nextPass.maxEl}\u00b0 max el / ${durMin} min${bestNote}`; } } @@ -1592,7 +1649,8 @@ const WeatherSat = (function() { if (bestEl) { if (best) { const t = formatShortTime(best.startTimeISO) + getTZLabel(); - bestEl.textContent = `Best: ${best.name} at ${t} (${best.maxEl}\u00b0 el, ${best.duration} min)`; + const bestDurMin = Math.round((best.duration || 0) / 60); + bestEl.textContent = `Best: ${best.name} at ${t} (${best.maxEl}\u00b0 el, ${bestDurMin} min)`; } else { bestEl.textContent = 'No upcoming passes'; } @@ -2343,13 +2401,13 @@ const WeatherSat = (function() { const offsets = [25, 95, 200, 340, 510, 720, 880, 1020]; const elevations = [72, 45, 28, 63, 18, 55, 82, 35]; - const durations = [14, 12, 8, 13, 6, 11, 15, 10]; + const durations = [840, 720, 480, 780, 360, 660, 900, 600]; // seconds const riseAzs = [350, 15, 200, 310, 170, 40, 280, 90]; const setAzs = [170, 195, 20, 130, 350, 220, 100, 270]; offsets.forEach((offset, i) => { const start = new Date(now.getTime() + offset * 60000); - const end = new Date(start.getTime() + durations[i] * 60000); + const end = new Date(start.getTime() + durations[i] * 1000); const sat = demoSats[i % 2]; const el = elevations[i]; const quality = el >= 60 ? 'excellent' : el >= 30 ? 'good' : 'fair'; diff --git a/templates/index.html b/templates/index.html index 68b38ff..585d48b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2612,10 +2612,36 @@
Upcoming Passes 0 + +
+ +
diff --git a/templates/partials/modes/weather-satellite.html b/templates/partials/modes/weather-satellite.html index 48a4697..702aa84 100644 --- a/templates/partials/modes/weather-satellite.html +++ b/templates/partials/modes/weather-satellite.html @@ -11,6 +11,74 @@

+ +
+

Getting Started

+
+ +
+ What are Meteor satellites? +

+ Russia's Meteor-M2-3 and Meteor-M2-4 + are polar-orbiting weather satellites that continuously transmit real-time color imagery (clouds, land, sea) at 137.900 MHz + using the LRPT digital format. Unlike old analog NOAA APT, LRPT produces sharp, full-color images. +

+

+ They orbit ~830 km high, circling the Earth every ~100 minutes in a near-polar sun-synchronous orbit. + From any location, you'll typically get 4–8 usable passes per day, + each lasting 8–15 minutes as the satellite crosses your sky. +

+
+ +
+ Step-by-step +
    +
  1. Set your location — Enter your lat/lon in the strip bar above (or click GPS). This is required for pass predictions.
  2. +
  3. Check upcoming passes — The pass list shows when each satellite will be overhead. Higher max elevation = better signal. Passes above 30° are "good", above 60° are "excellent".
  4. +
  5. Prepare your antenna — You need a 137 MHz antenna outdoors with clear sky (see Antenna Guide below). A $5 V-dipole works for high passes.
  6. +
  7. Click Capture on a pass card when it's about to start, or enable AUTO to let the scheduler capture automatically.
  8. +
  9. Wait for images — SatDump will tune, lock the signal, and decode. Decoded images appear in the gallery after the pass completes.
  10. +
+
+ +
+ When to look +
    +
  • Best passes: When the satellite is high overhead (>30° elevation). The countdown timer shows the next one.
  • +
  • Day vs night: Daytime passes produce visible-light imagery. Night passes still work but only produce infrared/thermal images.
  • +
  • Both satellites share 137.9 MHz so they won't transmit at the same time. You'll see separate pass predictions for each.
  • +
  • Pass direction: Meteor satellites travel roughly north→south or south→north. The pass cards show the exact rise/set direction.
  • +
+
+ +
+ What you need + + + + + + + + + + + + + + + + + + + + + +
SDR receiverRTL-SDR V3/V4 ($25-35)
Antenna137 MHz V-dipole ($5 DIY) or QFH ($20-30)
LNA (optional)137 MHz filtered, at antenna ($15-25)
LocationOutdoors, clear sky view
No hardware?Use Load Demo Data below to explore the UI
+
+
+
+

Satellite

@@ -33,10 +101,13 @@
- +
-

Antenna Guide

-
+

+ Antenna Guide + +

+