1
0

feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul

The build the studios needed, across eight workstreams and one strict file
partition.

**The render rig was the quality ceiling.** The renderer ran three's
NoToneMapping default while atmosphere drove the sun to 2.35 and assets set
emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is
why walls blew out and every fitting looked like a white rectangle. ACES filmic
tone mapping and an explicit output colour space land in `stage.ts`, and the
atmosphere intensity table and palette headroom are re-tuned against the new
curve rather than left tuned for the clipping we removed.

`engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally,
so nothing binary is committed. There was no environment map anywhere before, so
every `metalness > 0` role had nothing to reflect and rendered dull grey — a
defect the code already documented against itself in `office/optimus.ts`, where a
whole material role was abandoned over it, and worked around in `modelX.ts` with
a fake emissive that this change deletes. Atmosphere remains the sole light
owner; the rig derives from the `LightingState` it already produced.

**Studio hardware exists.** There was no device concept anywhere in the product:
no type, no route, no state. `devices/types.ts` fixes a declaration/state/
capability/command contract that a smart light, a thermostat, a door sensor and a
charger all fit without a schema change, and both studios now carry a desk mic
and a computer speaker with deterministic simulated behaviour behind an adapter
seam a real API can occupy later. Reads are the demo and are open; commands are a
signed-in action and are kept off the read body entirely, because a shared cache
replaying a GET that turned a microphone on is exactly what the fail-closed
cache default exists to prevent.

**The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the
response was served publicly cacheable, and the attribution hardcoded adsb.lol
regardless of where the endpoint pointed — one env var away from republishing
non-redistributable data under an open-terms credit. The host is now allowlisted,
the credit is derived from the host actually configured, public cacheability is
conditional on redistributability, and a refused endpoint demotes to simulated
flights and says so in `degraded[]`. The gate is on the source, not the feature:
live aircraft and their detail cards stay open to anonymous visitors.

**The LA studio was never the smaller pack** — 16 rooms and 248 props against
SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were
ceiling troffers, it bound no props to seats, placed none of the habitat kit, and
12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather
than more instances, because `furnish.ts` draws once per kind and folds colour
into the batch key, so repeat instances add nothing the eye can read.

**The interface stops being forty imperative mutations.** Every visibility
decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the
chrome has coverage for the first time. Deleted: ~100 lines of CSS and two
bindings targeting elements that no longer exist, and a `body:has()` rule that
shifted the desktop layout by 160px for touch controls hidden there. Fixed: the
office picker tabs that drew their label and their badge on top of each other.
Added: a first-run flow, because the product is two verbs and neither was ever
stated on screen. Mobile is designed on its own terms instead of being the
desktop with things hidden — the plan view comes back, and the keyboard-only
shortcuts button is replaced by touch controls.

`arena/studioOps.ts` frames the whole thing as the multi-variable environment it
is, wrapping the same simulators the renderer drives rather than a headless copy.

Also removed `input/vehicle.ts`, which nothing but its own test imported.

Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six
matrix cells, no-binaries, provenance, dependency licences, zero-config boot and
arena source hashes all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 19:44:24 -07:00
parent 8738367258
commit db074e9cf7
150 changed files with 36237 additions and 2586 deletions
+187 -1
View File
@@ -27,10 +27,22 @@
* | `GET /markers` | `MarkersBody` | yes |
* | `GET /offices/:id` | `OfficeDoc` | public offices only |
* | `GET /offices/:id/presence` | `PresenceBody` | never |
* | `GET /offices/:id/devices` | `DevicesBody` | never |
* | `POST /offices/:id/devices/command` | `DeviceCommandResultBody` | never |
*
* Note the last two. Reading device state and commanding a device are two
* routes and two methods, and that is a **security boundary rather than REST
* taste**: a command that rode in the read body could be replayed by any shared
* cache that had kept a copy of the GET, and turning a microphone on by
* replaying a cached read is precisely the outcome the fail-closed
* `private, no-store` default in CONTRACT.md §5 exists to prevent. Neither
* route is ever `publicCache`d, and the command route is the only body in this
* file that goes *up* the wire.
*
* See CONTRACT.md §5.
*/
import type { DeviceCommand, DeviceState } from "../devices/types.ts";
import type { Marker, SatelliteGroup } from "../engine/types.ts";
import type { Office, Presence } from "../interiors/types.ts";
@@ -61,8 +73,36 @@ export type WeatherSourceId = "none" | "nws" | "metno" | "openmeteo";
export type FlightsSourceId = "sim" | "adsb" | "dump1090";
export type SatellitesSourceId = "none" | "celestrak";
export type MarkersSourceId = "none" | "file";
/**
* Where device readings come from.
*
* `none` is the default and serves an empty array — a box nobody has told about
* any hardware has no hardware, which renders as a studio whose panels say so
* rather than as an error. `sim` is the deterministic state machine in
* `src/devices/sim.ts`, the same module the arena wraps, and it is what this
* build ships. `homeassistant` is named here and implemented nowhere: it is the
* door a `first-party-sensor` provenance comes through, and naming it in the
* union now is what stops the next person from adding a second, differently
* shaped source field when they build it.
*/
export type DevicesSourceId = "none" | "sim" | "homeassistant";
export type AuthMode = "none" | "sso" | "jwt";
/**
* One place this box will answer about. Structurally `Region` in
* `server/src/regions.ts`, restated here for the same reason `WireSimRoute` is:
* this file is the contract and the server's own module is an implementation of
* it, and the browser must not have to import server code to read a body.
*/
export interface WireRegion {
/** Url-safe, and the same id the browser's city pack uses: `sf`, `socal`. */
id: string;
lat: number;
lng: number;
/** Kilometres. */
radiusKm: number;
}
/**
* What this deployment turned out to be, once the environment had its say.
*
@@ -81,12 +121,33 @@ export interface HealthBody {
flights: FlightsSourceId;
satellites: SatellitesSourceId;
markers: MarkersSourceId;
/**
* Newer than the four above it, and read defensively by
* `feedsFrom()` in `src/access.ts` for exactly that reason: a browser
* meeting a server one version behind this one sees `undefined` and
* concludes the box has no devices, which is the safe direction for a
* missing field to fall. Its job is to let a client know that asking is
* pointless before it opens a watch that will 404 forever.
*/
devices: DevicesSourceId;
};
auth: {
mode: AuthMode;
/** Where a browser sends someone to sign in. `null` unless mode is `sso`. */
entryUrl: string | null;
};
/**
* Every place this box will answer about, in the config's order, so the first
* entry is what a request with no query gets.
*
* Here rather than in a `HealthBodyWithRegions` alias next to the route.
* Weather and flights refuse a place this box does not serve, so a client
* that guesses `?city=` earns a 400 it cannot explain; publishing the
* allowlist turns that into one question asked once. It gives nothing away —
* knowing what is served is not the same as widening it, and the ids are the
* names of cities the map already draws.
*/
regions: WireRegion[];
/** One human sentence per demotion. Empty on a fully-configured box. */
degraded: string[];
}
@@ -119,6 +180,23 @@ export interface WireAircraft {
/** Degrees clockwise from true north. */
heading: number;
callsign?: string;
/**
* The transponder's 24-bit ICAO address, lowercase hex, when the feed gave a
* real one.
*
* Carried explicitly even though `id` is usually the same string, because
* "usually" is the problem: `id` falls back to the callsign for a record with
* no hex, and both community feeds emit `~`-prefixed anonymous addresses for
* TIS-B and MLAT targets, which are *not* ICAO addresses. Somebody pastes
* this into a registry lookup, so a wrong one names another aircraft
* altogether — and `Aircraft` in `engine/types.ts` has nowhere to put it,
* which is why the adapter keeps this record beside the position rather than
* inferring the address back out of the id.
*
* Absent, never invented. `aircraftDetail()` in `engine/flights.ts` is where
* it becomes a card.
*/
icao24?: string;
}
/**
@@ -153,8 +231,33 @@ export interface FlightsLiveBody {
observedAt: number;
aircraft: WireAircraft[];
ttlSeconds: number;
/** Attribution the consumer is expected to display, if the feed asks for it. */
/**
* Attribution the consumer is expected to display, if the feed asks for it.
*
* **Derived from the host that answered**, in `server/src/flights/licence.ts`,
* and never authored next to the request. It said `adsb.lol` unconditionally
* once, whatever `TERA_ADSB_ENDPOINT` pointed at, which is how a credit line
* and a source come to disagree.
*/
attribution?: string[];
/**
* May a shared cache — or the consumer — hand these bytes to a third party?
*
* The licence the positions arrived under, reduced to the one bit that
* changes behaviour. `false` keeps the route on the fail-closed
* `private, no-store` default from CONTRACT.md §5, so a feed this deployment
* may *use* but not *redistribute* stops at the browser that asked. Required
* rather than optional: a body with no answer to this question is a body
* somebody will assume `true` for.
*/
redistributable: boolean;
/**
* The licence id the source publishes under, e.g. `"ODbL-1.0"`, or
* `"first-party"` for an operator's own receiver. For display and for a
* human reading `/api/v1/flights` directly; the machine-readable half of the
* same fact `attribution` states in prose.
*/
licence?: string;
}
export type FlightsBody = FlightsPlanBody | FlightsLiveBody;
@@ -368,3 +471,86 @@ export interface PresenceBody {
* an index and is never publicly cached.
*/
export type OfficeVisibility = "public" | "unlisted" | "private";
// ---- Devices --------------------------------------------------------------
/**
* What the hardware in one office is doing, right now.
*
* The runtime half of the split `src/devices/types.ts` opens with, and the
* reason this body exists at all: a `DeviceDeclaration` is authored into the
* office pack and is therefore public by construction, while a `DeviceState`
* never appears in a file anybody can download. It arrives here, from a route
* that can refuse it, and it is the same line `PresenceBody` draws between a
* floorplan and the people standing on it.
*
* **Never publicly cached, in any configuration.** Two reasons and they are
* different: the body took a credential to obtain, so a shared cache holding it
* would hand one viewer's copy to the next; and the state is mutable by a
* command, so a cached copy is a stale claim about a room somebody is standing
* in. `routes/devices.ts` therefore never calls `publicCache`, and says so out
* loud rather than merely omitting the call.
*
* An office with no authored devices is `devices: []` and a 200 — not a 404.
* "This office does not exist" and "nobody has declared any hardware in it" are
* different facts with different fixes, exactly as `presence/store.ts` argues
* for a missing roster.
*/
export interface DevicesBody {
officeId: string;
devices: DeviceState[];
/** Epoch milliseconds at which this snapshot was taken. */
observedAt: number;
source: DevicesSourceId;
/**
* Did anybody observe any of this?
*
* `true` for everything this build ships, because the only implemented source
* is a state machine. It is the body-level statement of the same fact
* `DeviceState.synthetic` makes per device, and it is carried separately so
* that an empty array still says where it came from — an empty `devices` with
* `synthetic: false` is a box with a real bridge and nothing plugged into it,
* which is a different picture from a box that is making it all up.
*/
synthetic: boolean;
ttlSeconds: number;
attribution?: string[];
}
/**
* A command, going up.
*
* The only body in this file that travels from the browser to the server, and
* deliberately the narrowest one: exactly one command, for exactly one device,
* with no batching. A batch would need partial-failure semantics, and the first
* write surface in this product is not the place to invent those.
*
* The command is validated **server-side against the resolved office plan** —
* that the device id names an authored declaration, that the declaration's
* asset really is hardware of the kind it claims, and that the op is one that
* declaration declared. `normalizeDeviceCommand()` is the shared validator and
* both ends run it, which is not redundancy: the browser runs it so a slider
* cannot send nonsense, and the server runs it because a browser is not a
* boundary. The same move `officeHasMediaBinding()` makes for screens.
*/
export interface DeviceCommandBody {
command: DeviceCommand;
}
/**
* What a command did, as the state that resulted from it.
*
* The new state rather than an `ok: true`, so the panel has something to draw
* without a follow-up GET — and so the answer to "did that work" is the reading
* itself rather than an acknowledgement that a request was received. A command
* that was accepted and clamped (a gain of 40 dB on a device whose range stops
* at 36) reports the clamped value here, and the slider snaps to what the
* hardware actually did.
*/
export interface DeviceCommandResultBody {
officeId: string;
/** The device as it stands after the command was applied. */
device: DeviceState;
/** Epoch milliseconds. */
observedAt: number;
}