1
0

Real weather, real aircraft, a heightfield off the main thread, and instruments

Three things that were built and never connected, connected.

**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.

**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.

**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.

**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.

Two blockers the review caught:

  - Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
    — and ~10.5 shader programs, and deleteTexture had never been called once in
    the app's lifetime. The renderer was being built per scene; it belongs to the
    canvas, for the life of the page.
  - An upstream fetch that threw rather than returning null skipped the cache
    stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
    upstream request per inbound request, and the caller got a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 03:25:31 -07:00
parent a6f6a91813
commit e41c90fe8d
39 changed files with 8482 additions and 503 deletions
+35 -5
View File
@@ -1,18 +1,48 @@
/**
* `GET /api/v1/flights`.
* `GET /api/v1/flights` — for a city, not for the box.
*
* `?city=socal`, or `?lat=&lng=` resolved against the same allowlist the weather
* route uses, or neither for the default region. `regions.ts` owns the
* validation, the refusal, and the reasoning behind refusing at all: a live
* traffic endpoint that will fetch any coordinate on demand is an amplifier
* pointed at a volunteer-funded feed.
*
* Publicly cacheable, because the whole design of the plan is that one response
* serves every viewer for its whole TTL. Aircraft are not personal data and this
* body never varies by who asked.
* serves every viewer of a region for its whole TTL. Aircraft are not personal
* data and this body never varies by who asked — only by where.
*
* ### `radiusNm` on the query is ignored, deliberately
*
* The browser sends one. It is dropped, and the size of the circle stays
* `TERA_ADSB_RADIUS_NM`, for a reason that is not stubbornness: the cache key
* would have to include it, and a caller who can choose the key can make this
* box hold an unbounded number of entries and issue an unbounded number of
* distinct upstream requests — each one more expensive than the last, since a
* wider circle is more work for the feed to answer. Every bound in
* `flights/index.ts` and `regions.ts` rests on the key space being the operator's
* region list, and a query parameter that widens it dissolves all of them.
*
* An operator whose board is bigger than the circle raises
* `TERA_ADSB_RADIUS_NM`; 60 nm covers both shipped cities. The client already
* discards aircraft outside the region it drew, so a circle that is too large
* costs a little bandwidth and nothing else.
*/
import type { FastifyInstance } from "fastify";
import { publicCache } from "../cache.ts";
import { resolveRegion, type RegionQuery } from "../regions.ts";
import type { ErrorBody } from "../../../src/server/wire.ts";
import type { Services } from "../services.ts";
export function registerFlights(app: FastifyInstance, services: Services): void {
app.get("/api/v1/flights", async (req, reply) => {
const body = await services.flights.current();
app.get<{ Querystring: RegionQuery }>("/api/v1/flights", async (req, reply) => {
const resolved = resolveRegion(services.config.regions, req.query);
if (!resolved.ok) {
const error: ErrorBody = { error: "bad_request", message: resolved.message };
return reply.code(400).send(error);
}
const body = await services.flights.current(resolved.region);
publicCache(req, reply, body.ttlSeconds);
return body;
});