1
0

The sky gets the things above the aeroplanes

Satellites, end to end: CelesTrak element sets behind the same TTL cache
the weather and the flights use, served as TLEs rather than as positions,
and propagated in the browser with SGP4.

Sending elements is the same trick `flights/plan.ts` plays and it has a
better excuse here — a TLE *is* the closed form, valid for days either
side of its epoch, so one cacheable fetch every six hours replaces a poll
and every viewer agrees about where everything is.

Two things are worth knowing about the shape of it:

  - There is no region parameter. An aeroplane at 10,000 m is local and
    a satellite at 550 km is above the horizon for a circle two thousand
    kilometres across, so one catalogue serves both boards and the client
    decides what is above its own horizon. Only the observer is per-city,
    which is why `main.ts` shares the elements and rebuilds the catalogue.
  - The layer draws on a dome, because it cannot draw anywhere else.
    `world.metres(550_000)` is 21,000 scene units against a far plane at
    3,000. Azimuth and elevation are real; the radius carries nothing.

Off by default: a clone that started pulling CelesTrak on `npm run dev`
would have volunteered somebody else's bandwidth for its onboarding.

Godmode gets the two dials that point at the sky rather than at the
light — fabricated traffic, which composes with a live ADS-B feed instead
of replacing it, and a switch for the satellite layer with a count beside
it. Both are god-only lies about the inputs, in the manner of the weather
override.

`satellite.js` is the second runtime dependency this package has taken.
Its entry point star-exports an Emscripten build that cannot be shaken
out, so `noWasmPropagator` in the Vite config cuts it: 308 kB of WASM
loader for a bulk propagator nothing calls, against 26 kB for the SGP4
that does the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 20:57:14 -07:00
parent 0cc2126e85
commit a229fb2721
23 changed files with 2062 additions and 12 deletions
+31
View File
@@ -42,6 +42,7 @@ import {
type SimRoute,
type SkyRegion,
} from "../engine/flights.ts";
import type { SatelliteElements } from "../engine/satellites.ts";
import type { Aircraft, FlightSource, Marker, MarkerPalette } from "../engine/types.ts";
import { seededRandom } from "../engine/world.ts";
import type {
@@ -51,6 +52,7 @@ import type {
MarkersBody,
OfficeDoc,
PresenceBody,
SatellitesBody,
WeatherBody,
} from "../server/wire.ts";
import { SAMPLE_MARKERS, SAMPLE_PALETTE } from "./sample.ts";
@@ -223,6 +225,13 @@ export interface TeraClient {
* should pass them; `sampleRoutesFor` in `sample.ts` has them.
*/
flights(region: SkyRegion, fallbackRoutes?: SimRoute[]): TrafficSource;
/**
* Every element set this deployment serves, once. `[]` when it serves none,
* which is the default and is not an error.
*
* Not per-region and not watched — see the implementation for both reasons.
*/
satellites(options?: { signal?: AbortSignal }): Promise<SatelliteElements[]>;
/**
* One office pack. `null` for anything the server will not serve — including
* a private one, which answers 404 rather than 403 so the endpoint cannot be
@@ -361,6 +370,28 @@ export function createTeraClient(options: TeraApiOptions = {}): TeraClient {
return new HttpFlights(get, region, fallbackRoutes ?? syntheticRoutes(region));
},
/**
* The satellite catalogue, once.
*
* The only feed here with no watcher, no back-off ladder and no fallback,
* and all three absences are the same fact: element sets are good for days
* and the server caches them for hours, so there is nothing to poll for. One
* fetch per page load is not a compromise, it is the whole requirement.
*
* No sample constellation underneath it either, unlike `markers` and
* `flights`. An invented aeroplane is a plausible aeroplane; an invented
* Starlink is a false claim about a numbered object somebody could go
* outside and fail to find. `[]` is the honest answer and it renders as an
* empty sky, which is what a box with no satellite source actually has.
*/
async satellites(opts: { signal?: AbortSignal } = {}): Promise<SatelliteElements[]> {
const body = await get<SatellitesBody>("/satellites", {
...(opts.signal ? { signal: opts.signal } : {}),
});
if (!body || !Array.isArray(body.satellites)) return [];
return body.satellites;
},
office: (id) => get<OfficeDoc>(`/offices/${encodeURIComponent(id)}`),
/**