fix(gnss): age degraded evidence and replayed snapshots on their own clocks

Maintainer review on #855 (mazy1998, 2026-10-06):

- records: presence (seenAt) and degraded evidence (degradedAt) age
  separately, so continuing healthy reports no longer keep an older
  degraded flag alive past 30 minutes; an aircraft returning after the
  window starts fresh. pruneGnssObservations ages the store with no new
  rows.
- Rows are dated by the snapshot's fetchedAt, which the proxy keeps on
  cached and stale answers: a replay never restarts the window, the
  proxy stops serving a stale snapshot older than the window, the source
  refuses an undated one, and cells expire during fetch failures and
  while there is no view anchor.
- Normalization accepts ADS-B versions 1 and 2 only; a missing, invalid
  or negative seen_pos is not a confirmed position from the last minute.
- qa-gnss: keyboard, 390x844 viewport (fresh page: isMobile reloads) and
  replay-age sections; the live cell assertion bins the returned rows
  with the layer's own rules instead of a raw row count.
This commit is contained in:
Leo Pechnicki authored and Mazeyar Moeini Feizabadi committed 2026-10-13 17:20:43 +02:00
1 parent c8d3f44bb7
commit fde8b2a64f
11 files changed
+557 -65

No files matched your search

+18 -2
View File
@@ -8,6 +8,7 @@ import {
accumulateGnssObservations,
binGnssCells,
gnssProvenance,
pruneGnssObservations,
} from './records.js';
export * from './records.js';
export { createAdsbGnssSource } from './source.js';
@@ -62,6 +63,13 @@ export function createGnssIntegrityLayer({
const _observations = new Map();
const provenance = gnssProvenance({ windowMs });
/** Age the store without new evidence, so expired cells leave the globe. */
function expire() {
pruneGnssObservations(_observations, now(), { windowMs });
_cells = binGnssCells(_observations);
render();
}
function render() {
// Only the id and band change what is drawn; counts alone do not.
const signature = JSON.stringify(
@@ -129,8 +137,11 @@ export function createGnssIntegrityLayer({
const anchor = viewAnchor(_viewer);
// No anchor yet (camera still settling) is not a failure: returning
// false would make the manager reject the enable. The next interval
// retries.
if (!anchor) return true;
// retries; cells already drawn still age out of the window.
if (!anchor) {
expire();
return true;
}
_request?.abort();
const request = new AbortController();
_request = request;
@@ -140,8 +151,11 @@ export function createGnssIntegrityLayer({
});
if (request.signal.aborted || _request !== request || !_enabled)
return false;
// Date the rows by when the proxy observed them: a cached or stale
// replay keeps its original age instead of restarting the window.
accumulateGnssObservations(_observations, snapshot.rows, now(), {
windowMs,
observedAt: snapshot.fetchedAt,
});
_cells = binGnssCells(_observations);
render();
@@ -154,6 +168,8 @@ export function createGnssIntegrityLayer({
return false;
console.warn('[Data:GNSS] Fetch error:', e);
_lastError = e?.message || 'GNSS integrity source unavailable';
// Keep the last cells only while their evidence is inside the window.
expire();
return false;
} finally {
if (_request === request) _request = null;