d8afc42d152c90676463928945ba617f2c733677
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a229fb2721 |
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>
|
||
|
|
dac12cecec |
The floor keeps up with the room
Occupancy was fetched once, on the way in. Somebody sat down and you did not find out until you left the office and came back, which for a view whose entire subject is who is in the building is the point missed by one request. `watchPresence` is `watchWeather`'s shape without the two pieces of judgement that one needs and this does not: weather has to decide whether an observation is too old to be honest about and whether it describes the place you are looking at, and a roster is neither — it is true when it is served, and it is addressed by office id, so it cannot arrive about somewhere else. Thirty seconds, and the number comes from what the data does rather than from what the network will stand. Ten minutes is right for weather because nothing upstream of it moves faster; occupancy moves when a person stands up. Backoff tops out at five minutes rather than the weather watch's hour, because the difference between the two is what the user is doing while the box is down: nobody is staring at the sky waiting for it to be redescribed, and somebody *is* standing in a room they expect to see people arrive in. An hour of silence there reads as a broken feature rather than as a quiet API. Two things it does that the weather watch does not, both because this one runs while a person is looking at the thing it describes. It stops dead while the tab is hidden and asks again the moment it comes back — a backgrounded tab polling a roster nobody can see is waste on both ends, and it is the commonest state a long-lived office tab is in; coming back has to be immediate rather than at the next tick, or you return to a floor up to thirty seconds stale at exactly the moment you are looking hardest. And it publishes only on change, by signature rather than by identity: every answer is a fresh array, so without the comparison a still floor would rebuild its presence meshes twice a minute forever. The watch belongs to the visit and not to the page, which is `weatherWatch`'s rule for a sharper reason — a roster is requested with a credential and names people, so one left running after somebody stepped out to the city is a page quietly asking about a room nobody is looking at. Stopped before the scene swap in `leaveOffice`, so the last thing it can do is abort a request rather than publish into a room already left, and stopped again in `mountCity`, which disposes the office under it. Checked in the browser, both exits. Standing in the office: one ask on arrival, the next 31 s later under failure backoff, not a flood. Stepping out by the `O` key on the office host (a real navigation) and by the scene swap on `?view=office` (which is what actually exercises `stop()`): zero requests in the following 45 s, in both. |
||
|
|
3e9b97ed8b |
Signing in puts people in the building
`member` and `anon` were told apart inside the office by the client and by nothing else. `access.ts` picked an `officeDepth`, `createOfficeScene` built a presence layer at full depth — and then nobody ever called `setPresence`, so both tiers rendered the identical empty room. A tier that changes nothing you can see is not a tier, and `routes/markers.ts` had already written down why one drawn only in the browser is worse than none: it is a UI hiding a control over a body the API hands to whoever asks. So the refusal happens on the server now. `GET /api/v1/offices/:id/presence` is the one route that always takes a session, whatever else the deployment is configured for. `markers.ts` serves its feed to anonymous callers when no feed is configured, on the grounds that there is nothing there to protect; that reasoning does not transfer, and the difference is the whole point — a marker is a company at an address and a presence is a person at a desk. The ordering inside the handler is the security property, not a detail. It resolves the viewer *before* it looks at the id, so an anonymous caller gets an identical 401 for a real office, a private one and one that was never created. Check the office first and 404-for-unknown against 401-for-known tells them apart perfectly, which is the enumeration oracle CONTRACT.md §6 forbids, wearing a different status code. Three requests and one `deepEqual` hold that down. `TERA_PRESENCE_DIR` is a second directory rather than a `people` field on the pack, and that is the design. `types.ts` says a `Presence` binds to a `seatId` and never to a coordinate so the geometry can be published while the people cannot — which buys nothing if both live in one file, because an operator who wants a public floorplan then has to strip the roster out by hand, and the first time they forget the leak is permanent. Two directories makes the safe thing the default thing. An office with no roster is 200 and empty, never 404: "no such office" and "nobody has told me who is in this one" are different problems with different fixes, and one 404 sends an operator after the wrong one. On the client, occupancy arrives after the room is on screen rather than before — the building is worth looking at while a second request is in flight. An API that answers is believed, including when it answers with nobody; an office where everyone has gone home is a real fact and overwriting it with invented people to liven up the demo is the one thing this must never do. An API that does not answer falls back to a fabricated roster, exactly as the markers do, because a clone with no server is the flagship case and a member shown the same empty room as a stranger has been told the tier means something when it does not. Those twenty-five people are invented and the page says so. `sample.ts` says it to a reader of the source; `#office-badge` now says "Sample occupancy — these people are invented" to the person looking at the room, and it is not suppressed when a real deployment's API merely happened to be down — that is exactly the case where a member would otherwise read invented names as their colleagues. Fabricated names at real desks look like a staff list, and a screenshot of one must not be possible to take without the caption. The floor plan marks the occupied desks, one colour for everybody where the scene has four: at three device pixels a hue is a guess. The plan answers "is anyone there" and the room answers "who, and what are they doing". Hovering a desk names them, and the readout reads as an address getting more specific — metres, then room, then person. server: 127 tests pass, 11 of them new. Client typechecks and builds; the office chunk absorbed the plan renderer and the entry chunk moved 2.3 kB for the sample roster. Checked in the browser at office.lumbridgecorp.com: FULL VIEW, the badge, figures at the benches, dots on the plan, and "3.7, 16.7 m · Alcatraz · Clementine Roux" under the pointer. |
||
|
|
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>
|
||
|
|
29c93c1dd3 |
An empty live feed is not live data
A server whose marker source is unconfigured answers 200 with an empty array, and the client counted that as success. The deployed map came up with no pins at all, wearing a green "live data" badge — which is the one outcome worse than having no badge, because the badge exists precisely so a fabricated demo cannot be mistaken for real data, and here it was doing the reverse. Empty now falls back to the sample set and keeps saying "sample". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
44c5a79424 |
SoCal, the whole bay, a moon, and gates that actually run
Six agents in parallel, and the two city packs independently reported the same blocker: `focusRegions` and `coarseFactor` existed on the `City` type and nothing implemented them. Uniform lattices would have been 2.9M points for Southern California and 3.7M for the expanded bay. Both packs were unloadable as written. `buildAxis` is the answer, and it is honest about its limits: refinement is per axis, not per rectangle, so a focus region sharpens its whole row *and* its whole column. Two regions at opposite corners refine nearly everything between them. Measured, not guessed — the bay went 0.53M points with one region and 1.64M with three, for detail nobody is looking at from a board this wide. One region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s. Then three things that were only ever right because San Francisco was the only city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a 230-unit board; the bay is 1003 units across and the camera physically could not retreat far enough to frame it. Fog distances were scene units pinned to the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest weather, which over ninety-four kilometres of bay correctly hides three quarters of it — the night view was a black rectangle for a completely reasonable reason. All three now derive from the board. The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a physical ratio of one to four hundred thousand. What is being reproduced is what a moonlit night looks like on a screen in a lit room. The CI gate caught itself, which is the part worth keeping. Port 8431 was already held by a server from an earlier session, so the boot check polled a healthy stranger while the process it started died on EADDRINUSE. It now refuses to run rather than pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |