1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/ui/firePanel.ts
T
karti b25f217e3e feat: real fire on the boards, the LA office as a twin, and a night sky worth reading
The world stops being a simulation of California and starts being California.

**THE PROMOTION GATE WAS THE FIRST COMMIT, BEFORE ANY ORANGE PIXEL EXISTED.**
On today's live store the SoCal board contains 22 incidents. Every one has NULL
acreage and fifteen are nameless LA County dispatch numbers. Drawn naively that
is 22 orange marks over Los Angeles on a day nothing is burning — in a frame that
contains no other warm colour, so one glyph would be the most salient object on
the board and twenty-two would spend its credibility permanently.

`acres >= 10 AND contained < 80 AND type != 'RX' AND last_seen = max(last_seen)`
returns 0 on SoCal, exactly 5 on California, 0 on the Bay — same body, same day,
three correct answers. The empty board is a deliverable, not a fallback: it says
"No active fire on this board — CAL FIRE and WFIGS, just now", states that 21
records were gated and why, lists the largest fires burning OUTSIDE the frame
with distances, and counts the hot pixels it is deliberately not drawing.

**The privacy leak is structurally impossible rather than carefully avoided.**
cloud-1 serves a projection; the four home-relative columns never leave that box.
`observations.threat` was the one that nearly got through — it is
`(16/distance)^2 x log10(acres) x momentum x containment x wind-alignment`, so
with acreage and containment public it inverts to a distance circle around a
house and three fires give an intersection. A grep of the built bundle for
distance_km, bearing_deg, threat, 7762 and the street name returns nothing.

**Deliberately not used, and both would have produced a confident wrong answer:**
the store's `air` table retains only the last parameter of each poll, so all 78
rows read "Good" while the live feed reports ozone 101 "Unhealthy for Sensitive
Groups" — haze driven off it would clear the sky during a smoke event. And
`weather` is written only inside the NWS alerts loop, so a quiet day stores no
wind at all. Tera's own per-region NWS wind is already correct and already what
the clouds drift on.

Satellite detections are drawn as evidence and never as incidents. The permanent
industrial heat source 4.7 km from the owner's house is flagged persistent and
dropped, asserted by a test that first proves it is present in the fixture.
MODIS integer confidence and VIIRS string confidence are branched on `sat`.

**The LA office is a twin.** Its entire authored second storey — Model Loft,
Model Bay, The Materials Room, 430 lines nobody had ever stood in — is reachable
on foot: a walker crosses level-1 to level-2 in 73 fixed steps, floorY 0 to 5,
verified against the real pack rather than a synthetic plan. Its two studio
devices read real hardware through a field-allowlisted bridge: mute, volume and
reachability only. Never level, because there is no passive level upstream and
obtaining one would record a room with people in it. Never dB, because upstream
is gainPct across four different native scales. The bridge refuses all writes.

Fixed at its root: an anonymous visitor was getting permanently at-rest
instruments backing off against a 401. The tier moves into `createDeviceSource`,
so anon gets the living simulator three file headers already promised.

**Item 8 is closed, not fixed, and the correction is the point.** The Bay Area
"stutter" was GPU power management — the card sat at 500 MHz of 2725 through
every run that reproduced it, 4096/2048/1024/256 shadow maps all render in
1.21-1.31 ms, and two consecutive runs over a byte-identical dist gave 33.4 then
16.7. The allowance is removed and the cell is back to 16.7. Geometry is the
gate; frame time is advisory.

Item 7 was re-scoped after measuring: 1,069,006 of the Bay Area's 2,265,056
triangles were the second submission of the same buildings into the shadow pass.
Mobile now has its own triangle caps and bay-area mobile draws 1,266,096.

Also: bridges and the freeway corridor light up at night as emission, not lights
— 1,614 deck lamps and 18 tower heads on the Bay in two draw calls. The single
change that made US-101 legible was moving its edge lines from the lit material
to the unlit one: retroreflective paint, the argument the SFO night frame already
makes. California went 21,991 lamps to 4,051, clustered at the 17 town districts,
because a rural interurban corridor genuinely is unlit.

Tests 1137 -> 1340, server 280. All ten budget cells pass on first attempt with
no cap raised.

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

480 lines
18 KiB
TypeScript

