75908774f7
The real reason the sky was fake, and it was not the timeout. `api.adsb.lol` answers `403` with the body `"User-Agent too generic; include valid contact info."` to any request that does not say who is making it. Node's `fetch` sends nothing useful, so every live flight fetch was refused — in 268 ms, not at the 6 s timeout, which is why the previous commit's longer budget changed nothing. `http.ts` already exported `userAgent()` and `weather/metno.ts` already used it; flights simply never did. `config.flights.contact` now follows the pattern satellites already established: `TERA_FLIGHTS_CONTACT`, falling back to `TERA_WEATHER_CONTACT`, because an operator has one contact address and not four. This failed in the way worth being angry about. A `403` became `null` in `getJson`, `null` means "serve the simulated plan" in `flights/index.ts`, and both of those are correct in isolation. So visitors got fabricated aircraft while `/health` reported `flights: adsb` — the source *was* configured and the host *was* reachable — and `degraded[]` stayed empty because nothing had degraded at boot. Every signal the service publishes about itself said it was fine. `server/src/test/adsbUserAgent.test.ts` asserts the header carries the contact, that the URL is still the region asked for, and that a non-allowlisted host is refused before a single byte reaches the network. A load-bearing header with no test is a header that comes off in a refactor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>