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/devicePanel.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

584 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The mic and the monitor speaker, drawn as instruments rather than as a table
* of key/value pairs.
*
* There was no device concept anywhere in this product a week ago — no type, no
* route, no state — so this panel is not a rewrite of anything and has no legacy
* to preserve. What it does have is a rule it must not break, and it is the same
* rule the media-surface panel and `RobotOperationsDefinition` already live
* under: **a reading that was invented has to say so, on the same surface as the
* reading.** A level meter bouncing next to a photograph of a desk is a claim
* about a real room. `DeviceDeclaration.disclosure` is the sentence that makes it
* an honest one, `validateDeviceDeclaration` refuses a `simulated` device whose
* disclosure does not contain the word, and this panel prints it under every
* instrument rather than once at the top — because a panel that scrolls is a
* panel whose header you have already scrolled past.
*
* ### One control per declared capability, and never a `switch` on kind
*
* The controls are built by walking `declaration.capabilities` and looking each
* one up in `CAPABILITY_READING`. Nothing here asks whether a device is a mic.
* That is the whole design of `src/devices/types.ts` and it is what makes a
* third kind — a smart light, a thermostat — data rather than a code change: the
* capability is declared, the reading is named by the table, and the control
* falls out. A `if (kind === "mic") renderGain()` ladder would have to be found
* and edited in four consumers the first time somebody added one.
*
* `level` is the odd one and is deliberately not a control: it is a *reading*.
* You can ask a microphone for its programme level; you cannot set it.
* `DeviceCommandOp` is `Exclude<DeviceCapability, "level">` for exactly this
* reason, so the type system agrees with the panel about which rows are knobs.
*
* ### Anonymous visitors get the panel
*
* Declarations are authored into the office pack, which is bundled into the
* static build, which means they are public by construction — the hardware in
* the room is a description of the room. What an account buys is the live
* readings from the authenticated route. So this mounts for everyone and shows
* the at-rest state from `initialDeviceState` until something better arrives,
* and the disclosure line is what tells you which of the two you are looking at.
*/
import {
CAPABILITY_READING,
DEVICE_RANGES,
deviceRange,
initialDeviceState,
normalizeDeviceCommand,
} from "../devices/types.ts";
import type {
DeviceCapability,
DeviceCommand,
DeviceDeclaration,
DeviceRange,
DeviceState,
} from "../devices/types.ts";
import { TOUCH_TARGET_PX } from "./tokens.ts";
// ---- Options and handle ---------------------------------------------------
export interface DevicePanelOptions {
/**
* The authored hardware in this room, from the resolved `Plan`.
*
* Required, and this is the one addition to the signature the build spec
* fixed: the panel's structure is a function of the declarations and there is
* no honest way to render a control without one — the capability list, the
* label, the ranges and the disclosure all come from here. States alone would
* leave the panel guessing which readings a device is *supposed* to have,
* which is precisely the distinction `undefined` is reserved for.
*/
declarations: readonly DeviceDeclaration[];
/**
* Send one instruction. Already validated and clamped by
* `normalizeDeviceCommand` before it arrives; a refusal is a silent no-op here
* and never reaches this callback.
*/
onCommand(command: DeviceCommand): void;
/**
* False while nothing is connected — an anonymous visitor, or a build with no
* server. The controls stay visible and stay pressable, because a disabled
* mixing desk teaches nothing; what changes is the line under them.
*/
live?: boolean;
}
export interface DevicePanelHandle {
root: HTMLElement;
apply(states: readonly DeviceState[]): void;
dispose(): void;
}
// ---- Style ----------------------------------------------------------------
/**
* Exported so the stylesheet test can assert the two properties that are easy to
* lose and impossible to see in a Node test: that every interactive row clears
* the 44px touch target, and that nothing in here writes a raw `z-index`.
*/
export const DEVICE_PANEL_CSS = `
.tera-devices { display: flex; flex-direction: column; gap: var(--s2, 8px); }
.tera-device {
display: flex;
flex-direction: column;
gap: var(--s2, 8px);
padding: var(--s3, 12px);
border: 1px solid var(--hairline, rgba(255,255,255,.11));
border-radius: var(--r-sm, 5px);
background: rgba(255, 255, 255, 0.03);
}
.tera-device__head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s2, 8px); }
.tera-device__name { font-size: 11px; letter-spacing: .04em; color: var(--ink, rgba(255,255,255,.78)); }
.tera-device__kind { font-size: 9px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3, rgba(255,255,255,.4)); }
.tera-device__row {
display: grid;
grid-template-columns: 4.5rem minmax(0, 1fr) 3.25rem;
align-items: center;
gap: var(--s2, 8px);
min-height: ${TOUCH_TARGET_PX}px;
}
.tera-device__row[data-unavailable="true"] { display: none; }
.tera-device__label { font-size: 9px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3, rgba(255,255,255,.4)); }
.tera-device__value { font-size: 10px; text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-2, rgba(255,255,255,.56)); }
.tera-device__switch {
min-height: ${TOUCH_TARGET_PX}px;
min-width: ${TOUCH_TARGET_PX}px;
padding: 0 var(--s3, 12px);
font: inherit;
font-size: 10px;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
color: var(--ink-2, rgba(255,255,255,.56));
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--hairline, rgba(255,255,255,.11));
border-radius: var(--r-pill, 999px);
}
.tera-device__switch[aria-pressed="true"] {
color: var(--amber-ink, #ffd68a);
background: rgba(242, 177, 52, 0.2);
border-color: rgba(242, 177, 52, 0.5);
}
.tera-device__slider { width: 100%; min-height: ${TOUCH_TARGET_PX}px; accent-color: var(--amber, #f2b134); }
.tera-device__meter {
position: relative;
height: 6px;
border-radius: 3px;
overflow: hidden;
background: rgba(255, 255, 255, 0.08);
}
.tera-device__meter i {
display: block;
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, #6fd58f 0%, #f2b134 78%, #ff8f6b 100%);
transition: width 120ms linear;
}
.tera-device__status {
margin: 0;
font-size: 9px;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--ink-3, rgba(255,255,255,.4));
}
.tera-device__status[data-reachable="false"] { color: var(--ink-2, rgba(255,255,255,.56)); }
.tera-device__disclosure {
margin: 0;
font-size: 9px;
line-height: 1.6;
color: var(--ink-3, rgba(255,255,255,.4));
border-left: 2px solid rgba(242, 177, 52, 0.4);
padding-left: var(--s2, 8px);
}
@media (prefers-reduced-motion: reduce) { .tera-device__meter i { transition: none; } }
`;
const STYLE_ID = "tera-device-panel-style";
// ---- Rows ----------------------------------------------------------------
/** How each capability presents itself. `level` is a meter because it is a reading. */
type RowShape = "switch" | "slider" | "meter";
const ROW_SHAPE: Readonly<Record<DeviceCapability, RowShape>> = {
power: "switch",
mute: "switch",
playback: "switch",
gain: "slider",
volume: "slider",
level: "meter",
};
const ROW_LABEL: Readonly<Record<DeviceCapability, string>> = {
power: "Power",
mute: "Mute",
playback: "Play",
gain: "Gain",
volume: "Volume",
level: "Level",
};
/**
* The step a slider moves in, derived from the range rather than tabled against
* the capability.
*
* It used to be a constant per capability — 1 for gain, 0.01 for volume — which
* was right only while every gain in the world was the default 12…+36 dB. A
* Yeti Nano declares 0100 "%" and a step of 1 is still right there; a
* hypothetical 01 gain would need 0.01. So the rule is about the span: a wide
* range moves in whole units, a narrow one in hundredths.
*/
function sliderStep(range: DeviceRange): number {
return range.max - range.min > 5 ? 1 : (range.max - range.min) / 100;
}
interface Row {
capability: DeviceCapability;
/** This device's own bounds and unit, or `null` for a switch. */
range: DeviceRange | null;
/**
* How this row presents itself, carried rather than re-derived.
*
* Deliberately not an `instanceof HTMLButtonElement` check at apply time:
* `HTMLButtonElement` is not a global in Node, so an `instanceof` against it
* throws a `ReferenceError` rather than returning false — which would make
* this module untestable outside a browser for no benefit at all.
*/
shape: RowShape;
root: HTMLElement;
control: HTMLElement;
/** The slider itself, for the two rows that have one. */
input: HTMLInputElement | null;
value: HTMLElement;
meterFill: HTMLElement | null;
}
interface Instrument {
declaration: DeviceDeclaration;
rows: readonly Row[];
/**
* The one line per instrument that says which of the two sentences the viewer
* is reading, and whether anybody answered the door.
*/
status: HTMLElement;
disclosure: HTMLElement;
}
/**
* One reading, in the unit the *declaration* says it is in.
*
* The unit comes from the range and never from the capability, and that is the
* whole reason `DeviceDeclaration.ranges` exists. A Blue Yeti Nano's capture
* level is an ALSA position on a 050 scale; there is no arithmetic that turns
* 68% of it into decibels, and printing "+20.6 dB" beside it would present a
* guess in the typography of a measurement. The field on `DeviceState` is still
* called `gainDb` — renaming it would break four consumers to fix a label — so
* this is the one place that decides what the number is called on screen, and it
* asks the declaration.
*/
function formatReading(
capability: DeviceCapability,
reading: unknown,
range: DeviceRange | null,
): string {
if (reading === undefined || reading === null) return "—";
if (typeof reading === "boolean") {
if (capability === "power") return reading ? "On" : "Off";
if (capability === "mute") return reading ? "Muted" : "Open";
return reading ? "Playing" : "Stopped";
}
if (typeof reading !== "number" || !Number.isFinite(reading)) return "—";
const unit = range?.unit ?? "";
// A fraction is the one unit nobody wants to read as a fraction.
if (unit === "fraction") return `${Math.round(reading * 100)}%`;
if (unit === "%") return `${Math.round(reading)}%`;
if (unit === "dB") return `${reading >= 0 ? "+" : ""}${reading.toFixed(0)} dB`;
if (unit === "dBFS") return `${reading.toFixed(0)} dBFS`;
if (unit === "") return String(reading);
return `${reading.toFixed(reading % 1 === 0 ? 0 : 2)} ${unit}`;
}
/**
* A level as a fraction of the meter's travel. On the default range, `-60` dBFS
* is empty and `0` is full.
*
* `range` is optional and defaults to the global one so that every existing
* caller — and every test that already asserts on this — is unchanged. A device
* that declared its own level range gets its own travel.
*/
export function meterFraction(levelDb: number | undefined, range?: DeviceRange): number {
if (levelDb === undefined || !Number.isFinite(levelDb)) return 0;
const { min, max } = range ?? DEVICE_RANGES.level;
if (!(max > min)) return 0;
return Math.min(1, Math.max(0, (levelDb - min) / (max - min)));
}
/**
* How old a reading is, in the coarsest unit that is still true.
*
* Deliberately vague past an hour. A panel that said "not reached — last reading
* 3h 41m ago" would be inviting a precision the underlying clock does not have:
* `observedAt` is the bridge's own `checkedAt`, restamped through two caches.
*/
function sinceLabel(observedAt: number | undefined, nowMs: number = Date.now()): string {
if (typeof observedAt !== "number" || !Number.isFinite(observedAt) || observedAt <= 0) {
return "at an unknown time";
}
const seconds = Math.max(0, Math.round((nowMs - observedAt) / 1000));
if (seconds < 90) return `${seconds}s ago`;
const minutes = Math.round(seconds / 60);
if (minutes < 90) return `${minutes} min ago`;
const hours = Math.round(minutes / 60);
return hours < 36 ? `${hours}h ago` : "over a day ago";
}
// ---- Mount ----------------------------------------------------------------
export function mountDevicePanel(
host: HTMLElement,
options: DevicePanelOptions,
): DevicePanelHandle {
const doc = host.ownerDocument;
const root = doc.createElement("section");
root.className = "tera-devices";
root.setAttribute("aria-label", "Studio hardware");
// One stylesheet per document, not per panel: this panel is torn down and
// rebuilt on every office switch, and a `<style>` per mount would accumulate
// one copy of the same rules per building the visitor walked through.
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 = DEVICE_PANEL_CSS;
head.append(style);
styleAdded = true;
}
const instruments: Instrument[] = [];
let disposed = false;
/** Last known state per device, so a control can compute its own toggle. */
const latest = new Map<string, DeviceState>();
function send(declaration: DeviceDeclaration, command: DeviceCommand): void {
if (disposed) return;
// Validated here as well as on the route, and that is not belt-and-braces
// duplication: this is the copy that stops the UI *sending* something the
// route would refuse, so a slider that overshoots its range by a float
// rounding error is clamped rather than round-tripped into a 400.
const normalized = normalizeDeviceCommand(declaration, command);
if (normalized === null) return;
options.onCommand(normalized);
}
for (const declaration of options.declarations) {
const card = doc.createElement("article");
card.className = "tera-device";
card.setAttribute("data-device", declaration.id);
card.setAttribute("data-kind", declaration.kind);
const head2 = doc.createElement("div");
head2.className = "tera-device__head";
const name = doc.createElement("span");
name.className = "tera-device__name";
name.textContent = declaration.label;
const kind = doc.createElement("span");
kind.className = "tera-device__kind";
kind.textContent = declaration.kind;
head2.append(name, kind);
card.append(head2);
const rows: Row[] = [];
for (const capability of declaration.capabilities) {
const shape = ROW_SHAPE[capability];
if (shape === undefined) continue;
const row = doc.createElement("div");
row.className = "tera-device__row";
row.setAttribute("data-capability", capability);
const label = doc.createElement("span");
label.className = "tera-device__label";
label.textContent = ROW_LABEL[capability];
const value = doc.createElement("span");
value.className = "tera-device__value";
value.textContent = "—";
let control: HTMLElement;
let input: HTMLInputElement | null = null;
let meterFill: HTMLElement | null = null;
let range: DeviceRange | null = null;
if (shape === "switch") {
const button = doc.createElement("button");
button.type = "button";
button.className = "tera-device__switch";
button.setAttribute("aria-pressed", "false");
button.textContent = ROW_LABEL[capability];
button.addEventListener("click", () => {
const current = latest.get(declaration.id);
const reading = current?.[CAPABILITY_READING[capability]];
// The command is the *negation of what is on screen*, not a blind
// `true`: a toggle that always sends `true` is a button that works
// once. `undefined` — no reading yet — is treated as off, which is
// what `initialDeviceState` says a device at rest is.
send(declaration, {
deviceId: declaration.id,
op: capability === "power" ? "power" : capability === "mute" ? "mute" : "playback",
value: !(reading === true),
});
});
control = button;
} else if (shape === "slider") {
// The device's own bounds, not the global defaults. See `deviceRange`.
range = deviceRange(declaration, capability === "gain" ? "gain" : "volume");
const step = sliderStep(range);
const slider = doc.createElement("input");
slider.type = "range";
slider.className = "tera-device__slider";
slider.min = String(range.min);
slider.max = String(range.max);
slider.step = String(step);
slider.value = String(range.initial);
slider.setAttribute("aria-label", `${declaration.label} ${ROW_LABEL[capability]}`);
slider.addEventListener("input", () => {
const parsed = Number(slider.value);
if (!Number.isFinite(parsed)) return;
send(declaration, {
deviceId: declaration.id,
op: capability === "gain" ? "gain" : "volume",
value: parsed,
});
});
control = slider;
input = slider;
} else {
range = deviceRange(declaration, "level");
const meter = doc.createElement("div");
meter.className = "tera-device__meter";
meter.setAttribute("role", "meter");
meter.setAttribute("aria-valuemin", String(range.min));
meter.setAttribute("aria-valuemax", String(range.max));
const fill = doc.createElement("i");
fill.style.width = "0%";
meter.append(fill);
meterFill = fill;
control = meter;
}
row.append(label, control, value);
card.append(row);
rows.push({ capability, range, shape, root: row, control, input, value, meterFill });
}
// Per instrument, not once per panel. A disclosure at the top of a scrolling
// list is a disclosure you have already scrolled past by the time you are
// looking at the meter that needed it.
const status = doc.createElement("p");
status.className = "tera-device__status";
status.textContent = "";
const disclosure = doc.createElement("p");
disclosure.className = "tera-device__disclosure";
disclosure.textContent = declaration.disclosure;
card.append(status, disclosure);
root.append(card);
instruments.push({ declaration, rows, status, disclosure });
}
host.append(root);
function apply(states: readonly DeviceState[]): void {
if (disposed) return;
const byId = new Map(states.map((state) => [state.id, state]));
latest.clear();
for (const state of states) latest.set(state.id, state);
for (const instrument of instruments) {
const state = byId.get(instrument.declaration.id) ?? null;
for (const row of instrument.rows) {
const reading = state === null ? undefined : state[CAPABILITY_READING[row.capability]];
// `undefined` is "this device has no such reading", which is a different
// statement from zero and gets a different treatment: the row goes away
// rather than sitting there reporting a confident 0 dB.
row.root.setAttribute("data-unavailable", String(reading === undefined));
row.value.textContent = formatReading(row.capability, reading, row.range);
if (row.shape === "switch") {
row.control.setAttribute("aria-pressed", String(reading === true));
} else if (row.shape === "slider") {
if (row.input !== null && typeof reading === "number" && Number.isFinite(reading)) {
row.input.value = String(reading);
}
} else if (row.meterFill !== null) {
const range = row.range ?? DEVICE_RANGES.level;
const fraction = meterFraction(
typeof reading === "number" ? reading : undefined,
range,
);
row.meterFill.style.width = `${(fraction * 100).toFixed(1)}%`;
row.control.setAttribute(
"aria-valuenow",
typeof reading === "number" ? reading.toFixed(1) : String(range.min),
);
}
}
applyProvenance(instrument, state);
}
}
/**
* The two sentences under one instrument, and which of them is true right now.
*
* There are three facts a viewer needs and only one of them is a reading:
*
* - **Which disclosure applies.** A `first-party-sensor` declaration's own
* `disclosure` says the hardware is live. An anonymous visitor is handed the
* *local simulator* running that same declaration — see
* `devices/adapter.ts` — and printing "live, LA Studio, north wall" over a
* number invented a millisecond ago in their own browser is the exact
* provenance confusion `DeviceProvenance` exists to prevent, arriving
* through the honest path. So when a state says `synthetic` and the
* declaration does not, `simulatedDisclosure` is what is shown.
* - **Whether anybody answered.** `reachable: false` means the readings below
* still stand and nobody has been able to confirm them since. It is
* emphatically not `powered: false`: a microphone on a machine that is
* asleep is not a switched-off microphone. There is deliberately no fifth
* indicator colour in the 3D scene for it — a new colour would have to be
* learned, and would be learned wrong. The sentence lives here, where there
* is room for a sentence.
* - **How old the reading is**, once it has stopped being confirmed.
*/
function applyProvenance(instrument: Instrument, state: DeviceState | null): void {
const { declaration } = instrument;
const invented = state === null ? true : state.synthetic;
const simulatedSentence = declaration.simulatedDisclosure;
instrument.disclosure.textContent =
invented && declaration.provenance !== "simulated" && typeof simulatedSentence === "string"
? simulatedSentence
: declaration.disclosure;
// Absent means the concept does not apply to this source, which is every
// simulated device — a state machine in this tab is never unreachable, and a
// line claiming it is reachable would be a fact about nothing.
const reachable = state?.reachable;
if (reachable === undefined) {
instrument.status.removeAttribute("data-reachable");
instrument.status.textContent = "";
return;
}
instrument.status.setAttribute("data-reachable", String(reachable));
instrument.status.textContent = reachable
? "Reached"
: `Not reached — last reading ${sinceLabel(state?.observedAt)}`;
}
/**
* Everything starts at rest rather than blank.
*
* `initialDeviceState` is a pure function of the declaration and is the same
* one the simulator and the route start from, so the pre-connection panel is
* not an invented placeholder — it is the honest answer to "what does this
* device report when nobody has observed it": powered off, gain at its resting
* value, meter at the floor, and `synthetic: true` whatever the declaration's
* provenance claims. A panel of dashes would teach nothing about the
* instrument; this teaches the whole of it before a single reading arrives.
*/
apply(options.declarations.map((declaration) => initialDeviceState(declaration, 0)));
return {
root,
apply,
dispose() {
if (disposed) return;
disposed = true;
root.remove();
latest.clear();
if (styleAdded) doc.querySelector(`#${STYLE_ID}`)?.remove();
},
};
}