mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Fix weather satellite next-pass countdown timestamps
This commit is contained in:
@@ -461,6 +461,26 @@ const WeatherSat = (function() {
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse pass timestamps, accepting legacy malformed UTC strings (+00:00Z).
|
||||
*/
|
||||
function parsePassDate(value) {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
|
||||
let parsed = new Date(value);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Backward-compatible cleanup for accidentally double-suffixed UTC timestamps.
|
||||
parsed = new Date(value.replace(/\+00:00Z$/, 'Z'));
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load pass predictions (with trajectory + ground track)
|
||||
*/
|
||||
@@ -553,13 +573,15 @@ const WeatherSat = (function() {
|
||||
const modeClass = pass.mode === 'APT' ? 'apt' : 'lrpt';
|
||||
const timeStr = pass.startTime || '--';
|
||||
const now = new Date();
|
||||
const passStart = new Date(pass.startTimeISO);
|
||||
const diffMs = passStart - now;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const passStart = parsePassDate(pass.startTimeISO);
|
||||
const diffMs = passStart ? passStart - now : NaN;
|
||||
const diffMins = Number.isFinite(diffMs) ? Math.floor(diffMs / 60000) : NaN;
|
||||
const isSelected = idx === selectedPassIndex;
|
||||
|
||||
let countdown = '';
|
||||
if (diffMs < 0) {
|
||||
let countdown = '--';
|
||||
if (!Number.isFinite(diffMs)) {
|
||||
countdown = '--';
|
||||
} else if (diffMs < 0) {
|
||||
countdown = 'NOW';
|
||||
} else if (diffMins < 60) {
|
||||
countdown = `in ${diffMins}m`;
|
||||
@@ -828,8 +850,11 @@ const WeatherSat = (function() {
|
||||
let isActive = false;
|
||||
|
||||
for (const pass of passes) {
|
||||
const start = new Date(pass.startTimeISO);
|
||||
const end = new Date(pass.endTimeISO);
|
||||
const start = parsePassDate(pass.startTimeISO);
|
||||
const end = parsePassDate(pass.endTimeISO);
|
||||
if (!start || !end) {
|
||||
continue;
|
||||
}
|
||||
if (end > now) {
|
||||
nextPass = pass;
|
||||
isActive = start <= now;
|
||||
@@ -858,7 +883,19 @@ const WeatherSat = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = new Date(nextPass.startTimeISO);
|
||||
const target = parsePassDate(nextPass.startTimeISO);
|
||||
if (!target) {
|
||||
if (daysEl) daysEl.textContent = '--';
|
||||
if (hoursEl) hoursEl.textContent = '--';
|
||||
if (minsEl) minsEl.textContent = '--';
|
||||
if (secsEl) secsEl.textContent = '--';
|
||||
if (satEl) satEl.textContent = '--';
|
||||
if (detailEl) detailEl.textContent = 'Invalid pass time';
|
||||
if (boxes) boxes.querySelectorAll('.wxsat-countdown-box').forEach(b => {
|
||||
b.classList.remove('imminent', 'active');
|
||||
});
|
||||
return;
|
||||
}
|
||||
let diffMs = target - now;
|
||||
|
||||
if (isActive) {
|
||||
@@ -918,8 +955,9 @@ const WeatherSat = (function() {
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
passList.forEach((pass, idx) => {
|
||||
const start = new Date(pass.startTimeISO);
|
||||
const end = new Date(pass.endTimeISO);
|
||||
const start = parsePassDate(pass.startTimeISO);
|
||||
const end = parsePassDate(pass.endTimeISO);
|
||||
if (!start || !end) return;
|
||||
|
||||
const startPct = Math.max(0, Math.min(100, ((start - dayStart) / dayMs) * 100));
|
||||
const endPct = Math.max(0, Math.min(100, ((end - dayStart) / dayMs) * 100));
|
||||
|
||||
@@ -7,10 +7,10 @@ and ground track generation.
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from utils.weather_sat_predict import predict_passes
|
||||
from utils.weather_sat_predict import _format_utc_iso, predict_passes
|
||||
|
||||
|
||||
class TestPredictPasses:
|
||||
@@ -673,3 +673,20 @@ class TestPassDataStructure:
|
||||
with patch.dict('sys.modules', {'skyfield': None, 'skyfield.api': None}):
|
||||
with pytest.raises((ImportError, AttributeError)):
|
||||
predict_passes(lat=51.5, lon=-0.1)
|
||||
|
||||
|
||||
class TestTimestampFormatting:
|
||||
"""Tests for UTC timestamp serialization helpers."""
|
||||
|
||||
def test_format_utc_iso_from_aware_datetime(self):
|
||||
"""Aware UTC datetimes should not get a duplicate UTC suffix."""
|
||||
dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
value = _format_utc_iso(dt)
|
||||
assert value == '2024-01-01T12:00:00Z'
|
||||
assert '+00:00Z' not in value
|
||||
|
||||
def test_format_utc_iso_from_naive_datetime(self):
|
||||
"""Naive datetimes should be treated as UTC and serialized consistently."""
|
||||
dt = datetime(2024, 1, 1, 12, 0, 0)
|
||||
value = _format_utc_iso(dt)
|
||||
assert value == '2024-01-01T12:00:00Z'
|
||||
|
||||
@@ -14,6 +14,15 @@ from utils.weather_sat import WEATHER_SATELLITES
|
||||
logger = get_logger('intercept.weather_sat_predict')
|
||||
|
||||
|
||||
def _format_utc_iso(dt: datetime.datetime) -> str:
|
||||
"""Return an ISO8601 UTC timestamp with a single timezone designator."""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
||||
else:
|
||||
dt = dt.astimezone(datetime.timezone.utc)
|
||||
return dt.isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def predict_passes(
|
||||
lat: float,
|
||||
lon: float,
|
||||
@@ -38,8 +47,9 @@ def predict_passes(
|
||||
Raises:
|
||||
ImportError: If skyfield is not installed.
|
||||
"""
|
||||
from skyfield.api import load, wgs84, EarthSatellite
|
||||
from skyfield.almanac import find_discrete
|
||||
from skyfield.api import EarthSatellite, load, wgs84
|
||||
|
||||
from data.satellites import TLE_SATELLITES
|
||||
|
||||
# Use live TLE cache from satellite module if available (refreshed from CelesTrak)
|
||||
@@ -100,8 +110,10 @@ def predict_passes(
|
||||
i += 1
|
||||
continue
|
||||
|
||||
rise_dt = rise_time.utc_datetime()
|
||||
set_dt = set_time.utc_datetime()
|
||||
duration_seconds = (
|
||||
set_time.utc_datetime() - rise_time.utc_datetime()
|
||||
set_dt - rise_dt
|
||||
).total_seconds()
|
||||
duration_minutes = round(duration_seconds / 60, 1)
|
||||
|
||||
@@ -141,14 +153,14 @@ def predict_passes(
|
||||
_, set_az, _ = set_topo.altaz()
|
||||
|
||||
pass_data: dict[str, Any] = {
|
||||
'id': f"{sat_key}_{rise_time.utc_datetime().strftime('%Y%m%d%H%M%S')}",
|
||||
'id': f"{sat_key}_{rise_dt.strftime('%Y%m%d%H%M%S')}",
|
||||
'satellite': sat_key,
|
||||
'name': sat_info['name'],
|
||||
'frequency': sat_info['frequency'],
|
||||
'mode': sat_info['mode'],
|
||||
'startTime': rise_time.utc_datetime().strftime('%Y-%m-%d %H:%M UTC'),
|
||||
'startTimeISO': rise_time.utc_datetime().isoformat() + 'Z',
|
||||
'endTimeISO': set_time.utc_datetime().isoformat() + 'Z',
|
||||
'startTime': rise_dt.strftime('%Y-%m-%d %H:%M UTC'),
|
||||
'startTimeISO': _format_utc_iso(rise_dt),
|
||||
'endTimeISO': _format_utc_iso(set_dt),
|
||||
'maxEl': round(max_el, 1),
|
||||
'maxElAz': round(max_el_az, 1),
|
||||
'riseAz': round(rise_az.degrees, 1),
|
||||
|
||||
Reference in New Issue
Block a user