Skip to content

ADR-091 — Station TLEs live in the served /data overlay, refreshed by one server-side fetcher ​

Status · Accepted · 2026-09-07 · Operator-directed (Physics Lab v0.9 · H4c · #464) · applies the launches overlay pattern (RFC-035 / ADR-065) to station element sets · supersedes the runtime browser→Celestrak fetch introduced with the AR sky (#405)

Context ​

Two code paths fetched station TLEs (ISS / Tiangong) from Celestrak, independently:

  1. The AR sky (/explore "look at the sky", #405) fetched Celestrak from the browser at runtime (tle-source.ts), cached in localStorage for a day, and fell back to a bundled sample.
  2. A build-time refresh (scripts/fetch-station-tles.mjs) existed but nothing ran it — its "runs daily" comment was false and the bundled set had gone ~49 days stale (found in H · #464).

Meanwhile the Lab's iss-pass read the bundled JSON directly (baked into the app + the MCP image).

This is three sources of truth and a browser egress dependency. A stale TLE propagated weeks forward puts the ISS marker / pass in the wrong place; the browser fetch adds a CORS / uptime dependency at the moment a user opens the feature; and the MCP image only ever saw the baked copy.

The launches subsystem already solved the same shape (RFC-035): fetch server-side into the served static/data overlay, refresh it live on the VPS every 6h with no redeploy, and let the SPA read the same-origin file.

Decision ​

One canonical file, one Celestrak fetcher, served from the /data overlay.

  • The element sets live in static/data/station-tles.json (served overlay), not in src.
  • scripts/fetch-station-tles.mjs is the only code that touches Celestrak, and it runs server-side in two places, exactly like launches:
    • daily on main via .github/workflows/refresh-station-tles.yml (bot-commit, chore(tle): refresh, on the ci-databot-* concurrency group so it never cancels user CI);
    • every 6h on the prod VPS via ops/refresh-prod-data.sh (live overlay write, backup + min-bytes safety-restore, precompressed-sibling drop — no rebuild, no redeploy).
  • The app reads the fresh served copy at runtime through the single resolver resolveStationTle / resolveStationTleBlock (tle-source.ts), fetching same-origin ${base}/data/station-tles.json, memoised per session, with no browser→Celestrak egress. Both the AR sky and the Lab's injected iss-pass TLE go through it.
  • The build-baked import (stations.ts → static/data/station-tles.json) is the offline / MCP fallback. The MCP image carries the baked copy; a manually-deployed container that lags is not silently wrong because iss-pass discloses epochAgeDays vs staleAfterDays (H5).

Consequences ​

  • No browser egress; one Celestrak-fetching code path; prod freshness ≤6h without a redeploy.
  • The AR-sky consumer is unchanged at the call site (resolveStationTle keeps its signature); only the source behind it changed.
  • Freshness is now uniform across the AR sky, the Lab, and (baked + disclosed) the MCP tool.
  • Trade-off: the served copy and the baked copy can diverge between deploys — intended. The app prefers the served (fresh) copy; the baked copy is the honest, age-disclosed fallback.

Orrery — architecture documentation · MIT · No tracking