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 = `
-
${hasLocation ? 'No passes in next 24h' : 'Set location to see pass predictions'}
+
+ ${hasLocation
+ ? ' '
+ : ' '}
+
+
+ ${hasLocation ? 'No passes in next 24 hours' : 'Set your location'}
+
+
+ ${hasLocation
+ ? 'All Meteor passes may be below the minimum elevation. Try again later.'
+ : 'Enter lat/lon in the strip bar above or click GPS to load pass predictions'}
+
`;
+ // 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 @@
+
+
+
+ AOS
+ --
+ --
+
+
→
+
+ TCA
+ --
+ --
+
+
→
+
+ LOS
+ --
+ --
+
+
--
-
Set location to see pass predictions
+
+
+
+
Set your location
+
Enter lat/lon in the strip bar above or click the GPS button to load pass predictions
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
+
+ Set your location — Enter your lat/lon in the strip bar above (or click GPS). This is required for pass predictions.
+ 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".
+ 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.
+ Click Capture on a pass card when it's about to start, or enable AUTO to let the scheduler capture automatically.
+ Wait for images — SatDump will tune, lock the signal, and decode. Decoded images appear in the gallery after the pass completes.
+
+
+
+
+
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 receiver
+ RTL-SDR V3/V4 ($25-35)
+
+
+ Antenna
+ 137 MHz V-dipole ($5 DIY) or QFH ($20-30)
+
+
+ LNA (optional)
+ 137 MHz filtered, at antenna ($15-25)
+
+
+ Location
+ Outdoors, clear sky view
+
+
+ No hardware?
+ Use Load Demo Data below to explore the UI
+
+
+
+
+
+
Satellite
@@ -33,10 +101,13 @@
-
+
-
Antenna Guide
-
+
+ Antenna Guide
+ ▼
+
+
137 MHz band — your stock SDR antenna will NOT work.
@@ -174,14 +245,22 @@
- Test Decode (File)
+ Offline Decode (IQ File)
▼
- Decode a pre-recorded Meteor IQ file without SDR hardware.
- Shared ground-station recordings are also accepted by the backend.
+ Decode a pre-recorded Meteor IQ baseband file without SDR hardware.
+ You need an actual .raw, .sigmf-data, or .wav recording of a Meteor pass.
+
+
Where to get a test file:
+
+ Record one yourself with rtl_sdr -f 137900000 -s 2400000 meteor.raw during a pass
+ Download samples from SigID Wiki or community forums
+ Place the file in data/weather_sat/ on the server
+
+
Satellite
@@ -191,8 +270,8 @@
- File Path (server-side)
-
+ File Path (server-side, relative to app root)
+
Sample Rate
@@ -203,7 +282,7 @@
- Test Decode
+ Decode File