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/server/src/fires/cloud1.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

230 lines
8.8 KiB
TypeScript

/**
* The fire feed's one upstream: a **projection**, served by the machine that
* owns the database.
*
* ### Read this before changing anything here
*
* The obvious way to build this feed was to copy `fires.sqlite` onto this box
* and query it locally. That was rejected, and the reason is not performance and
* not dependency count — though it is also both of those.
*
* The store is centred on a private home. `observations` carries `distance_km`,
* `bearing_deg` and `threat`, and `detections` carries `distance_km`, all four
* measured from that address. `threat` is the one that was nearly missed: it is
*
* (16 / distance_to_house)^2 x log10(acres) x momentum x containment
* x wind-alignment-to-house
*
* and acreage and containment are already public — they come from CAL FIRE. So
* that expression **inverts**: one fire gives a circle around the house, three
* give an intersection. A copy of that database on cloud-2, which is public
* facing, is a home address sitting on disk waiting for one careless star-query
* in a route somebody writes next year.
*
* So this module has **no database**. It makes two bounded GETs against an
* endpoint whose SELECT lists are written out by hand on the other machine, and
* it cannot be careless with columns it was never sent. That ordering is the
* security property: the leak is made impossible rather than merely avoided.
*
* It is also why there is no sqlite driver in this repo's dependency list, and
* `scripts/check-dependency-licenses.mjs` is entitled to keep it that way.
*
* ### Both halves, or neither
*
* `fetchProjection` asks for incidents and detections in parallel and returns
* `null` if **either** fails. That is deliberate and it is the opposite of what
* a partial-tolerance instinct suggests. A body carrying live incidents and a
* silently empty detection array is a board that has quietly stopped showing the
* strongest evidence it has, with nothing to say so — the same class of failure
* as a cron that reports success while every snapshot inside it fails. One
* snapshot, one answer; `upstream.ts` above this keeps serving the last good
* whole body until a whole one arrives.
*/
import { getJson } from "../http.ts";
import type { FireDetection, FireIncident } from "../../../src/server/wire.ts";
/** How long either GET may take. The upstream reads sqlite behind its own cache. */
const TIMEOUT_MS = 8_000;
/**
* The most rows this build will adopt from either endpoint.
*
* The upstream caps its own SQL, so this is the second of two bounds rather than
* the only one — but a caller that trusts an upstream's cap is a caller that
* inherits the day the upstream's cap changes. `flights/adsb.ts` takes the same
* belt-and-braces position for the same reason: `http.ts` bounds the *bytes*,
* and this bounds what is kept and served on.
*/
const MAX_INCIDENTS = 500;
const MAX_DETECTIONS = 2_000;
export interface FiresSnapshot {
/** Epoch ms at which this box completed the fetch. */
fetchedAt: number;
/** The upstream de-duplication watermark. Every incident row matched it. */
latestSeen: string | null;
incidents: FireIncident[];
detections: FireDetection[];
detectionWindowHours: number;
attribution: string[];
}
export interface FiresLog {
warn(msg: string): void;
}
/** What the projection endpoint answers with. Restated, never imported. */
interface IncidentsResponse {
latestSeen?: unknown;
incidents?: unknown;
attribution?: unknown;
}
interface DetectionsResponse {
windowHours?: unknown;
detections?: unknown;
attribution?: unknown;
}
export async function fetchProjection(
base: string,
key: string,
windowHours: number,
log: FiresLog,
): Promise<FiresSnapshot | null> {
if (base === "") return null;
const headers = key === "" ? undefined : { "x-tera-key": key };
const options = { timeoutMs: TIMEOUT_MS, ...(headers === undefined ? {} : { headers }) };
const [incidentsBody, detectionsBody] = await Promise.all([
getJson<IncidentsResponse>(`${base}/incidents`, options),
getJson<DetectionsResponse>(
`${base}/detections?hours=${encodeURIComponent(String(Math.round(windowHours)))}`,
options,
),
]);
// Both or neither. See the header — a live incident set beside a silently
// empty detection array is a board that has stopped saying what it knows.
if (incidentsBody === null || detectionsBody === null) {
log.warn(
"fires:cloud1: the projection did not answer with both halves " +
`(incidents ${incidentsBody === null ? "failed" : "ok"}, ` +
`detections ${detectionsBody === null ? "failed" : "ok"}); keeping the last whole body`,
);
return null;
}
const incidents = readArray(incidentsBody.incidents, MAX_INCIDENTS, readIncident);
const detections = readArray(detectionsBody.detections, MAX_DETECTIONS, readDetection);
return {
fetchedAt: Date.now(),
latestSeen: nonEmptyString(incidentsBody.latestSeen),
incidents,
detections,
detectionWindowHours: finite(detectionsBody.windowHours) ?? Math.round(windowHours),
attribution: mergeAttribution(incidentsBody.attribution, detectionsBody.attribution),
};
}
// ---- Reading somebody else's JSON -----------------------------------------
//
// Field by field, checked rather than cast. The upstream is a machine on the
// same tailnet run by the same person, which is exactly the relationship that
// produces "it will always be the right shape" — and then a collector gains a
// column and a renderer draws a fire at 0,0 in the Gulf of Guinea. Nothing here
// throws: a row that will not read is dropped, and the fetch above still returns
// a body.
function readIncident(raw: unknown): FireIncident | null {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
const row = raw as Record<string, unknown>;
const lat = finite(row.lat);
const lon = finite(row.lon);
const id = nonEmptyString(row.id);
const lastSeen = nonEmptyString(row.lastSeen);
// No id, no coordinate or no watermark means nothing downstream can place it,
// de-duplicate it or link to it. All three are structural, not cosmetic.
if (id === null || lat === null || lon === null || lastSeen === null) return null;
return {
id,
source: nonEmptyString(row.source) ?? "",
name: nonEmptyString(row.name),
lat,
lon,
provenance: nonEmptyString(row.provenance) ?? "us-gov",
county: nonEmptyString(row.county),
type: (nonEmptyString(row.type) ?? "").trim(),
url: nonEmptyString(row.url),
firstSeen: nonEmptyString(row.firstSeen) ?? lastSeen,
lastSeen,
observedAt: nonEmptyString(row.observedAt),
acres: finite(row.acres),
pctContained: finite(row.pctContained),
};
}
function readDetection(raw: unknown): FireDetection | null {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
const row = raw as Record<string, unknown>;
const lat = finite(row.lat);
const lon = finite(row.lon);
if (lat === null || lon === null) return null;
const persistentDays = finite(row.persistentDays) ?? 0;
return {
sat: nonEmptyString(row.sat) ?? "",
acquiredAt: nonEmptyString(row.acquiredAt) ?? "",
lat,
lon,
frp: finite(row.frp),
// Carried verbatim. MODIS puts an integer 0-100 in this column and VIIRS
// puts low/nominal/high; normalising here would mean choosing one of them to
// be wrong. `detectionConfidence()` in `src/server/fires.ts` does the branch
// once, on the client, where `sat` is beside it.
confidence: nonEmptyString(row.confidence),
// Absent means not persistent, which is the direction that draws MORE rather
// than fewer — so a projection one version behind this one shows the
// industrial flare as a weak hot pixel rather than hiding a real fire.
persistent: row.persistent === true,
persistentDays,
};
}
function readArray<T>(raw: unknown, cap: number, read: (row: unknown) => T | null): T[] {
if (!Array.isArray(raw)) return [];
const out: T[] = [];
for (const row of raw) {
if (out.length >= cap) break;
const parsed = read(row);
if (parsed !== null) out.push(parsed);
}
return out;
}
/** Both credit lines, de-duplicated, in the order they arrived. */
function mergeAttribution(a: unknown, b: unknown): string[] {
const out: string[] = [];
for (const source of [a, b]) {
if (!Array.isArray(source)) continue;
for (const line of source) {
if (typeof line !== "string" || line === "" || out.includes(line)) continue;
out.push(line);
}
}
return out;
}
function nonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed === "" ? null : trimmed;
}
function finite(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}