1
0

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>
This commit is contained in:
2026-08-05 22:53:30 -07:00
parent 47faec9f9d
commit 5bc7258753
24 changed files with 3982 additions and 211 deletions
+88 -6
View File
@@ -21,6 +21,26 @@
* this building — see ARCHITECTURE.md §3.3, and the note on `Presence` in
* `interiors/types.ts`, which is the same rule one level in.
*
* ### Two audiences, and why this pack barely uses the second one
*
* A handful of items below carry `audience: "private"`, which means a public
* build — `createOfficeScene(office, { depth: "public" })`, the office an
* anonymous visitor gets — does not construct them. There are seven of them: two
* zones and five objects in the server room. Everything else in this file is
* built at both depths, and that is the honest answer for this pack rather than
* a gap in it. The difference between the public building and the signed-in one
* is **the people**, and the people are not in this file at all.
*
* That is the whole reason a pack can be published. `Presence` binds by seat id
* and arrives from somewhere else, so the geometry and the occupancy are two
* separate acts and only one of them is happening here. Read the note on
* `Audience` in `interiors/types.ts` before reaching for the field, especially
* the part that says it is a UI tier and not a security boundary: **this file is
* bundled into the static build**, so a self-hoster who marks their real floor
* plan private has published their real floor plan with an extra step. The
* building here is fabricated. If yours is not, the thing that keeps a stranger
* out is your API, not this field.
*
* ### The coordinate frame
*
* Metres, `1 unit = 1 m`, the floor on the XZ plane with +Y up. The origin is
@@ -51,6 +71,7 @@
import type {
AssetId,
Audience,
DeskBank,
Level,
Office,
@@ -281,7 +302,7 @@ function scatter(
prefix: string,
kind: AssetId,
points: Point2[],
opts: { rotation?: Yaw; elevation?: number; colorKey?: string } = {},
opts: { rotation?: Yaw; elevation?: number; colorKey?: string; audience?: Audience } = {},
): Prop[] {
return points.map((position, i) => ({
id: `${prefix}-${String(i + 1).padStart(2, "0")}`,
@@ -290,6 +311,10 @@ function scatter(
rotation: opts.rotation ?? NORTH,
elevation: opts.elevation,
colorKey: opts.colorKey,
// A run of identical props is a run of identical props at both depths.
// Nothing in the format stops a pack marking one rack of four private, but
// half a row is a stranger thing to look at than no row.
audience: opts.audience,
}));
}
@@ -1143,9 +1168,35 @@ const PROPS: Prop[] = [
// hot-aisle arrangement and also the only way two rows of anything read as
// deliberate. A storage locker is not a rack, but it is a 1.2 x 0.5 x 1.8 m
// box with a front and a back, and at this scale that is a rack.
...scatter("mdf-rack-n", LOCKER, grid(27.4, 13.4, 2, 1, 1.3, 0), { rotation: NORTH }),
...scatter("mdf-rack-s", LOCKER, grid(27.4, 15.8, 2, 1, 1.3, 0), { rotation: SOUTH }),
{ id: "mdf-shelf", kind: SHELF, position: { x: 29.4, z: DEPTH - EXT_FACE - 0.18 }, rotation: SOUTH },
//
// The five objects in this room are the pack's one worked example of
// `audience: "private"`. The room, its raised floor, its walls and its lid are
// all still built at public depth — the *architecture* is not the secret — but
// what is standing in it is not shown to a stranger. How many cabinets an
// organisation runs and which way the aisle faces is the kind of detail that
// is worth nothing to a visitor and something to somebody else, and the room
// already keeps its ceiling for the same reason, which is the pack saying the
// same thing twice in two vocabularies.
//
// At public depth this leaves a lit, empty, raised-floor room, which is an
// honest picture of a room you are not being shown the inside of. The lights
// stay: a dark hole in a floor plan reads as a rendering fault, not as
// discretion.
...scatter("mdf-rack-n", LOCKER, grid(27.4, 13.4, 2, 1, 1.3, 0), {
rotation: NORTH,
audience: "private",
}),
...scatter("mdf-rack-s", LOCKER, grid(27.4, 15.8, 2, 1, 1.3, 0), {
rotation: SOUTH,
audience: "private",
}),
{
id: "mdf-shelf",
kind: SHELF,
position: { x: 29.4, z: DEPTH - EXT_FACE - 0.18 },
rotation: SOUTH,
audience: "private",
},
...scatter("mdf-light", TROFFER, grid(27.6, 14.0, 2, 2, 2.0, 2.4), { elevation: 2.6 }),
// -- Facilities -----------------------------------------------------------
@@ -1169,10 +1220,41 @@ const PROPS: Prop[] = [
* "eng" is a team, a cost centre or a colour scheme is not the engine's
* business — the same rule as `Marker.colorKey`, which is why there is no
* `kind` field to be tempted by.
*
* ### Two of them are private, and it is the names that make them so
*
* The engine cannot tell these four apart, but a reader can. "Social" and
* "Focus" describe what the floor is *for*: anyone standing in the lounge can
* see that it is the lounge, and a public visitor learning that the three
* glass-lidded boxes are the focus booths has learned nothing they could not
* have guessed from the plan.
*
* "Engineering" and "Studio" describe who *sits* there, and that is a different
* kind of fact. It is org chart drawn on a floor: how many desks each function
* has, where they are relative to each other, which corner grew last quarter.
* Nobody signed in is surprised by it and nobody anonymous is owed it, which is
* exactly the line `audience` exists to draw — so the two team zones are not
* built at public depth and the two spatial ones are.
*
* This is the granularity the field is for. The alternative anybody reaches for
* first is a single `private: true` on the whole floorplan, and it is useless:
* a building is not private or public, the labels on it are.
*/
const ZONES: Zone[] = [
{ id: "zone-eng", name: "Engineering", outline: rect(8.0, 0.6, 18.9, 7.6), colorKey: "team-a" },
{ id: "zone-studio", name: "Studio", outline: rect(19.4, 0.6, 25.2, 7.6), colorKey: "team-b" },
{
id: "zone-eng",
name: "Engineering",
outline: rect(8.0, 0.6, 18.9, 7.6),
colorKey: "team-a",
audience: "private",
},
{
id: "zone-studio",
name: "Studio",
outline: rect(19.4, 0.6, 25.2, 7.6),
colorKey: "team-b",
audience: "private",
},
{ id: "zone-social", name: "Social", outline: rect(SOCIAL_W, 0, WIDTH, SPINE_N), colorKey: "social" },
{ id: "zone-focus", name: "Focus", outline: rect(X_BOOTH_1, BOOTH_N, X_BOOTH_E, SPINE_N), colorKey: "focus" },
];