mirror of
https://github.com/smittix/intercept.git
synced 2026-07-20 15:28:11 -07:00
Fix satellite dashboard refresh flows
This commit is contained in:
+158
-85
@@ -48,11 +48,18 @@ _tle_cache = dict(TLE_SATELLITES)
|
|||||||
|
|
||||||
# Ground track cache: key=(sat_name, tle_line1[:20]) -> (track_data, computed_at_timestamp)
|
# Ground track cache: key=(sat_name, tle_line1[:20]) -> (track_data, computed_at_timestamp)
|
||||||
# TTL is 1800 seconds (30 minutes)
|
# TTL is 1800 seconds (30 minutes)
|
||||||
_track_cache: dict = {}
|
_track_cache: dict = {}
|
||||||
_TRACK_CACHE_TTL = 1800
|
_TRACK_CACHE_TTL = 1800
|
||||||
|
|
||||||
|
_BUILTIN_NORAD_TO_KEY = {
|
||||||
|
25544: 'ISS',
|
||||||
|
40069: 'METEOR-M2',
|
||||||
|
57166: 'METEOR-M2-3',
|
||||||
|
59051: 'METEOR-M2-4',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _load_db_satellites_into_cache():
|
def _load_db_satellites_into_cache():
|
||||||
"""Load user-tracked satellites from DB into the TLE cache."""
|
"""Load user-tracked satellites from DB into the TLE cache."""
|
||||||
global _tle_cache
|
global _tle_cache
|
||||||
try:
|
try:
|
||||||
@@ -67,8 +74,102 @@ def _load_db_satellites_into_cache():
|
|||||||
loaded += 1
|
loaded += 1
|
||||||
if loaded:
|
if loaded:
|
||||||
logger.info(f"Loaded {loaded} user-tracked satellites into TLE cache")
|
logger.info(f"Loaded {loaded} user-tracked satellites into TLE cache")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to load DB satellites into TLE cache: {e}")
|
logger.warning(f"Failed to load DB satellites into TLE cache: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_satellite_name(value: object) -> str:
|
||||||
|
"""Normalize satellite identifiers for loose name matching."""
|
||||||
|
return str(value or '').strip().replace(' ', '-').upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tracked_satellite_maps() -> tuple[dict[int, dict], dict[str, dict]]:
|
||||||
|
"""Return tracked satellites indexed by NORAD ID and normalized name."""
|
||||||
|
by_norad: dict[int, dict] = {}
|
||||||
|
by_name: dict[str, dict] = {}
|
||||||
|
try:
|
||||||
|
for sat in get_tracked_satellites():
|
||||||
|
try:
|
||||||
|
norad_id = int(sat['norad_id'])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
by_norad[norad_id] = sat
|
||||||
|
by_name[_normalize_satellite_name(sat.get('name'))] = sat
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to read tracked satellites for lookup: {e}")
|
||||||
|
return by_norad, by_name
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_satellite_request(sat: object, tracked_by_norad: dict[int, dict], tracked_by_name: dict[str, dict]) -> tuple[str, int | None, tuple[str, str, str] | None]:
|
||||||
|
"""Resolve a satellite request to display name, NORAD ID, and TLE data."""
|
||||||
|
norad_id: int | None = None
|
||||||
|
sat_key: str | None = None
|
||||||
|
tracked: dict | None = None
|
||||||
|
|
||||||
|
if isinstance(sat, int):
|
||||||
|
norad_id = sat
|
||||||
|
elif isinstance(sat, str):
|
||||||
|
stripped = sat.strip()
|
||||||
|
if stripped.isdigit():
|
||||||
|
norad_id = int(stripped)
|
||||||
|
else:
|
||||||
|
sat_key = stripped
|
||||||
|
|
||||||
|
if norad_id is not None:
|
||||||
|
tracked = tracked_by_norad.get(norad_id)
|
||||||
|
sat_key = _BUILTIN_NORAD_TO_KEY.get(norad_id) or (tracked.get('name') if tracked else str(norad_id))
|
||||||
|
else:
|
||||||
|
normalized = _normalize_satellite_name(sat_key)
|
||||||
|
tracked = tracked_by_name.get(normalized)
|
||||||
|
if tracked:
|
||||||
|
try:
|
||||||
|
norad_id = int(tracked['norad_id'])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
norad_id = None
|
||||||
|
sat_key = tracked.get('name') or sat_key
|
||||||
|
|
||||||
|
tle_data = None
|
||||||
|
candidate_keys: list[str] = []
|
||||||
|
if sat_key:
|
||||||
|
candidate_keys.extend([
|
||||||
|
sat_key,
|
||||||
|
_normalize_satellite_name(sat_key),
|
||||||
|
])
|
||||||
|
if tracked and tracked.get('name'):
|
||||||
|
candidate_keys.extend([
|
||||||
|
tracked['name'],
|
||||||
|
_normalize_satellite_name(tracked['name']),
|
||||||
|
])
|
||||||
|
|
||||||
|
seen: set[str] = set()
|
||||||
|
for key in candidate_keys:
|
||||||
|
norm = _normalize_satellite_name(key)
|
||||||
|
if norm in seen:
|
||||||
|
continue
|
||||||
|
seen.add(norm)
|
||||||
|
if key in _tle_cache:
|
||||||
|
tle_data = _tle_cache[key]
|
||||||
|
break
|
||||||
|
if norm in _tle_cache:
|
||||||
|
tle_data = _tle_cache[norm]
|
||||||
|
break
|
||||||
|
|
||||||
|
if tle_data is None and tracked and tracked.get('tle_line1') and tracked.get('tle_line2'):
|
||||||
|
display_name = tracked.get('name') or sat_key or str(norad_id or 'UNKNOWN')
|
||||||
|
tle_data = (display_name, tracked['tle_line1'], tracked['tle_line2'])
|
||||||
|
_tle_cache[_normalize_satellite_name(display_name)] = tle_data
|
||||||
|
|
||||||
|
if tle_data is None and sat_key:
|
||||||
|
normalized = _normalize_satellite_name(sat_key)
|
||||||
|
for key, value in _tle_cache.items():
|
||||||
|
if key == normalized or _normalize_satellite_name(value[0]) == normalized:
|
||||||
|
tle_data = value
|
||||||
|
break
|
||||||
|
|
||||||
|
display_name = _BUILTIN_NORAD_TO_KEY.get(norad_id or -1)
|
||||||
|
if not display_name:
|
||||||
|
display_name = (tracked.get('name') if tracked else None) or (tle_data[0] if tle_data else None) or (sat_key if sat_key else str(norad_id or 'UNKNOWN'))
|
||||||
|
return display_name, norad_id, tle_data
|
||||||
|
|
||||||
|
|
||||||
def _start_satellite_tracker():
|
def _start_satellite_tracker():
|
||||||
@@ -328,45 +429,30 @@ def predict_passes():
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return api_error(str(e), 400)
|
return api_error(str(e), 400)
|
||||||
|
|
||||||
norad_to_name = {
|
|
||||||
25544: 'ISS',
|
|
||||||
40069: 'METEOR-M2',
|
|
||||||
57166: 'METEOR-M2-3',
|
|
||||||
59051: 'METEOR-M2-4',
|
|
||||||
}
|
|
||||||
|
|
||||||
sat_input = data.get('satellites', ['ISS', 'METEOR-M2-3', 'METEOR-M2-4'])
|
sat_input = data.get('satellites', ['ISS', 'METEOR-M2-3', 'METEOR-M2-4'])
|
||||||
satellites = []
|
passes = []
|
||||||
for sat in sat_input:
|
|
||||||
if isinstance(sat, int) and sat in norad_to_name:
|
|
||||||
satellites.append(norad_to_name[sat])
|
|
||||||
else:
|
|
||||||
satellites.append(sat)
|
|
||||||
|
|
||||||
passes = []
|
|
||||||
colors = {
|
colors = {
|
||||||
'ISS': '#00ffff',
|
'ISS': '#00ffff',
|
||||||
'METEOR-M2': '#9370DB',
|
'METEOR-M2': '#9370DB',
|
||||||
'METEOR-M2-3': '#ff00ff',
|
'METEOR-M2-3': '#ff00ff',
|
||||||
'METEOR-M2-4': '#00ff88',
|
'METEOR-M2-4': '#00ff88',
|
||||||
}
|
}
|
||||||
name_to_norad = {v: k for k, v in norad_to_name.items()}
|
tracked_by_norad, tracked_by_name = _get_tracked_satellite_maps()
|
||||||
|
|
||||||
ts = _get_timescale()
|
ts = _get_timescale()
|
||||||
observer = wgs84.latlon(lat, lon)
|
observer = wgs84.latlon(lat, lon)
|
||||||
t0 = ts.now()
|
t0 = ts.now()
|
||||||
t1 = ts.utc(t0.utc_datetime() + timedelta(hours=hours))
|
t1 = ts.utc(t0.utc_datetime() + timedelta(hours=hours))
|
||||||
|
|
||||||
for sat_name in satellites:
|
for sat in sat_input:
|
||||||
if sat_name not in _tle_cache:
|
sat_name, norad_id, tle_data = _resolve_satellite_request(sat, tracked_by_norad, tracked_by_name)
|
||||||
continue
|
if not tle_data:
|
||||||
|
continue
|
||||||
tle_data = _tle_cache[sat_name]
|
|
||||||
|
# Current position for map marker (computed once per satellite)
|
||||||
# Current position for map marker (computed once per satellite)
|
current_pos = None
|
||||||
current_pos = None
|
try:
|
||||||
try:
|
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
||||||
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
|
||||||
geo = satellite.at(ts.now())
|
geo = satellite.at(ts.now())
|
||||||
sp = wgs84.subpoint(geo)
|
sp = wgs84.subpoint(geo)
|
||||||
current_pos = {
|
current_pos = {
|
||||||
@@ -376,14 +462,14 @@ def predict_passes():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sat_passes = _predict_passes(tle_data, observer, ts, t0, t1, min_el=min_el)
|
sat_passes = _predict_passes(tle_data, observer, ts, t0, t1, min_el=min_el)
|
||||||
for p in sat_passes:
|
for p in sat_passes:
|
||||||
p['satellite'] = sat_name
|
p['satellite'] = sat_name
|
||||||
p['norad'] = name_to_norad.get(sat_name, 0)
|
p['norad'] = norad_id or 0
|
||||||
p['color'] = colors.get(sat_name, '#00ff00')
|
p['color'] = colors.get(sat_name, '#00ff00')
|
||||||
if current_pos:
|
if current_pos:
|
||||||
p['currentPos'] = current_pos
|
p['currentPos'] = current_pos
|
||||||
passes.extend(sat_passes)
|
passes.extend(sat_passes)
|
||||||
|
|
||||||
passes.sort(key=lambda p: p['startTimeISO'])
|
passes.sort(key=lambda p: p['startTimeISO'])
|
||||||
|
|
||||||
@@ -413,35 +499,23 @@ def get_satellite_position():
|
|||||||
sat_input = data.get('satellites', [])
|
sat_input = data.get('satellites', [])
|
||||||
include_track = bool(data.get('includeTrack', True))
|
include_track = bool(data.get('includeTrack', True))
|
||||||
|
|
||||||
norad_to_name = {
|
ts = _get_timescale()
|
||||||
25544: 'ISS',
|
observer = wgs84.latlon(lat, lon)
|
||||||
40069: 'METEOR-M2',
|
now = ts.now()
|
||||||
57166: 'METEOR-M2-3',
|
now_dt = now.utc_datetime()
|
||||||
59051: 'METEOR-M2-4',
|
tracked_by_norad, tracked_by_name = _get_tracked_satellite_maps()
|
||||||
}
|
|
||||||
|
positions = []
|
||||||
satellites = []
|
|
||||||
for sat in sat_input:
|
for sat in sat_input:
|
||||||
if isinstance(sat, int) and sat in norad_to_name:
|
sat_name, norad_id, tle_data = _resolve_satellite_request(sat, tracked_by_norad, tracked_by_name)
|
||||||
satellites.append(norad_to_name[sat])
|
# Special handling for ISS - use real-time API for accurate position
|
||||||
else:
|
if norad_id == 25544 or sat_name == 'ISS':
|
||||||
satellites.append(sat)
|
iss_data = _fetch_iss_realtime(lat, lon)
|
||||||
|
if iss_data:
|
||||||
ts = _get_timescale()
|
# Add orbit track if requested (using TLE for track prediction)
|
||||||
observer = wgs84.latlon(lat, lon)
|
if include_track and 'ISS' in _tle_cache:
|
||||||
now = ts.now()
|
try:
|
||||||
now_dt = now.utc_datetime()
|
|
||||||
|
|
||||||
positions = []
|
|
||||||
|
|
||||||
for sat_name in satellites:
|
|
||||||
# Special handling for ISS - use real-time API for accurate position
|
|
||||||
if sat_name == 'ISS':
|
|
||||||
iss_data = _fetch_iss_realtime(lat, lon)
|
|
||||||
if iss_data:
|
|
||||||
# Add orbit track if requested (using TLE for track prediction)
|
|
||||||
if include_track and 'ISS' in _tle_cache:
|
|
||||||
try:
|
|
||||||
tle_data = _tle_cache['ISS']
|
tle_data = _tle_cache['ISS']
|
||||||
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
||||||
orbit_track = []
|
orbit_track = []
|
||||||
@@ -460,16 +534,15 @@ def get_satellite_position():
|
|||||||
iss_data['track'] = orbit_track
|
iss_data['track'] = orbit_track
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
positions.append(iss_data)
|
positions.append(iss_data)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Other satellites - use TLE data
|
# Other satellites - use TLE data
|
||||||
if sat_name not in _tle_cache:
|
if not tle_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tle_data = _tle_cache[sat_name]
|
try:
|
||||||
try:
|
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
||||||
satellite = EarthSatellite(tle_data[1], tle_data[2], tle_data[0], ts)
|
|
||||||
|
|
||||||
geocentric = satellite.at(now)
|
geocentric = satellite.at(now)
|
||||||
subpoint = wgs84.subpoint(geocentric)
|
subpoint = wgs84.subpoint(geocentric)
|
||||||
@@ -480,7 +553,7 @@ def get_satellite_position():
|
|||||||
|
|
||||||
pos_data = {
|
pos_data = {
|
||||||
'satellite': sat_name,
|
'satellite': sat_name,
|
||||||
'norad_id': next((nid for nid, name in norad_to_name.items() if name == sat_name), None),
|
'norad_id': norad_id,
|
||||||
'lat': float(subpoint.latitude.degrees),
|
'lat': float(subpoint.latitude.degrees),
|
||||||
'lon': float(subpoint.longitude.degrees),
|
'lon': float(subpoint.longitude.degrees),
|
||||||
'altitude': float(geocentric.distance().km - 6371),
|
'altitude': float(geocentric.distance().km - 6371),
|
||||||
|
|||||||
+11
-6
@@ -11554,10 +11554,13 @@
|
|||||||
function fetchCelestrakCategory(category) {
|
function fetchCelestrakCategory(category) {
|
||||||
const status = document.getElementById('celestrakStatus');
|
const status = document.getElementById('celestrakStatus');
|
||||||
status.innerHTML = '<span style="color: var(--accent-cyan);">Fetching ' + category + '...</span>';
|
status.innerHTML = '<span style="color: var(--accent-cyan);">Fetching ' + category + '...</span>';
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||||
|
|
||||||
fetch('/satellite/celestrak/' + category)
|
fetch('/satellite/celestrak/' + category, { signal: controller.signal })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
|
clearTimeout(timeout);
|
||||||
if (data.status === 'success' && data.satellites) {
|
if (data.status === 'success' && data.satellites) {
|
||||||
const toAdd = data.satellites
|
const toAdd = data.satellites
|
||||||
.filter(sat => !trackedSatellites.find(s => s.norad === String(sat.norad)))
|
.filter(sat => !trackedSatellites.find(s => s.norad === String(sat.norad)))
|
||||||
@@ -11602,8 +11605,10 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
const msg = err && err.message ? err.message : 'Network error';
|
const msg = err && err.message ? err.message : 'Network error';
|
||||||
status.innerHTML = `<span style="color: var(--accent-red);">Import failed: ${msg}</span>`;
|
const label = err && err.name === 'AbortError' ? 'Request timed out' : msg;
|
||||||
|
status.innerHTML = `<span style="color: var(--accent-red);">Import failed: ${label}</span>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11613,7 +11618,7 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'success' && data.satellites) {
|
if (data.status === 'success' && data.satellites) {
|
||||||
trackedSatellites = data.satellites.map(sat => ({
|
trackedSatellites = data.satellites.map(sat => ({
|
||||||
id: sat.name.replace(/[^a-zA-Z0-9]/g, '-').toUpperCase(),
|
id: String(sat.norad_id),
|
||||||
name: sat.name,
|
name: sat.name,
|
||||||
norad: sat.norad_id,
|
norad: sat.norad_id,
|
||||||
builtin: sat.builtin,
|
builtin: sat.builtin,
|
||||||
@@ -11627,9 +11632,9 @@
|
|||||||
// Fallback to hardcoded defaults if API fails
|
// Fallback to hardcoded defaults if API fails
|
||||||
if (trackedSatellites.length === 0) {
|
if (trackedSatellites.length === 0) {
|
||||||
trackedSatellites = [
|
trackedSatellites = [
|
||||||
{ id: 'ISS', name: 'ISS (ZARYA)', norad: '25544', builtin: true, checked: true },
|
{ id: '25544', name: 'ISS (ZARYA)', norad: '25544', builtin: true, checked: true },
|
||||||
{ id: 'METEOR-M2-3', name: 'Meteor-M2-3', norad: '57166', builtin: true, checked: true },
|
{ id: '57166', name: 'Meteor-M2-3', norad: '57166', builtin: true, checked: true },
|
||||||
{ id: 'METEOR-M2-4', name: 'Meteor-M2-4', norad: '59051', builtin: true, checked: true }
|
{ id: '59051', name: 'Meteor-M2-4', norad: '59051', builtin: true, checked: true }
|
||||||
];
|
];
|
||||||
renderSatelliteList();
|
renderSatelliteList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,19 +200,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decoded Packets -->
|
|
||||||
<div class="panel packets-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<span>DECODED PACKETS <span id="packetCount" style="color:var(--accent-cyan);"></span></span>
|
|
||||||
<div class="panel-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-content" id="packetList">
|
|
||||||
<div style="text-align:center;color:var(--text-secondary);padding:15px;font-size:11px;">
|
|
||||||
No packets received yet.<br>Run a ground-station observation with telemetry tasks enabled to populate this panel.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ground Station -->
|
<!-- Ground Station -->
|
||||||
<div class="panel gs-panel" id="gsPanel">
|
<div class="panel gs-panel" id="gsPanel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -352,6 +339,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Decoded Packets -->
|
||||||
|
<div class="panel packets-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>DECODED PACKETS <span id="packetCount" style="color:var(--accent-cyan);"></span></span>
|
||||||
|
<div class="panel-indicator"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content" id="packetList">
|
||||||
|
<div style="text-align:center;color:var(--text-secondary);padding:15px;font-size:11px;">
|
||||||
|
No packets received yet.<br>Run a ground-station observation with telemetry tasks enabled to populate this panel.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controls Bar -->
|
<!-- Controls Bar -->
|
||||||
@@ -601,6 +601,7 @@
|
|||||||
let agents = [];
|
let agents = [];
|
||||||
let _txRequestId = 0;
|
let _txRequestId = 0;
|
||||||
let _telemetryPollTimer = null;
|
let _telemetryPollTimer = null;
|
||||||
|
let _passRequestId = 0;
|
||||||
|
|
||||||
let satellites = {
|
let satellites = {
|
||||||
25544: { name: 'ISS (ZARYA)', color: '#00ffff' },
|
25544: { name: 'ISS (ZARYA)', color: '#00ffff' },
|
||||||
@@ -620,33 +621,49 @@
|
|||||||
fetch('/satellite/tracked?enabled=true')
|
fetch('/satellite/tracked?enabled=true')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'success' && data.satellites && data.satellites.length > 0) {
|
const prevSelected = selectedSatellite;
|
||||||
const prevSelected = selectedSatellite;
|
const newSats = {
|
||||||
const newSats = {};
|
25544: { name: 'ISS (ZARYA)', color: satellites[25544]?.color || satColors[0] },
|
||||||
const select = document.getElementById('satSelect');
|
57166: { name: 'METEOR-M2-3', color: satellites[57166]?.color || satColors[2] },
|
||||||
select.innerHTML = '';
|
59051: { name: 'METEOR-M2-4', color: satellites[59051]?.color || satColors[4] },
|
||||||
|
};
|
||||||
|
const select = document.getElementById('satSelect');
|
||||||
|
if (!select) return;
|
||||||
|
if (data.status === 'success' && Array.isArray(data.satellites)) {
|
||||||
data.satellites.forEach((sat, i) => {
|
data.satellites.forEach((sat, i) => {
|
||||||
const norad = parseInt(sat.norad_id);
|
const norad = parseInt(sat.norad_id);
|
||||||
|
if (!Number.isFinite(norad)) return;
|
||||||
newSats[norad] = {
|
newSats[norad] = {
|
||||||
name: sat.name,
|
name: sat.name,
|
||||||
color: satellites[norad]?.color || satColors[i % satColors.length]
|
color: satellites[norad]?.color || satColors[i % satColors.length]
|
||||||
};
|
};
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = norad;
|
|
||||||
opt.textContent = sat.name;
|
|
||||||
select.appendChild(opt);
|
|
||||||
});
|
});
|
||||||
satellites = newSats;
|
|
||||||
// Restore previous selection if still available, else default to ISS
|
|
||||||
if (newSats[prevSelected]) {
|
|
||||||
select.value = prevSelected;
|
|
||||||
} else if (newSats[25544]) {
|
|
||||||
select.value = '25544';
|
|
||||||
}
|
|
||||||
selectedSatellite = parseInt(select.value);
|
|
||||||
}
|
}
|
||||||
|
satellites = newSats;
|
||||||
|
select.innerHTML = '';
|
||||||
|
Object.entries(newSats).forEach(([norad, sat]) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = norad;
|
||||||
|
opt.textContent = sat.name;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
if (newSats[prevSelected]) {
|
||||||
|
select.value = String(prevSelected);
|
||||||
|
} else if (newSats[25544]) {
|
||||||
|
select.value = '25544';
|
||||||
|
}
|
||||||
|
selectedSatellite = parseInt(select.value);
|
||||||
|
clearTelemetry();
|
||||||
|
loadTransmitters(selectedSatellite);
|
||||||
|
calculatePasses();
|
||||||
|
fetchCurrentTelemetry();
|
||||||
|
if (window.gsLoadOutputs) window.gsLoadOutputs();
|
||||||
|
if (window.gsOnSatelliteChange) window.gsOnSatelliteChange();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
if (btn) btn.classList.remove('spinning');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSatelliteChange() {
|
function onSatelliteChange() {
|
||||||
@@ -670,7 +687,7 @@
|
|||||||
loadTransmitters(selectedSatellite);
|
loadTransmitters(selectedSatellite);
|
||||||
calculatePasses();
|
calculatePasses();
|
||||||
fetchCurrentTelemetry();
|
fetchCurrentTelemetry();
|
||||||
gsLoadOutputs();
|
if (window.gsLoadOutputs) window.gsLoadOutputs();
|
||||||
if (window.gsOnSatelliteChange) gsOnSatelliteChange();
|
if (window.gsOnSatelliteChange) gsOnSatelliteChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1073,9 +1090,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function calculatePasses() {
|
async function calculatePasses() {
|
||||||
|
const requestId = ++_passRequestId;
|
||||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||||
const satName = satellites[selectedSatellite]?.name || 'Unknown';
|
const container = document.getElementById('passList');
|
||||||
|
const button = document.querySelector('.controls-bar .btn.primary');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Calculating passes...</div>';
|
||||||
|
}
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'WORKING...';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/satellite/predict', {
|
const response = await fetch('/satellite/predict', {
|
||||||
@@ -1091,25 +1117,38 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (requestId !== _passRequestId) return;
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
passes = data.passes;
|
passes = data.passes;
|
||||||
renderPassList();
|
renderPassList();
|
||||||
updateStats();
|
updateStats();
|
||||||
if (passes.length > 0) {
|
if (passes.length > 0) {
|
||||||
selectPass(0);
|
selectPass(0);
|
||||||
|
} else {
|
||||||
|
clearTelemetry();
|
||||||
}
|
}
|
||||||
updateObserverMarker(lat, lon);
|
updateObserverMarker(lat, lon);
|
||||||
|
|
||||||
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
||||||
} else {
|
} else {
|
||||||
|
passes = [];
|
||||||
|
renderPassList();
|
||||||
document.getElementById('trackingStatus').textContent = 'ERROR';
|
document.getElementById('trackingStatus').textContent = 'ERROR';
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (requestId !== _passRequestId) return;
|
||||||
console.error('Pass calculation error:', err);
|
console.error('Pass calculation error:', err);
|
||||||
|
passes = [];
|
||||||
|
renderPassList();
|
||||||
document.getElementById('trackingStatus').textContent = 'OFFLINE';
|
document.getElementById('trackingStatus').textContent = 'OFFLINE';
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
||||||
|
} finally {
|
||||||
|
if (requestId === _passRequestId && button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'CALCULATE';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2046,6 +2085,7 @@
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
window.gsLoadOutputs = gsLoadOutputs;
|
||||||
|
|
||||||
function gsLoadDecodeJobs(norad) {
|
function gsLoadDecodeJobs(norad) {
|
||||||
const panel = document.getElementById('gsOutputsPanel');
|
const panel = document.getElementById('gsOutputsPanel');
|
||||||
|
|||||||
+15
-3
@@ -179,15 +179,27 @@ def get_transmitters(norad_id: int) -> list[dict]:
|
|||||||
"""
|
"""
|
||||||
global _transmitters, _fetched_at # noqa: PLW0603
|
global _transmitters, _fetched_at # noqa: PLW0603
|
||||||
|
|
||||||
with _fetch_lock:
|
sat_id = int(norad_id)
|
||||||
|
age = time.time() - _fetched_at
|
||||||
|
|
||||||
|
# Fast path: serve warm cache immediately.
|
||||||
|
if _transmitters and age <= _CACHE_TTL:
|
||||||
|
return _transmitters.get(sat_id, _BUILTIN_TRANSMITTERS.get(sat_id, []))
|
||||||
|
|
||||||
|
# Avoid blocking the UI behind a long-running background refresh.
|
||||||
|
if not _fetch_lock.acquire(blocking=False):
|
||||||
|
return _transmitters.get(sat_id, _BUILTIN_TRANSMITTERS.get(sat_id, []))
|
||||||
|
|
||||||
|
try:
|
||||||
age = time.time() - _fetched_at
|
age = time.time() - _fetched_at
|
||||||
if not _transmitters or age > _CACHE_TTL:
|
if not _transmitters or age > _CACHE_TTL:
|
||||||
fetched = fetch_transmitters()
|
fetched = fetch_transmitters()
|
||||||
if fetched:
|
if fetched:
|
||||||
_transmitters = fetched
|
_transmitters = fetched
|
||||||
_fetched_at = time.time()
|
_fetched_at = time.time()
|
||||||
|
return _transmitters.get(sat_id, _BUILTIN_TRANSMITTERS.get(sat_id, []))
|
||||||
return _transmitters.get(int(norad_id), _BUILTIN_TRANSMITTERS.get(int(norad_id), []))
|
finally:
|
||||||
|
_fetch_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def refresh_transmitters() -> int:
|
def refresh_transmitters() -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user