1
0
Commit Graph

3 Commits

Author SHA1 Message Date
karti 1db868ad70 The sky is not something an account grants you
`liveData` was one flag meaning two unrelated things, and it was set to
`tier !== "anon"`. That reasoning does not survive asking what the data actually
is. Cloud cover over San Francisco is a reading from a government sensor. The
aircraft are broadcasting their positions, unencrypted, to anyone within range
who owns a forty-dollar receiver. Neither is withheld from anybody by anybody,
so neither is a thing an account can grant access to — and putting them behind a
sign-in cost the only moment that makes this project land: real fog rolling off
the Pacific onto a city you recognise, at the real time of day, on a first
visit. On an SSO-gated deployment it cost that moment for every visitor there
currently is.

So `liveData` splits. `liveEnvironment` is public and unconditional.
`liveMarkers` is asked for by everyone and granted by the server, because the
marker set is the one feed here that can carry something private — a company's
pipeline, a person's job search — and whether it is public is a property of the
deployment, not of a file in this repo.

`TERA_MARKERS_ACCESS` is therefore a server switch and its default is `members`,
which is the safe answer rather than the common one. The failure mode of getting
this wrong is silent: nothing throws, nothing looks broken, the data is simply
readable by the internet. An operator who wires real markers up gets the shut
door without having chosen it and has to say `public` out loud — and saying it
appends a line to `degraded[]`, so `/api/v1/health` reports that this box is
publishing its map without anyone having to go and read the env file. Same
reasoning as `TERA_ADMIN_SUBJECTS=*`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 03:36:12 -07:00
karti e41c90fe8d 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>
2026-08-06 03:25:31 -07:00
karti 5bc7258753 A plan view in the corner, a night you can actually see, and three kinds of visitor
The right half of the screen was empty sky. It holds the board now, drawn flat,
with the footprint of the camera's own frustum on it — the one part of a minimap
that earns its place, because it answers "where am I looking from" without
leaving the shot. Click it, drag it, scroll it. It is a 2D canvas rather than a
second WebGL context, cached per city and redrawn only when something moved.

Night was black. Not dark — black: at 3 a.m. the coastline, the hills and the
bay were one shape, and the frame read as a failed render rather than as
darkness. The sky already had a floor for exactly this reason and nothing did
the equivalent for the ground, so the ground has one now. The moon still has to
be worth computing, so the gap between a moonlit night and a moonless one is
preserved rather than filled in.

Three tiers, resolved once in the new src/access.ts: anonymous, signed in,
admin. Anonymous gets the map and a public office — the shell, the furniture,
the named viewpoints, nobody home — built without the private objects rather
than with them hidden, because scene.traverse makes hiding a leak with a bow on
it. The time scrubber and the debug readouts are admin only, and admin is
granted by TERA_ADMIN_SUBJECTS on the server and inferred nowhere else. An
unreachable API means member, never god: the promise is "clone it and it works",
not "clone it and you are an administrator of a deployment you did not
configure".

Three things this run found and fixed rather than shipped:

  - entryUrl came off the wire and went straight into an href with no scheme
    check, and a CSP of script-src 'self' 'unsafe-inline' does not stop a
    javascript: URL from navigating. One rejection point in access.ts now.
  - A 5xx from /health was the same null as "no API at all" and therefore the
    opposite conclusion. Eight seconds of tera-api restarting would have told
    every anonymous visitor they were a member. A 5xx is an answer; it fails
    closed.
  - decodeURIComponent in cookieToken was the one path in auth/index.ts that
    threw rather than returning ANONYMOUS, so one malformed cookie header from
    an unauthenticated caller turned /api/v1/session into a 500.

Also: keyboard shortcuts, focus rings, a boot state instead of a blank 2.3
seconds, a collapsible panel under 900px, and no horizontal overflow at 375,
768, 1440 or 2560.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 22:53:30 -07:00