From 72d4fab25ec8ed3243e39e103a4a123f545b9183 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 19 Mar 2026 22:55:56 +0000 Subject: [PATCH] Fix pass calculation race condition and 1Hz distance updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move _passAbortController = null to after response.json() so the retry scheduler cannot see a false idle state mid-parse, increment _passRequestId, and discard the in-flight response — this was causing non-ISS satellites to show no passes intermittently - Add _computeSlantRange() helper using 3D ECEF geometry - Update applyTelemetryPosition to compute slant range from SSE lat/lon/ altitude, giving distance updates at 1Hz instead of 5s HTTP poll rate Co-Authored-By: Claude Sonnet 4.6 --- templates/satellite_dashboard.html | 40 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/templates/satellite_dashboard.html b/templates/satellite_dashboard.html index f4344ca..df9c559 100644 --- a/templates/satellite_dashboard.html +++ b/templates/satellite_dashboard.html @@ -1024,6 +1024,23 @@ return Number.isFinite(value); } + // Compute slant range (km) from observer on Earth's surface to satellite. + // Uses spherical Earth approximation; accurate enough for display. + function _computeSlantRange(obsLat, obsLon, satLat, satLon, satAltKm) { + const R = 6371; + const toRad = Math.PI / 180; + const lat1 = obsLat * toRad, lon1 = obsLon * toRad; + const lat2 = satLat * toRad, lon2 = satLon * toRad; + const rSat = R + satAltKm; + const ox = R * Math.cos(lat1) * Math.cos(lon1); + const oy = R * Math.cos(lat1) * Math.sin(lon1); + const oz = R * Math.sin(lat1); + const sx = rSat * Math.cos(lat2) * Math.cos(lon2); + const sy = rSat * Math.cos(lat2) * Math.sin(lon2); + const sz = rSat * Math.sin(lat2); + return Math.sqrt((sx-ox)**2 + (sy-oy)**2 + (sz-oz)**2); + } + function normalizeLivePosition(pos, previous = latestLivePosition) { if (!pos) return null; const merged = { @@ -1063,7 +1080,17 @@ if (telAlt && _isFiniteNumber(normalized.altitude ?? normalized.alt)) telAlt.textContent = (normalized.altitude ?? normalized.alt).toFixed(0) + ' km'; if (telEl && _isFiniteNumber(normalized.elevation ?? normalized.el)) telEl.textContent = (normalized.elevation ?? normalized.el).toFixed(1) + '°'; if (telAz && _isFiniteNumber(normalized.azimuth ?? normalized.az)) telAz.textContent = (normalized.azimuth ?? normalized.az).toFixed(1) + '°'; - if (telDist && _isFiniteNumber(normalized.distance ?? normalized.dist)) telDist.textContent = (normalized.distance ?? normalized.dist).toFixed(0) + ' km'; + // Compute slant range from the current lat/lon/altitude so the + // distance display updates at the same rate as position (1 Hz via + // SSE). Falls back to the Skyfield-computed value from the HTTP + // poll if geometry data is not available. + let displayDist = normalized.distance ?? normalized.dist; + const { lat: obsLat, lon: obsLon, valid: obsValid } = getObserverCoords(); + const satAlt = normalized.altitude ?? normalized.alt; + if (obsValid && _isFiniteNumber(normalized.lat) && _isFiniteNumber(normalized.lon) && _isFiniteNumber(satAlt)) { + displayDist = _computeSlantRange(obsLat, obsLon, normalized.lat, normalized.lon, satAlt); + } + if (telDist && _isFiniteNumber(displayDist)) telDist.textContent = displayDist.toFixed(0) + ' km'; if (selectedPass == null && _isFiniteNumber(normalized.azimuth ?? normalized.az) && _isFiniteNumber(normalized.elevation ?? normalized.el)) { drawPolarPlotWithPosition( @@ -2068,9 +2095,10 @@ clearTimeout(_passTimeoutId); _passTimeoutId = null; } - if (_passAbortController === controller) { - _passAbortController = null; - } + // Keep _passAbortController set until AFTER response.json() so + // the retry scheduler doesn't incorrectly see the request as + // finished (passes=0 + no controller) and increment _passRequestId + // mid-parse, which would cause this response to be discarded. if (_activePassRequestKey === requestKey) { _activePassRequestKey = null; } @@ -2080,6 +2108,10 @@ throw new Error('Unexpected response while calculating passes'); } const data = await response.json(); + // Now safe to release the controller slot + if (_passAbortController === controller) { + _passAbortController = null; + } if (requestId !== _passRequestId) return; if (data.status === 'success') { const resolvedPasses = Array.isArray(data.passes) ? data.passes : [];