/**
* What the board is actually saying about fire — including, almost every day,
* that there is none.
*
* ### The empty state is the feature
*
* Most days this panel's whole job is to render nothing, convincingly. That
* sounds like a small ask and it is the hardest sentence in the round to get
* right, because **a quiet board and a dead feed look identical**. On the day
* this was written the SoCal board drew zero fires — correctly, nothing was
* burning in Los Angeles — while twenty-two live incident records sat inside its
* bounds, every one with no acreage and fifteen of them nameless LA County
* dispatch numbers. A board that just showed empty terrain would have been right
* for a reason nobody could see, and would have been indistinguishable from a
* board whose upstream had been down since Tuesday.
*
* So there are **three** empty states here and they are never collapsed into
* one. This is the same argument `HealthBody.degraded` already makes for a log,
* applied to a picture:
*
* 1. `ageMs === null` — nothing has *ever* answered. That is a fault, and it
* says "no fire feed configured", not "no fire".
* 2. `drawn` empty with `suppressed > 0` — the board is quiet and *n* live
* records were refused. That is a finding, and the number is printed,
* because "twenty-two records did not meet the threshold" is a far stronger
* claim than silence.
* 3. `drawn` empty with `suppressed === 0` — nothing is happening inside this
* frame at all. Also a finding, and a different one.
*
* Every one of the three carries the fetch age, because a finding with no
* timestamp on it is not a finding.
*
* ### The board is a crop and says so
*
* `california.ts` caps at 38.05 N. Eighteen live incidents are north of that
* line, and on an ordinary day the largest fire in the whole store is one of
* them. A board captioned "California" that reads all-clear while 93,733 acres
* burn off-frame is the cry-wolf failure inverted — worse, in fact, because the
* viewer has no way to know the frame is the limit rather than the world. So the
* off-board section is not decoration; it is the sentence that makes the frame
* honest. It is fed from `FirePromotion.offBoard`, which is gated by the same
* ladder as `drawn` and capped at three, so it names the largest and never
* claims to be a complete list.
*
* ### Hot pixels are captioned as evidence, every time
*
* A satellite detection is a report that one cell of one overpass was warm. The
* upstream store contains a permanent industrial heat source 4.7 km from its
* operator's house that reports on every pass, on every day, with no incident
* behind it. The caption says "evidence, not incidents" and the persistent count
* is printed beside it, because a reader who can see the number of known flare
* stacks can calibrate the ones that are not.
*
* ### Nothing home-relative reaches this file
*
* `observations.distance_km`, `bearing_deg`, `threat` and
* `detections.distance_km` are computed against the upstream operator's house
* and invert to a circle around it. They never leave cloud-1, which is the whole
* design of the feed. This panel is the surface where such a value would
* eventually be *displayed*, so it is worth writing down here too: there is no
* distance-to-anything in this file, and a future "how far is that from me"
* feature is a new coordinate the viewer supplies, never one the feed carries.
*/
// ---- The shape this panel is handed ---------------------------------------
/**
* One fire, as the panel needs it.
*
* A structural subset of `DrawnFire` in `src/server/fires.ts`, restated rather
* than imported for the same reason the engine restates it: this module is DOM
* and strings, and it should not pull a wire module in to render a name. A
* `FirePromotion` is assignable to `FirePanelView` with no adapter.
*/
export interface FirePanelFire {
id: string;
name: string | null;
acres: number;
pctContained: number | null;
lat: number;
lon: number;
county?: string | null;
url?: string | null;
tier?: 1 | 2;
}
export interface FirePanelView {
/** ISO-8601 of the last **successful** upstream fetch. Epoch zero when never. */
fetchedAt: string;
/** Milliseconds since `fetchedAt`, or `null` when nothing has ever answered. */
ageMs: number | null;
drawn: readonly FirePanelFire[];
/** Gated fires outside the frame, largest first. Capped upstream at three. */
offBoard?: readonly FirePanelFire[];
/** Live rows inside the frame the gate refused. The number behind a calm board. */
suppressed?: number;
/** Hot pixels drawn. Known furniture is not in here; see `persistentDetections`. */
detections?: readonly unknown[];
persistentDetections?: number;
detectionWindowHours?: number;
}
export interface FirePanelOptions {
/**
* The board's bounds, so an off-board fire can be described as "north of this
* frame" rather than as "somewhere else". Optional: without it the direction
* is omitted rather than guessed.
*/
bounds?: { minLat: number; maxLat: number; minLng: number; maxLng: number };
/**
* Who the incidents came from, for the caption. Defaults to the two agencies
* the upstream collector actually reads.
*/
agencies?: string;
}
export interface FirePanelHandle {
root: HTMLElement;
apply(view: FirePanelView | null): void;
dispose(): void;
}
const DEFAULT_AGENCIES = "CAL FIRE and WFIGS";
// ---- Style ----------------------------------------------------------------
/**
* Exported so the stylesheet test can assert what a Node test cannot see: that
* nothing in here writes a raw `z-index`, and that the panel uses the shared
* tokens rather than inventing a second palette.
*/
export const FIRE_PANEL_CSS = `
.tera-fire { display: flex; flex-direction: column; gap: var(--s2, 8px); }
.tera-fire__state {
margin: 0;
font-size: 11px;
line-height: 1.5;
color: var(--ink, rgba(255,255,255,.78));
}
.tera-fire__state[data-state="fault"] { color: var(--amber-ink, #ffd68a); }
.tera-fire__note {
margin: 0;
font-size: 9px;
line-height: 1.6;
letter-spacing: .04em;
color: var(--ink-3, rgba(255,255,255,.4));
}
.tera-fire__list { display: flex; flex-direction: column; gap: var(--s2, 8px); margin: 0; padding: 0; list-style: none; }
.tera-fire__fire {
display: flex;
flex-direction: column;
gap: 2px;
padding: var(--s2, 8px) var(--s3, 12px);
border: 1px solid var(--hairline, rgba(255,255,255,.11));
border-left: 2px solid rgba(242, 177, 52, 0.55);
border-radius: var(--r-sm, 5px);
background: rgba(255, 255, 255, 0.03);
}
.tera-fire__fire[data-tier="2"] { border-left-color: rgba(255, 122, 42, 0.85); }
.tera-fire__name { font-size: 11px; letter-spacing: .04em; color: var(--ink, rgba(255,255,255,.78)); }
.tera-fire__facts {
font-size: 9px;
letter-spacing: .06em;
text-transform: uppercase;
font-variant-numeric: tabular-nums;
color: var(--ink-2, rgba(255,255,255,.56));
}
.tera-fire__section {
margin: 0;
font-size: 9px;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--ink-3, rgba(255,255,255,.4));
}
.tera-fire__off { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 2px; }
.tera-fire__off li {
font-size: 10px;
line-height: 1.5;
font-variant-numeric: tabular-nums;
color: var(--ink-2, rgba(255,255,255,.56));
}
.tera-fire__evidence {
margin: 0;
font-size: 9px;
line-height: 1.6;
color: var(--ink-3, rgba(255,255,255,.4));
border-left: 2px solid rgba(143, 196, 232, 0.45);
padding-left: var(--s2, 8px);
}
`;
const STYLE_ID = "tera-fire-panel-style";
// ---- Copy, as pure functions ----------------------------------------------
/**
* "4 minutes ago". The one number that separates a quiet board from a dead one.
*
* Coarse on purpose past the hour: this feed polls every ten minutes and an age
* printed to the second would imply a precision the cache does not have.
*/
export function relativeAge(ageMs: number | null): string {
if (ageMs === null || !Number.isFinite(ageMs)) return "never";
const seconds = Math.max(0, Math.round(ageMs / 1000));
if (seconds < 45) return "just now";
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
const hours = Math.round(minutes / 60);
if (hours < 36) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
const days = Math.round(hours / 24);
return `${days} day${days === 1 ? "" : "s"} ago`;
}
/** Grouped thousands, without pulling a locale into a test's expectations. */
export function formatAcres(acres: number): string {
if (!Number.isFinite(acres)) return "—";
const rounded = acres >= 100 ? Math.round(acres) : Math.round(acres * 10) / 10;
const [whole, fraction] = String(rounded).split(".");
const grouped = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return fraction === undefined ? grouped : `${grouped}.${fraction}`;
}
/**
* The headline sentence, and the three cases it must never collapse.
*
* Returned as a pair so the caller can tag the fault case for the stylesheet
* without re-deriving which case it is.
*/
export function fireHeadline(
view: FirePanelView,
agencies: string = DEFAULT_AGENCIES,
): { state: "fault" | "quiet" | "active"; text: string } {
const age = relativeAge(view.ageMs);
if (view.ageMs === null) {
return {
state: "fault",
text: "No fire feed configured — this board has never heard from one.",
};
}
const drawn = view.drawn.length;
if (drawn === 0) {
return { state: "quiet", text: `No active fire on this board — ${agencies}, ${age}.` };
}
return {
state: "active",
text: `${drawn} active fire${drawn === 1 ? "" : "s"} on this board — ${agencies}, ${age}.`,
};
}
/**
* The second line of a quiet board: what the gate refused, in a number.
*
* `null` where there is nothing to add. Twenty-two suppressed records is the
* most informative thing the SoCal board can say on an ordinary day, and it is
* the sentence that turns "we drew nothing" into "we looked, and here is what we
* looked at".
*/
export function suppressedNote(view: FirePanelView): string | null {
const suppressed = view.suppressed ?? 0;
if (view.ageMs === null) return null;
if (view.drawn.length > 0 || suppressed <= 0) return null;
return `${suppressed} live record${suppressed === 1 ? "" : "s"} inside this frame did not meet the threshold: under ten acres or no acreage at all, eighty percent contained or more, or a prescribed burn.`;
}
/** "340 km north of this frame", or `null` where the bounds are not known. */
export function offBoardDirection(
fire: { lat: number; lon: number },
bounds?: { minLat: number; maxLat: number; minLng: number; maxLng: number },
): string | null {
if (bounds === undefined) return null;
const north = fire.lat - bounds.maxLat;
const south = bounds.minLat - fire.lat;
const east = fire.lon - bounds.maxLng;
const west = bounds.minLng - fire.lon;
// Longitude is squashed by the cosine of the latitude before the two are
// compared, or a fire two degrees east of a board at 38 N wins against one two
// degrees north of it while actually being closer.
const squash = Math.cos((fire.lat * Math.PI) / 180);
const options: [string, number][] = [
["north", north * 111.32],
["south", south * 111.32],
["east", east * 111.32 * squash],
["west", west * 111.32 * squash],
];
let best: [string, number] | null = null;
for (const option of options) {
if (option[1] <= 0) continue;
if (best === null || option[1] > best[1]) best = option;
}
if (best === null) return null;
return `${Math.round(best[1])} km ${best[0]} of this frame`;
}
/** The hot-pixel caption. Always says "evidence, not incidents". Always. */
export function evidenceCaption(view: FirePanelView): string {
const count = view.detections?.length ?? 0;
const hours = view.detectionWindowHours ?? 0;
const window = hours > 0 ? `, last ${Math.round(hours)} h` : "";
const persistent = view.persistentDetections ?? 0;
const head =
count === 0
? `No satellite hot pixels on this board${window}.`
: `${count} satellite hot pixel${count === 1 ? "" : "s"}${window} — evidence, not incidents.`;
if (persistent <= 0) return head;
return `${head} ${persistent} known persistent source${persistent === 1 ? "" : "s"} — flare stacks, kilns, landfills — are counted and not drawn.`;
}
// ---- Mount ----------------------------------------------------------------
export function mountFirePanel(
host: HTMLElement,
options: FirePanelOptions = {},
): FirePanelHandle {
const doc = host.ownerDocument;
const agencies = options.agencies ?? DEFAULT_AGENCIES;
const root = doc.createElement("section");
root.className = "tera-fire";
root.setAttribute("aria-label", "Fire");
root.setAttribute("data-state", "fault");
// One stylesheet per document, not per panel: this is torn down and rebuilt on
// every board switch, and a `<style>` per mount accumulates one copy of the
// same rules per city the visitor has looked at.
const head = doc.head ?? null;
let styleAdded = false;
if (head !== null && head.querySelector(`#${STYLE_ID}`) === null) {
const style = doc.createElement("style");
style.id = STYLE_ID;
style.textContent = FIRE_PANEL_CSS;
head.append(style);
styleAdded = true;
}
const headline = doc.createElement("p");
headline.className = "tera-fire__state";
headline.setAttribute("data-role", "headline");
const stamp = doc.createElement("time");
stamp.setAttribute("data-role", "fetched");
stamp.className = "tera-fire__note";
const note = doc.createElement("p");
note.className = "tera-fire__note";
note.setAttribute("data-role", "suppressed");
const list = doc.createElement("ul");
list.className = "tera-fire__list";
list.setAttribute("data-role", "drawn");
const offHeading = doc.createElement("p");
offHeading.className = "tera-fire__section";
offHeading.setAttribute("data-role", "off-board-heading");
const off = doc.createElement("ul");
off.className = "tera-fire__off";
off.setAttribute("data-role", "off-board");
const evidence = doc.createElement("p");
evidence.className = "tera-fire__evidence";
evidence.setAttribute("data-role", "evidence");
root.append(headline, stamp, note, list, offHeading, off, evidence);
host.append(root);
let disposed = false;
function apply(view: FirePanelView | null): void {
if (disposed) return;
if (view === null) {
root.setAttribute("data-state", "fault");
headline.setAttribute("data-state", "fault");
headline.textContent = "No fire feed configured — this board has never heard from one.";
stamp.textContent = "";
stamp.removeAttribute("datetime");
note.textContent = "";
list.replaceChildren();
offHeading.textContent = "";
off.replaceChildren();
evidence.textContent = "";
return;
}
const head2 = fireHeadline(view, agencies);
root.setAttribute("data-state", head2.state);
headline.setAttribute("data-state", head2.state);
headline.textContent = head2.text;
// The machine-readable instant sits beside the human one on purpose. "4
// minutes ago" is what a reader wants; the ISO string is what a screenshot,
// a bug report and a test can all agree on afterwards.
if (view.ageMs === null) {
stamp.textContent = "";
stamp.removeAttribute("datetime");
} else {
stamp.setAttribute("datetime", view.fetchedAt);
stamp.textContent = `Fetched ${view.fetchedAt} · ${relativeAge(view.ageMs)}`;
}
const suppressed = suppressedNote(view);
note.textContent = suppressed ?? "";
const fires: HTMLElement[] = [];
for (const fire of view.drawn) {
const item = doc.createElement("li");
item.className = "tera-fire__fire";
item.setAttribute("data-fire", fire.id);
if (fire.tier !== undefined) item.setAttribute("data-tier", String(fire.tier));
const name = doc.createElement("span");
name.className = "tera-fire__name";
// An unnamed fire is printed as unnamed. Defaulting to the id would turn
// an agency's silence into a name, and `{6AD80DE9-…}` is not one.
name.textContent = fire.name ?? "Unnamed incident";
const facts = doc.createElement("span");
facts.className = "tera-fire__facts";
facts.textContent = factsFor(fire);
item.append(name, facts);
fires.push(item);
}
list.replaceChildren(...fires);
const offBoard = view.offBoard ?? [];
if (offBoard.length === 0) {
offHeading.textContent = "";
off.replaceChildren();
} else {
offHeading.textContent = "Largest gated fires outside this frame";
const rows: HTMLElement[] = [];
for (const fire of offBoard) {
const row = doc.createElement("li");
row.setAttribute("data-fire", fire.id);
const where = offBoardDirection(fire, options.bounds);
const parts = [
fire.name ?? "Unnamed incident",
`${formatAcres(fire.acres)} acres`,
where ?? "outside this frame",
];
row.textContent = `${parts.join(" — ")}. Listed, not drawn.`;
rows.push(row);
}
off.replaceChildren(...rows);
}
evidence.textContent = evidenceCaption(view);
}
apply(null);
return {
root,
apply,
dispose() {
disposed = true;
root.remove();
if (styleAdded) doc.head?.querySelector(`#${STYLE_ID}`)?.remove();
},
};
}
/** "7,591 acres · 29% contained", or the honest gap where an agency is silent. */
function factsFor(fire: FirePanelFire): string {
const parts = [`${formatAcres(fire.acres)} acres`];
// `null` containment means "the agency has not said", which is not zero, and
// printing "0% contained" would put a claim in the reader's head that nobody
// upstream made.
parts.push(
fire.pctContained === null || fire.pctContained === undefined
? "containment not reported"
: `${Math.round(fire.pctContained)}% contained`,
);
if (fire.county !== null && fire.county !== undefined && fire.county !== "") {
parts.push(fire.county);
}
return parts.join(" · ");
}