release: v0.1.0 — one-click install, keyless boot, in-app Provider Settings (#142)

The first formal release.

- One-click install via Pinokio; keyless boot lands on a live Esri World
  Imagery satellite globe with keyless terrain, with automatic OSM fallback
  and graceful degradation when terrain is unavailable.
- Provider Settings (the POWER UP panel): add, replace, or remove API keys
  inside the app; credential files made owner-only before any secret is
  written; external keys shown read-only; the panel refuses shared or proxied
  servers.
- Keyless capability responses for the optional HUD summary and place search.
- Aircraft-identity voice answers cover operator, type, and route, and say so
  plainly when enrichment is unavailable.
- README rewritten keyless-first; concise 0.1.0 changelog section; CI
  workflow included.

Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression
108/108, map-source tray QA pass, setup doctor ready.
This commit is contained in:
Bilawal Sidhu authored and GitHub committed 2026-08-31 17:49:45 -05:00
1 parent 6d83bb6008
commit b6da93b8ac
170 files changed
+5800 -853

No files matched your search

+2 -2
View File
@@ -163,7 +163,7 @@ export function createAnnotationEngine({
* reach — and the next annotate of the same geometry would then stack a fresh
* mark over the orphan. remove() tolerates partial and absent state, so this
* is safe to call unconditionally; it must never mask the original failure.
* (second review)
* (review round 2)
* @param {object} anno
* @returns {void}
*/
@@ -1027,7 +1027,7 @@ function pendingAnimation(anno, now) {
// Region-scale viewports (a mountain range, sea, or desert can span thousands of km)
// must not launch the assist flight to space: frameAnnotation flies at range × 2.4,
// so this cap keeps the camera at ≈290 km — the same regional swath scale the
// fly_to_location natural-region heuristic uses (field test 2026-07-23).
// fly_to_location natural-region heuristic uses (owner field test 2026-07-23).
const VIEWPORT_ASSIST_RANGE_CAP_M = 120000;
/** flyTo range from a Places viewport box (low/high lat-lng corners), or null. */
+1 -1
View File
@@ -575,7 +575,7 @@ test('fresh path: a renderer throw is rolled back the same way', async (t) => {
);
});
// ── Rollback must also unwind PARTIAL renderer state (second review) ──────────
// ── Rollback must also unwind PARTIAL renderer state (review round 2) ──────────
//
// The harness above throws on the FIRST statement of add(), so a rollback that
// only deletes the engine's map entry looked complete. The real renderers build
+6 -6
View File
@@ -270,7 +270,7 @@ export async function resolveAnnotationTarget({
const baseScope = refineScope(scopeFromTypes(geocodeTypes), entityKind);
// Point-like targets (monuments/statues/memorials/…) resolve POINT-FIRST: only an
// (almost) exactly-named, monument-scale polygon may replace the point; a nearby polygon
// sharing locality words must not.
// sharing locality words must not (docs/field-test-rootcause-2026-06-30.md §1).
const pointLike = isPointLikeTarget(target, entityKind, placeTypes, labelHint);
// Grounds/compound asks (target OR label wording, or entityKind fact) go outline-first:
// they reach the real enclosing-polygon sweep even under `around_the_thing` phrasing.
@@ -306,7 +306,7 @@ export async function resolveAnnotationTarget({
// BYPASSES the compound/building scope caps and the centroid drift bound
// below: the region IS the asked scope ("outline the Alps"), the 60 km²
// compound cap is for campuses (the meadow bug,
// the resolver's verified live-data contract), and a continental ring's
// docs/voice-engine-evaluation-2026-07-23.md §3), and a continental ring's
// centroid legitimately sits far from any anchor. Admin and street scopes
// are excluded — "Texas" must keep resolving as an admin boundary.
if (!isAdmin && scope !== 'street' && !around) {
@@ -343,7 +343,7 @@ export async function resolveAnnotationTarget({
// FIRST: a bundled neighborhood polygon (reliable, deterministic, OFFLINE — no live
// Overpass). Covered neighborhoods (e.g. SF: Chinatown/Marina/Mission/Presidio)
// resolve here instantly to a REAL boundary, sidestepping the slow/flaky live-Overpass
// path that times out and falls back to points.
// path that times out and falls back to points (see docs/field-test-2-analysis.md).
const ext = await lookupNeighborhoodRing(lat, lon, matchName);
if (ext) fp = { ring: ext.ring, kind: 'area', heightM: null };
// Else fall through to the OSM admin/place → named-landuse → synthesis ladder. Each
@@ -400,7 +400,7 @@ export async function resolveAnnotationTarget({
// primary footprint above returns the BUILDING (the dome) or null — neither is the grounds. The
// real enclosing polygon (e.g. "Capitol Square", leisure=park) IS in OSM but only surfaces via a
// radius sweep for NAMED non-building polygons, taking the SMALLEST that geometrically contains
// the point. So when a grounds-like
// the point (research docs/compound-containment-research.md §1.4–1.5). So when a grounds-like
// query produced a building or no polygon, prefer that REAL enclosing outline; fall to a
// synthesized disc only when OSM DEFINITIVELY has none.
if (groundsLike && (fp === null || fp?.kind === 'building')) {
@@ -522,7 +522,7 @@ const MIN_DRIFT_FLOOR_KM = 50;
// cover a large compound's monuments seen from an oblique view (Text Search is biased to 6 km here).
const PLACES_MAX_DISTANCE_M = 8000;
// Synthesis radii (m) for cases where OSM has only
// Synthesis radii (m) per osm-place-resolution-research.md §8.5. Used when OSM has only
// a label point (most US neighborhoods) or the user asks for the area AROUND a landmark.
const NEIGHBORHOOD_RADIUS_M = 750; // urban-neighborhood blob (600–900 m band)
const AROUND_LANDMARK_RADIUS_M = 400; // "the area around X" — a few blocks (300–500 m)
@@ -1281,7 +1281,7 @@ const ENCLOSING_RADIUS_M = 600; // sweep this far for an enclosing named non-bui
* leisure=park). OSM has no deterministic "parent polygon" call and the canonical
* compound name is rarely what the user utters ("grounds", not "Capitol Square"),
* so SELECTION is by containment → smallest area, with a name-match only as a
* tiebreak BONUS (never a filter).
* tiebreak BONUS (never a filter). See docs/compound-containment-research.md §1.4–1.5.
*
* Modeled on fetchLocalMonument: a 12 s fail-fast (an enrichment, not worth blocking
* narration — and narration no longer waits on it since outlines went progressive, so
+1 -1
View File
@@ -1,6 +1,6 @@
// Footprint-selection contract tests — pure fixtures, no network, no browser.
//
// Locks the monument-resolution regression:
// Locks the field-test-7 monument fix (docs/field-test-rootcause-2026-06-30.md §1):
// a POINT-LIKE target ("Tejano Monument, Austin") must never adopt a nearby
// polygon that merely shares locality/context words ("Austin", "History").
// The fixtures replicate the REAL Overpass candidates captured over the Texas
@@ -359,7 +359,7 @@ test('hybrid outline upgrade preserves the screen group and adds world geometry'
renderer.destroy();
});
// ── Partial-add rollback (second review) ─────────────────────────────────────
// ── Partial-add rollback (review round 2) ─────────────────────────────────────
//
// The hybrid builds a mark across TWO sub-renderers. It used to record the
// route only after both had run, so a throw in the second one left the first
+2 -2
View File
@@ -6,9 +6,9 @@ import { createHybridAnnotationRenderer } from './hybridAnnotationRenderer.js';
* for manual/dev use via `window.__gevAnnotations`.
*
* This module is the single swap point between annotation rendering strategies.
* The HYBRID renderer uses world-space draping for
* This branch (Direction C) uses the HYBRID renderer: world-space draping for
* footprints + screen-space SVG for callouts/rings/arrows. The engine, resolver,
* and voice tool wiring are shared across rendering strategies.
* and voice tool wiring are identical to the other two branches.
*/
export function initAnnotations({ viewer, tileset = null }) {
// World-space footprint draping; clamped marks can use the photoreal tiles.
@@ -259,7 +259,7 @@ test('annotation fade consumes the actual tracked host paint rectangle after lay
renderer.destroy();
});
// ── Partial-add unwind (second review) ───────────────────────────────────────
// ── Partial-add unwind (review round 2) ───────────────────────────────────────
//
// add() inserts the group and records it, then does more live-document work
// (draw-on wiring, the first projection pass). A throw in that tail used to
+2 -2
View File
@@ -1,6 +1,6 @@
/**
* Camera verbs — the "spy satellite simulator" feel
* documented in `docs/CURRENT-STATE.md`.
* (docs/superpowers/specs/2026-07-23-camera-verbs-fly-route-spec.md).
*
* One motion at a time, driven per clock tick. `once` = bounded eased nudge;
* `continuous` runs until move_camera{stop}, ANY manual camera input on the
@@ -31,7 +31,7 @@ const PITCH_MIN = Cesium.Math.toRadians(-89);
const PITCH_MAX = Cesium.Math.toRadians(-5);
/* ── Route dolly: cinematic tuning ──────────────────────────────────────────
* Every knob an maintainer may want to retune lives in this block. The shaping is
* Every knob an owner may want to retune lives in this block. The shaping is
* built from four independent layers, each of which flattens to nothing on its
* own: a trapezoid speed profile, a banked-turn roll, altitude breathing, and
* a gaze that leads the path. Under prefers-reduced-motion the last three are
+4 -4
View File
@@ -11,13 +11,13 @@ export const GLOBE_EXIT_CLEARANCE_PX = 12;
export const CELESTIAL_PLANE_EPSILON = 0.045;
/** Responsive radial fade band used by every keyhole-aligned text overlay —
* this is the Detection FADE (label/card fading), NOT the scope-mask feather
* in scopeMask.js. 0.07 since the 2026-08-24 final value (was 0.16). */
* in scopeMask.js. 0.07 since the 2026-08-24 owner final lock (was 0.16). */
export const KEYHOLE_LABEL_FEATHER_RATIO = 0.07;
export const KEYHOLE_LABEL_FEATHER_MAX_RATIO = 0.4;
/**
* First-run OUTSIDE opacity for keyhole-aligned world overlays.
*
* 0.01 since 2026-08-24 (final value; 0.03 on 08-23, 0.05 before). Keep in lockstep with
* 0.01 since 2026-08-24 (owner final lock; 0.03 on 08-23, 0.05 before). Keep in lockstep with
* `#detection-opacity-slider`'s markup value AND readout in index.html,
* `_detectionOutsideOpacityPct` in sharelink.js,
* `GLOBAL_POST_DEFAULTS.detectionOutsideOpacityPct` in ui.js, and
@@ -374,7 +374,7 @@ export class CelestialRing {
// Always mark dirty so a long-hidden interval can't serve stale
// sun/moon vectors on return — but only request the repaint frame
// while visible; the visibility-restore request (main.js) picks the
// dirty flag up immediately. (review review finding)
// dirty flag up immediately. (review finding)
this._ephemerisDirty = true;
if (typeof document !== 'undefined' && document.hidden) return;
governorRequestRender('celestial-ephemeris');
@@ -427,7 +427,7 @@ export class CelestialRing {
// rendered frames — under the idle render governor an enable (or the
// clearing disable) must request its frame or the ring never draws at
// all. Camera motion covers every later repaint; the 60 s ephemeris
// timer requests its own. (perf wave 2 fix — field test finding)
// timer requests its own. (perf wave 2 fix — owner playtest finding)
if (this.enabled !== wasEnabled) governorRequestRender('celestial-ring');
this._root.classList.toggle('disabled', !this.enabled);
if (!this.enabled) {
+1 -1
View File
@@ -44,7 +44,7 @@ test('Cockpit has one reset action beside its bottom exit path', () => {
assert.match(
css,
/body\.cockpit-mode #view-switcher \{[\s\S]*?bottom: max\(clamp\(128px, 15vh, 150px\), env\(safe-area-inset-bottom\)\);[\s\S]*?margin-bottom: -95px;/,
'Cockpit exit must retain the accepted bottom-center position',
'Cockpit exit must retain the owner-approved bottom-center position',
);
assert.match(
css,
+1 -1
View File
@@ -253,7 +253,7 @@ test('Contact panel hides only when there is no snapshot at all', () => {
test('Contact panel survives NEXT onto a vessel or installation subject', () => {
// The panel owns the NEXT button. Hiding it because the subject is not the
// tracked aircraft strands the operator with no way back (this was observed on
// tracked aircraft strands the operator with no way back (owner hit this on
// camera: "click next... whole left panel disappears").
for (const subject of [
{ layerId: 'ais-live-vessels', id: '353136000', label: 'MAERSK DETROIT' },
+2 -2
View File
@@ -1,6 +1,6 @@
// src/contactsDetectionPolicy.js — Contacts-scoped detection policy.
//
// Field test 2026-08-18: "when you click on Contacts, detections should just
// Owner playtest 2026-08-18: "when you click on Contacts, detections should just
// turn on, and they should stay on in Cockpit or in third-person tracking inside
// Contacts or inside Cockpit, both… when I leave the Cockpit, detections go off"
// — that last part being the bug.
@@ -88,7 +88,7 @@ export function contactsDetectionExitPlan(restore, current, styleOwnsDetection =
*
* The engine is injected so this is the SAME code the UI runs and the tests
* exercise, driven against the real `src/data/detection.js` in both. Turning on
* applies the TACTICAL PRESET the military styles use — field test: "I want
* applies the TACTICAL PRESET the military styles use — owner playtest: "I want
* that as the default. It should just happen" — rather than restoring whatever
* profile the operator last left detection at.
*
+4 -4
View File
@@ -1,4 +1,4 @@
// Contacts-scoped detection (field test 2026-08-18: "when you click on
// Contacts-scoped detection (owner playtest 2026-08-18: "when you click on
// Contacts, detections should just turn on, and they should stay on in Cockpit
// or in third-person tracking inside Contacts").
//
@@ -87,7 +87,7 @@ test('the snapshot carries every field activation mutates, not just the mode', (
assert.equal(getDetectionMode(), 'OFF');
assert.equal(getDetectionTuning().densityPct, 25, 'deactivation puts the density back');
// The pin the user would notice: the next manual enable returns their profile.
// The pin the owner would feel: the next manual enable returns their profile.
cycleDetectionMode();
assert.equal(getDetectionMode(), 'SPARSE', 'manual enable returns 25%, not the tactical 75%');
assert.equal(getDetectionTuning().densityPct, 25);
@@ -107,7 +107,7 @@ test('a density-only difference is still a restore worth making', () => {
});
test('activating Contacts lands on the tactical preset, not the last profile used', () => {
// Field test: the Contacts default is the military look, and it "should
// Owner playtest: the Contacts default is the military look, and it "should
// just happen" — so a SPARSE session does NOT drag SPARSE into Contacts.
setDetectionTuning({ densityPct: 25 });
startAt('SPARSE');
@@ -159,7 +159,7 @@ test('activating Contacts leaves an already-on profile untouched', () => {
});
test('detection survives cockpit enter and exit inside a Contacts session', () => {
// The observed bug: "when I leave the Cockpit, detections go off".
// The owner's actual bug: "when I leave the Cockpit, detections go off".
// Cockpit is a move WITHIN Contacts and must not touch detection at all, so
// the only transitions here are the Contacts ones — repeated syncs while the
// session stays active.
+1 -1
View File
@@ -271,7 +271,7 @@ test('a lost cross-mode switch says Context is off, and the state agrees', () =>
// The defect this replaced was the LIE, not the OFF: the transition reported
// a bare "did not complete" while the operator's Context was silently gone.
// Text and state are derived from the same verdict so they cannot disagree,
// and the failed layer ids survive (the honesty requirement).
// and the failed layer ids survive (Manjunath's honesty requirement).
const setter = src.slice(
src.indexOf(' async setContextMode(mode, {'),
src.indexOf(' getCockpitState() {'),
+1 -1
View File
@@ -137,7 +137,7 @@ export const CLASS_MODEL_URL = {
bizjet: '/models/airplane.glb', uav: '/models/airplane.glb',
};
/** Real per-class GLBs (2026-08-15 Hangar fleet, selected assets; CC-BY 4.0 —
/** Real per-class GLBs (2026-08-15 Hangar fleet, owner picks; CC-BY 4.0 —
* provenance in public/models/README.md). Every asset is vertex-baked to the
* airplane.glb convention: Y-up, X = length, Z = span, nose −X (so the layers'
* MODEL_HEADING_OFFSET_DEG = 180 applies unchanged), origin at bbox centre,
+1 -1
View File
@@ -18,7 +18,7 @@
* white / cyan-tracked / amber-military, plus .withAlpha fades) keeps working —
* no per-glyph hardcoded colors that would fight the tint.
*
* MIXED-SET UPDATE (2026-08-15, selected asset set): airliner/widebody/
* MIXED-SET UPDATE (2026-08-15, owner Hangar picks): airliner/widebody/
* turboprop/helicopter use the "refined" recognition-chart redraw;
* quadjet/glider use the "bold" chart-symbol redraw; light/fastjet keep the
* original drawings. Raster fidelity doubled (192px source, same 96 coords).
+1 -1
View File
@@ -1,7 +1,7 @@
// src/data/aisLiveVessels.test.mjs
// Focused tests for the AIS feed-status derivation helper (Batch 10, finding H3/AIS)
// and the vessel vertical-datum seam (2026-07-27 datum pass — see
// the vessel datum contract in docs/CURRENT-STATE.md).
// docs/superpowers/specs/2026-07-27-vessel-datum-design.md).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
+1 -1
View File
@@ -167,7 +167,7 @@ test('P0: a pre-disposal close event cannot erase the post-disposal socket', asy
assert.deepEqual(context.adapter.debug().generations, [2],
'the replacement must NOT reuse generation 1');
// The pre-disposal socket's close finally lands — review's exact interleaving.
// The pre-disposal socket's close finally lands — the exact adversarial interleaving.
first.flushClose();
const debug = context.adapter.debug();
+1 -1
View File
@@ -3,7 +3,7 @@
* client-side in the layers ("how many flights over Texas?", "biggest fire
* near LA?", "which ships are headed to Oakland?").
*
* Analyst-query behavior is documented in docs/CURRENT-STATE.md:
* Design (owner-ratified, docs/voice-engine-evaluation-2026-07-23.md §5.3):
* - ENGINE (this module) is pure query logic over plain record arrays; it
* renders nothing. SURFACES (voice narration, panels, detection brackets)
* consume the returned result set — the engine/surface seam is the
+15 -15
View File
@@ -140,7 +140,7 @@ const GEO_PROGRESS_NOTIFY_BATCH_LIMIT = 10;
// Throttle for placeholder repaints — the projection RAF loop must not
// re-fill a 1080p canvas on every frame while a feed image is still loading.
const PLACEHOLDER_REPAINT_MS = 750;
// v1 key is retired dead data (product rule #3, §9.3 — WIPE CLEAN, no
// v1 key is retired dead data (owner decision #3, §9.3 — WIPE CLEAN, no
// legacy import): kept here only as a documented constant so nothing ever
// re-reads it by accident. Exported for the unit suite's "v1 is ignored"
// assertion; there is NO read path for this key anywhere in the module.
@@ -366,7 +366,7 @@ let _cardFetchMode = 'steady';
* 20/28/40 tiers resume when loading completes (see refreshAmbientCards).
*/
const CCTV_AMBIENT_CARD_DRAIN_CAP = 16;
// Global static-frame pacing (field finding 3): the pacer ticks at the burst
// Global static-frame pacing (owner finding 3): the pacer ticks at the burst
// spacing (250 ms) but cardFetchPolicy gates launches — cold fill (selected
// cards still missing their FIRST frame) allows up to 4 in-flight fetches at
// 250 ms spacing; steady state keeps the salvaged Part C gate of at most one
@@ -403,7 +403,7 @@ let _projectionOverlayOwnerId = null;
* thumbnail absent because its monitor plane is the active representation.
*/
let _activeCameraCardEnabled = false;
// Hover-summoned card (follow-up round 2, item B): pointing at a cardless camera
// Hover-summoned card (owner round 2, item B): pointing at a cardless camera
// icon shows its card immediately as a PINNED entry (budget-exempt, top
// draw-pass declutter priority).
/** Min spacing between hover scene.pick calls (event-driven, user gesture). */
@@ -693,7 +693,7 @@ function safeWindowLocalStorage() {
*
* v2 entries carry provenance: `{ values: <7-field offsets>, source: 'manual',
* savedAt: <epoch ms> }`. The v1 key (`CCTV_CALIBRATION_STORAGE_KEY_V1`) is
* NEVER read here — product rule #3 (§9.3): wipe clean, no legacy import.
* NEVER read here — owner decision #3 (§9.3): wipe clean, no legacy import.
*
* @param {{getItem:function}|null} [storage] - Injectable storage (defaults
* to `window.localStorage`); lets the unit suite test this pure of a DOM.
@@ -1186,7 +1186,7 @@ function buildCatalogFromSources(rawSources) {
* tileset is present (OSM fallback) this returns true so ground sampling is
* not permanently blocked.
*
* Task 5 (spec correction, spec §2): a HIDDEN tileset (`show === false`,
* Task 5 (review correction, spec §2): a HIDDEN tileset (`show === false`,
* i.e. a globe stack is active) must NOT report ready — Cesium 1.138's
* the shared sampler can only inspect *visible* 3D tilesets, so a sample taken
* against the hidden Google tileset would silently miss.
@@ -1456,7 +1456,7 @@ function refreshProjectionTextures(record) {
// Only swap when the canvas content actually changed since the last swap.
// Frames land every ~10 s but this runs at 1 Hz — swapping an UNCHANGED
// canvas re-uploads the texture for nothing, and each material image
// reassignment is a flash opportunity on the live plane (field test
// reassignment is a flash opportunity on the live plane (owner field test
// 2026-07-04: intermittent white flashes on the monitor plane).
if (runtime.canvasStamp === runtime.lastSwappedCanvasStamp) return;
@@ -1884,7 +1884,7 @@ function drawProjectionFrame(record) {
// and a fresh 1920x1080 texture upload; the plane renders its white
// base color (planeMaterial color = WHITE, alpha .95) for the frame or
// two Cesium needs to rebind, which IS the periodic white flash from the
// field tests (2026-07-04 and 2026-07-30).
// owner field tests (2026-07-04 and 2026-07-30).
const signature = projectionFrameSignature(runtime);
runtime.drawnImageStamp = runtime.imageStamp;
if (signature !== null && signature === runtime.lastFrameSignature) {
@@ -2732,7 +2732,7 @@ function refreshHorizonCulling() {
// ---------------------------------------------------------------------------
// Ambient card tier (2026-07-29 design — spec:
// docs/CURRENT-STATE.md)
// docs/superpowers/specs/2026-07-29-cctv-ambient-cards-design.md)
// ---------------------------------------------------------------------------
/** Returns (creating on demand) the stable frame slot for a camera id. */
@@ -2805,7 +2805,7 @@ function refreshAmbientCards() {
});
}
// Field finding 4: current card holders rank with the 20% incumbency
// Owner finding 4: current card holders rank with the 20% incumbency
// distance discount, so a small camera move never batch-swaps the ring.
// Item C: passing the viewport dims + per-candidate screen anchors routes
// the budget fill through the screen-distribution grid, so periphery
@@ -2840,7 +2840,7 @@ function refreshAmbientCards() {
})),
{ limit: cardLimit }
);
// Field finding 2: grace must never apply to the active camera — drop any
// Owner finding 2: grace must never apply to the active camera — drop any
// lingering grace entry and keep it out of the retained-card baseline.
if (activeId) {
_cardIds.delete(activeId);
@@ -2911,7 +2911,7 @@ function pushAmbientCardEntries() {
}
/**
* Throttled MOUSE_MOVE hover pass (follow-up round 2, item B): pointing at a
* Throttled MOUSE_MOVE hover pass (owner round 2, item B): pointing at a
* camera icon that has no card summons its card immediately. This is
* EVENT-DRIVEN picking on a user gesture, not steady-state work — the
* ≥120 ms throttle caps it at ~8 scene.pick calls/s while the pointer is
@@ -3008,7 +3008,7 @@ function hoverFetchCardFrame(record) {
}
/**
* Card-frame pacer tick (field finding 3): launches AT MOST one fetch per
* Card-frame pacer tick (owner finding 3): launches AT MOST one fetch per
* tick, with cardFetchPolicy deciding whether a launch is allowed. Cold fill
* — any selected card still missing its FIRST frame — bursts up to 4
* in-flight fetches at 250 ms spacing so arriving in a new area populates
@@ -3592,7 +3592,7 @@ function ensureGizmo() {
}
/**
* §9.1 activation obstruction probe (LOCKED product rule): on camera
* §9.1 activation obstruction probe (LOCKED owner decision): on camera
* ACTIVATION only, fire ONE scene.pickFromRay along the frustum axis
* (mount → cap-center direction). If it hits the tiles closer than the pose
* range, clamp the plane's effective range just short of the first hit so the
@@ -4362,7 +4362,7 @@ const cctvLayer = {
// remain eligible for true empty-space deselection.
const pickedId = resolvePickId(picked);
if (pickedId !== null) return;
// Item A (follow-up round 2): the scene pick found no camera — try the
// Item A (owner round 2): the scene pick found no camera — try the
// painted ambient cards. The cards canvas is pointer-events:none (this
// handler owns the events), so a click landing on a card's rect selects
// its camera exactly like a click on the icon. Cesium click positions
@@ -4451,7 +4451,7 @@ const cctvLayer = {
_removeFocusAppearListener = null;
stopProjectionLoop();
stopGeometryLoadQueue();
// Ambient cards tear down COMPLETELY on disable (product design point 6):
// Ambient cards tear down COMPLETELY on disable (owner design point 6):
// source entries, pacer timer, in-flight handlers, and caches.
teardownAmbientCards();
hideCctvVisuals();
+7 -7
View File
@@ -1,6 +1,6 @@
// src/data/cctv.test.mjs — CCTV v2 pure frustum geometry (computeFrustumGeometry).
//
// Locks the CCTV frustum geometry described in docs/CURRENT-STATE.md:
// Locks the §2a math of docs/plans/2026-07-03-cctv-v2-design.md:
// - the far-cap (monitor plane) corners lie ON the plane through capCenter
// perpendicular to the frustum view axis (ε < 0.5 m) — this is the geometric
// invariant that welds the wireframe corner rays to the plane entity;
@@ -402,7 +402,7 @@ test('ground clamp lifts the CAP CENTER only — the rectangle stays rigid (true
assert.equal(g.capCenter.alt, floor, 'cap center clamps exactly to the floor');
// Corners derive rigidly from the lifted center: alt = floor ± cos(pitch)·halfH.
// The bottom pair sits BELOW the floor (tiles occlude it) — per-corner clamping
// is what flattened the wireframe into a fan (field test 2026-07-04).
// is what flattened the wireframe into a fan (owner field test 2026-07-04).
const upVert = Math.cos(toRad(-24)) * g.halfH;
for (const key of ['tl', 'tr']) {
assert.ok(Math.abs(g.corners[key].alt - (floor + upVert)) < 1e-6, `${key} alt ${g.corners[key].alt}`);
@@ -415,7 +415,7 @@ test('ground clamp lifts the CAP CENTER only — the rectangle stays rigid (true
test('clamped pose keeps corner/plane coincidence and the rigid 2·halfW × 2·halfH span', () => {
// The wireframe corner rays must terminate exactly on the monitor plane's
// corners AT THE DEFAULT AUSTIN POSE — this is the case that diverged by
// ~47.5 m under per-corner clamping (field test 2026-07-04).
// ~47.5 m under per-corner clamping (owner field test 2026-07-04).
const g = computeFrustumGeometry(AUSTIN_FABRICATED_CAMERA, AUSTIN_GROUND);
const d = viewDir(41, -24);
const capEnu = enu(g.mount, g.capCenter);
@@ -1090,7 +1090,7 @@ test('heading wrap: heading 350° produces a symmetric cap (left/right corners e
// ---------------------------------------------------------------------------
// Task 5 — calibration v2 store + CAL badge (design §3b/§3c as amended by the
// LOCKED product rules §9.2/§9.3: wipe-clean v2 store, panel-only badge).
// LOCKED owner decisions §9.2/§9.3: wipe-clean v2 store, panel-only badge).
// ---------------------------------------------------------------------------
/** Minimal in-memory localStorage stand-in for pure store-IO unit tests. */
@@ -1167,7 +1167,7 @@ test('calibration v2: a corrupt v1 key never leaks into the v2 store (v1 is dead
});
// v2 key is untouched/empty — v1's presence must have zero effect.
const restored = readCalibrationStoreV2(storage);
assert.equal(restored.size, 0, 'v2 store must start empty — no legacy import (product rule #3, §9.3)');
assert.equal(restored.size, 0, 'v2 store must start empty — no legacy import (owner decision #3, §9.3)');
assert.ok(!restored.has('austin-42'));
});
@@ -1190,7 +1190,7 @@ test('deriveCalBadge: RAW PRIOR for everything else (all Austin Open Data today)
// ---------------------------------------------------------------------------
// Task 5 (height-datum fix): regime-aware ground resolution pure helpers.
// the height-datum contract in docs/CURRENT-STATE.md.
// docs/superpowers/specs/2026-07-05-entity-height-datum-design.md §2.
// ---------------------------------------------------------------------------
test('surfaceRegimeKey: globe hidden (photoreal) → google-3d; globe visible → terrain-globe', () => {
@@ -1236,7 +1236,7 @@ test('normalizeCoverageMode: garbage keeps the current mode', () => {
assert.equal(normalizeCoverageMode(3, 'on'), 'on');
});
// Unchanged-frame signature (white-flash fix, field test 2026-07-30)
// Unchanged-frame signature (white-flash fix, owner field test 2026-07-30)
// ---------------------------------------------------------------------------
/** Builds an RGBA buffer from [r,g,b] triples. */
+9 -9
View File
@@ -1,7 +1,7 @@
/**
* @module cctvCards
* @description Screen-space thumbnail cards for the citywide ambient CCTV
* tier described in `docs/CURRENT-STATE.md`.
* tier (design: docs/superpowers/specs/2026-07-29-cctv-ambient-cards-design.md).
* Replaces the rejected world-space static-plane ring with small canvas cards
* anchored to each LOD-selected camera's screen position, showing its latest
* paced static frame.
@@ -18,10 +18,10 @@
* persistence, retry pacing, cache pruning) and supplies ready-to-draw stable
* frame-slot references to the host.
*
* Zero-flicker contract (product requirement):
* Zero-flicker contract (owner requirement):
* - An AMBIENT entry whose frame slot has no drawn frame yet (`stamp === 0`)
* renders NOTHING — no placeholder, no chip. The camera icon alone carries
* it. Sole documented exception (follow-up round 2, item B): a PINNED entry
* it. Sole documented exception (owner round 2, item B): a PINNED entry
* (hover-summoned, `entry.pinned === true`) paints its chrome immediately —
* explicit user gesture wants instant feedback — with an empty thumb area
* until its fast-tracked frame lands.
@@ -48,7 +48,7 @@ export const CCTV_FRAME_CANVAS_W = 192;
export const CCTV_FRAME_CANVAS_H = 108;
/**
* Min screen separation between accepted card anchors (greedy declutter).
* Field test 2026-07-30: 130 read too sparse once the HUD safe-zone
* Owner field test 2026-07-30: 130 read too sparse once the HUD safe-zone
* filter started dropping cards as well. 112 still exceeds the card box width
* (104 px) so accepted boxes cannot overlap. This is THE density knob.
*/
@@ -59,8 +59,8 @@ export const CCTV_CARD_SAFE_TOP_MAX_PX = 150;
/** Bounded thumbnail cache (frame slots kept beyond the live card set). */
export const CCTV_FRAME_CACHE_MAX = 96;
// ─── Altitude scaling (field test finding 5, 2026-07-29) ──────────────
// Validated curve: cards are full size at street level, "start to get
// ─── Altitude scaling (owner field-test finding 5, 2026-07-29) ──────────────
// Owner-decided curve: cards are full size at street level, "start to get
// smaller" from ~1,800 m, "scale down progressively" to ~0.45 by 6,000 m,
// keep shrinking slightly and alpha-fade out across 7,500→9,500 m, and are
// fully hidden above that ("just the icons" at the highest zooms).
@@ -71,7 +71,7 @@ export const CCTV_CARD_FADE_END_M = 9_500;
export const CCTV_CARD_SCALE_AT_MID = 0.45;
export const CCTV_CARD_SCALE_MIN = 0.35;
// ─── Frame-fetch pacing (field test finding 3, 2026-07-29) ────────────
// ─── Frame-fetch pacing (owner field-test finding 3, 2026-07-29) ────────────
/** Steady-state global gate: one card-frame fetch per second. */
export const CCTV_CARD_FETCH_STEADY_SPACING_MS = 1_000;
/** Cold-fill burst spacing between fetch launches. */
@@ -150,7 +150,7 @@ export function declutterCctvCards(candidates, { minSepPx = CCTV_CARD_MIN_SEP_PX
}
/**
* Altitude-driven card scale + opacity (field test finding 5,
* Altitude-driven card scale + opacity (owner field-test finding 5,
* 2026-07-29 — curve constants above). Piecewise, monotonic non-increasing
* in both channels:
* - ≤1,800 m: full size, fully opaque.
@@ -178,7 +178,7 @@ export function cardScaleForAltitude(cameraHeightM) {
}
/**
* Cold-fill burst pacing policy (field test finding 3, 2026-07-29), as
* Cold-fill burst pacing policy (owner field-test finding 3, 2026-07-29), as
* a pure decision so the pacer tick stays trivially testable. While any
* selected card still lacks its FIRST frame (`coldFill`), up to
* `CCTV_CARD_FETCH_BURST_LIMIT` fetches may be in flight with
+2 -2
View File
@@ -129,7 +129,7 @@ test('CCTV card module cannot resurrect a canvas, projection, listener, or priva
for (const pattern of forbidden) assert.doesNotMatch(source, pattern);
});
// ─── cardScaleForAltitude — the validated altitude curve (finding 5) ────
// ─── cardScaleForAltitude — the owner-decided altitude curve (finding 5) ────
test('cardScaleForAltitude: full size and opacity at or below 1,800 m', () => {
assert.deepEqual(cardScaleForAltitude(0), { scale: 1, alpha: 1 });
@@ -137,7 +137,7 @@ test('cardScaleForAltitude: full size and opacity at or below 1,800 m', () => {
assert.deepEqual(cardScaleForAltitude(NaN), { scale: 1, alpha: 1 });
});
test('cardScaleForAltitude: hits the validated waypoints', () => {
test('cardScaleForAltitude: hits the owner-decided waypoints', () => {
const mid = cardScaleForAltitude(CCTV_CARD_SCALE_MID_M);
assert.ok(Math.abs(mid.scale - CCTV_CARD_SCALE_AT_MID) < 1e-9, 'scale ~0.45 at 6,000 m');
assert.equal(mid.alpha, 1);
+1 -1
View File
@@ -2,7 +2,7 @@
* @module cctvGizmo
*
* Direct-manipulation calibration gizmo for the CCTV layer (design:
* the CCTV calibration contract in `docs/CURRENT-STATE.md`).
* docs/superpowers/specs/2026-07-05-cctv-viewshed-gizmo-design.md §3c).
*
* Two layers:
* - Pure drag math (this top section): ray↔axis closest-point, ray↔plane
+1 -1
View File
@@ -1,5 +1,5 @@
// src/data/cctvGizmo.test.mjs — pure drag math for the CCTV calibration gizmo
// documented in docs/CURRENT-STATE.md.
// (docs/superpowers/specs/2026-07-05-cctv-viewshed-gizmo-design.md §3c).
//
// Locks:
// - closestParamOnAxis returns the metre-parameter along the AXIS of the
+12 -11
View File
@@ -7,15 +7,16 @@
* card set, keeps that set stable across small camera moves (eviction
* grace), and paces static-frame refreshes per source.
*
* The engine retains zoom-scaled budgets, distance-ranked in-view selection,
* an eviction-grace planner, and source-aware refresh cadences, retargeted
* from the rejected world-space static-plane ring to the
* Engine adapted from Manjunath's Part C work (`fix/cctv-part-c-review`):
* the zoom-scaled budgets, distance-ranked in-view selection, the
* eviction-grace planner, and the source-aware refresh cadences are his,
* retargeted from the rejected world-space static-plane ring to the
* screen-space thumbnail cards (see
* the ambient-card behavior documented in `docs/CURRENT-STATE.md`).
* docs/superpowers/specs/2026-07-29-cctv-ambient-cards-design.md).
*/
// Ambient-card budgets (follow-up round 2, item C: raised 16/24/32 → 20/28/40 —
// "a lot of empty space"). Tunable as a set, together with the card
// Ambient-card budgets (owner round 2, item C: raised 16/24/32 → 20/28/40 —
// "a lot of empty space"). Owner-tunable as a set, together with the card
// scale waypoints (1,800/6,000/9,500 m, cctvCards.js) and
// CCTV_CARD_MIN_SEP_PX: budgets say how many cameras HOLD cards, the
// waypoints and separation say how many fit on screen.
@@ -34,7 +35,7 @@ const PROVIDER_STATIC_REFRESH_MS = Object.freeze({
/**
* Returns the bounded ambient-card budget for the current viewer height.
* Card counts stay inside the 20..40 range (follow-up round 2): street level
* Card counts stay inside the 20..40 range (owner round 2): street level
* keeps the overlay sparse, metro scale earns the full ring.
*
* @param {number} cameraHeightM
@@ -52,7 +53,7 @@ export function cctvLodBudgets(cameraHeightM) {
}
/**
* Selection-level incumbency (field test finding 4, 2026-07-29): a
* Selection-level incumbency (owner field-test finding 4, 2026-07-29): a
* camera currently holding a card ranks with its distance discounted by this
* factor, so a small camera move never batch-swaps the ring — a non-carded
* camera displaces a carded one only when it is meaningfully (>20%) closer.
@@ -144,17 +145,17 @@ export function blendCenterRankKm(
return (1 - w) * km + w * spread * fraction;
}
// Screen-distribution grid (follow-up round 2, item C — "a lot of empty space"):
// Screen-distribution grid (owner round 2, item C — "a lot of empty space"):
// pure nearest-first selection clusters winners at screen center (nearest ==
// most central at typical view pitch) and leaves the periphery bare. The
// viewport is bucketed into this grid and every occupied cell gets its best
// candidate before global rank fills the rest. Tunable together with
// candidate before global rank fills the rest. Owner-tunable together with
// the budgets above.
export const CCTV_CARD_GRID_COLS = 5;
export const CCTV_CARD_GRID_ROWS = 4;
/**
* Screen-space distribution pass (follow-up round 2, item C). Buckets the
* Screen-space distribution pass (owner round 2, item C). Buckets the
* viewport into a CCTV_CARD_GRID_COLS × CCTV_CARD_GRID_ROWS grid, assigns
* each candidate to its cell (anchors just outside the viewport clamp to
* the edge cells), ranks within cells by effective distance (`rankKm` —
+5 -5
View File
@@ -1,6 +1,6 @@
// src/data/cctvLod.test.mjs
// Pure LOD-engine tests, adapted from earlier Part C suite
// zoom-scaled card budgets, nearest-first in-view
// Pure LOD-engine tests, adapted from Manjunath's Part C suite
// (fix/cctv-part-c-review): zoom-scaled card budgets, nearest-first in-view
// selection, video exclusion, the eviction-grace planner, and source-aware
// static-frame pacing.
import { test } from 'node:test';
@@ -35,7 +35,7 @@ function candidates(count, options = {}) {
}
test('cctvLodBudgets scales the card budget from 20 to 40 with view height', () => {
// Follow-up round 2 (item C): budgets raised 16/24/32 -> 20/28/40.
// Owner round 2 (item C): budgets raised 16/24/32 -> 20/28/40.
assert.equal(CCTV_AMBIENT_CARD_MIN, 20);
assert.equal(CCTV_AMBIENT_CARD_MID, 28);
assert.equal(CCTV_AMBIENT_CARD_MAX, 40);
@@ -71,7 +71,7 @@ test('selectCctvLod tolerates malformed candidate rows', () => {
assert.deepEqual(selected.cardIds, ['cam-ok']);
});
// ─── Selection-level incumbency (field test finding 4) ────────────────
// ─── Selection-level incumbency (owner field-test finding 4) ────────────────
test('incumbentRankKm discounts incumbents by the 20% factor', () => {
assert.equal(incumbentRankKm(10, false), 10);
@@ -121,7 +121,7 @@ test('selectCctvLod: no incumbents means plain nearest-first (unchanged behavior
assert.deepEqual(plain.cardIds, withEmpty.cardIds);
});
// ─── distributeCctvCards — screen distribution (follow-up round 2, item C) ──────
// ─── distributeCctvCards — screen distribution (owner round 2, item C) ──────
// Grid: 5 cols x 4 rows. At viewW 1000 / viewH 800 each cell is 200x200 px.
test('distributeCctvCards spreads clustered candidates across cells', () => {
+2 -2
View File
@@ -2,7 +2,7 @@
* @module cctvViewshed
*
* Viewshed presentation for the CCTV layer (design:
* the CCTV viewshed geometry contract).
* docs/superpowers/specs/2026-07-05-cctv-viewshed-gizmo-design.md §3a/§3b).
*
* Two responsibilities, both pure of layer state:
* - Color identity: a stable per-camera hue (golden-angle spaced over the
@@ -30,7 +30,7 @@ const LINE_ALPHA_ACTIVE = 1.0;
* Stable hue (degrees, [0, 360)) for a camera's position in the id-sorted
* catalog. Golden-angle spacing keeps any local cluster of neighbor cameras
* visually separated; id-sorting makes the assignment deterministic across
* sessions for a stable catalog (design §3a, open question Q4).
* sessions for a stable catalog (design §3a, owner question Q4).
* @param {number} index - Camera index in the id-sorted catalog.
* @returns {number} Hue in degrees.
*/
+1 -1
View File
@@ -1,5 +1,5 @@
// src/data/cctvViewshed.test.mjs — viewshed hue assignment + frustum volume
// geometry documented in docs/CURRENT-STATE.md.
// geometry (docs/superpowers/specs/2026-07-05-cctv-viewshed-gizmo-design.md §3a/§3b).
//
// Locks:
// - cameraHue is golden-angle spaced and deterministic (color identity is
+1 -1
View File
@@ -42,7 +42,7 @@ for (const layer of LAYERS) {
assert.match(regime, /if \(!_models3dEnabled\) return false;/,
'OFF must keep Cockpit AIR contacts in 2D');
assert.doesNotMatch(regime, /!_models3dEnabled\s*&&\s*!_cockpitContactMode/,
'Cockpit must not bypass the user-visible Display toggle');
'Cockpit must not bypass the owner-visible Display toggle');
});
test(`${layer.name}: the pilot's own airframe stays hidden in cockpit`, () => {
+1 -1
View File
@@ -92,7 +92,7 @@ export function rankContactMatch({ query, hex = '', callsign = '', registration
* (`track_entity`) is a MUTATION fulfilling "follow that one" — returning an
* ambiguity for the caller to resolve would cost a round-trip mid-demo, and
* the model's observed response to a non-ok track result is to retry with
* different guesses rather than to ask (field session 2026-08-21,
* different guesses rather than to ask (owner field session 2026-08-21,
* 23:48). So the lookup always commits. What it owes the caller is STABILITY:
* hex is unique and always present, so the same query resolves to the same
* contact for as long as both are loaded, instead of flipping between polls
+1 -1
View File
@@ -4,7 +4,7 @@ import * as Cesium from 'cesium';
* Per-layer data attribution registered into Cesium's credit display.
*
* Legal requirement (see DATA_SOURCES.md, findings H10/H11 in
* every third-party data layer this app can
* docs/pre-ship-audit-2026-07-01.md): every third-party data layer this app can
* display carries its own license and required attribution — ODbL (OSM
* datacenters/dams, adsb.lol, Overpass roads), CC BY-NC-SA (TeleGeography
* cables), NASA FIRMS, CelesTrak, USGS, City of Austin, GBFS operators, OpenSky.
+1 -1
View File
@@ -30,7 +30,7 @@ export const AIRCRAFT_BRACKET_ALPHA_FLOOR = 0.35;
* stays a pure policy module with no Cesium dependency; detectionPolicy.test.mjs
* imports the real constant and pins the two together so they cannot drift.
*
* It MOVES WITH THE DEFAULT (0.05 → 0.03 → 0.01; final value 2026-08-24).
* It MOVES WITH THE DEFAULT (0.05 → 0.03 → 0.01; owner final lock 2026-08-24).
* That pin is the tripwire
* for exactly this change, and the decision it forces is which of two things the
* approval attaches to: the bracket BRIGHTNESS, or the slider POSITION. It is
+1 -1
View File
@@ -36,7 +36,7 @@ test('the bracket floor anchor mirrors the real keyhole default it is calibrated
test('the default OUTSIDE setting reproduces the approved 0.35 floor exactly', () => {
// Byte-identical at the default, at every keyhole alpha, with the setting
// passed explicitly and with it omitted. This is the accepted look; only
// passed explicitly and with it omitted. The owner approved this look; only
// the off-default range is allowed to change.
assert.equal(aircraftBracketAlphaFloor(KEYHOLE_OUTSIDE_OPACITY_DEFAULT), AIRCRAFT_BRACKET_ALPHA_FLOOR);
for (const alpha of [0.01, 0.05, 0.2, 0.34, 0.35, 0.36, 0.7, 1]) {
+1 -1
View File
@@ -11,7 +11,7 @@
* 2. Free-form name/description text ("5TH ST / WEST AVE", "N LAMAR BLVD") is
* full of STREET names that merely contain a cardinal word. Reading a bare
* "West" there as a facing direction mis-orients the camera with false
* confidence (59 of ~1000 Austin cameras hit this — field review
* confidence (59 of ~1000 Austin cameras hit this — owner adversarial review
* 2026-07-04). There, only explicit travel forms ("WESTBOUND"/"WB") count —
* leave `allowBare=false` (the default).
*
+1 -1
View File
@@ -1,5 +1,5 @@
// directionToHeading — two matching modes. The regression that motivated the
// split (adversarial field review, 2026-07-04): bare cardinal words were
// split (owner adversarial review, 2026-07-04): bare cardinal words were
// matched in free-form Austin camera names, so a street like "5TH ST / WEST
// AVE" was mis-read as a west-facing camera with false high confidence.
import { test } from 'node:test';
+1 -1
View File
@@ -1,5 +1,5 @@
// src/data/fireAnchors.js — DEM ground anchors for rendered FIRMS detections
// (field finding 2026-07-21: at close/oblique zoom over high country,
// (owner field finding 2026-07-21: at close/oblique zoom over high country,
// fire dots anchored at ellipsoid height 0 read as buried inside the terrain
// ~1-2 km below the visible surface).
//
+1 -1
View File
@@ -1,5 +1,5 @@
// src/data/fireAnchors.test.mjs — DEM ground anchors for rendered FIRMS
// detections (field finding 2026-07-21: close-zoom fire dots read as
// detections (owner field finding 2026-07-21: close-zoom fire dots read as
// buried under high terrain because anchors sat at ellipsoid height 0).
//
// Locks the module's two jobs:
+1 -1
View File
@@ -85,7 +85,7 @@ test('buildCellCard: plural noun, max FRP and newest age, accent passthrough', (
assert.equal(card.accent, accentForSeverity('orange'));
});
// Field finding 2026-07-21: anchors must sit on the DEM once the shared
// Owner field finding 2026-07-21: anchors must sit on the DEM once the shared
// ground floor is warm — and the cached per-fire position must re-anchor when
// the floor lands AFTER the first (cold, height-0) render. Distinct coords
// from every other test in this file (module caches persist across tests).
-1
View File
@@ -320,7 +320,6 @@ export function createFirmsHeatmapLayer({
lastUpdate: _lastUpdate,
loading: _loading,
stale: _stale,
keyRequired: _keyRequired,
error: _keyRequired ? 'KEY REQUIRED' : (_stale ? staleText : _error),
loadingLabel,
};
+23 -29
View File
@@ -17,7 +17,7 @@
*
* Press Escape or click empty space to deselect a tracked flight — the camera
* is released IN PLACE (no flyTo), so the user keeps the context they were
* looking at (product rule 2026-07-02).
* looking at (owner decision 2026-07-02).
*/
import * as Cesium from 'cesium';
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
@@ -100,14 +100,14 @@ const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
// --- Ground traffic (product change 2026-07-03: "absolutely we should see planes
// --- Ground traffic (owner reversal 2026-07-03: "absolutely we should see planes
// taxiing and landing") -----------------------------------------------------------
// Present-but-grounded planes are RENDERED instead of being skipped: same class
// silhouette + rotation pipeline, clickable/trackable/detectable, sticky metadata
// updating normally. Landing/takeoff is a TRANSITION — the on_ground flip restyles
// the existing billboard in place, never a removal. Ground planes draw no trails
// and are excluded from the ambient enrichment sweep (click-to-enrich still
// works). In 3D mode they take model slots like airborne planes (product rule
// works). In 3D mode they take model slots like airborne planes (owner decision
// 2026-07-03 — no air/ground distinction), placed by the one-shot ground snap
// (see _modelDisplayPosition).
//
@@ -121,7 +121,7 @@ const MIL_TINT = Cesium.Color.fromCssColorString('#FFB800');
const GROUND_SCALE = 0.8;
/** Fleet (untracked) billboard tint: amber for known-military, white otherwise.
* Ground traffic gets NO special tint (validated behavior 2026-07-03 field test). */
* Ground traffic gets NO special tint (owner verdict 2026-07-03 field test). */
function _fleetBillboardColor(icao24) {
return isMilitaryIcao(icao24) ? MIL_TINT : Cesium.Color.WHITE;
}
@@ -131,7 +131,7 @@ function _fleetBillboardScale(icao24, klass) {
return (CLASS_SCALE_2D[klass] || 1) * (_flightData.get(icao24)?.onGround ? GROUND_SCALE : 1);
}
/** Depth-test policy for aircraft billboards. Round 5 (product invariant
/** Depth-test policy for aircraft billboards. Round 5 (owner directive
* 2026-07-06: "I just want the planes and their lines to ALWAYS be
* visible... evenly applied"): EVERY contact renders depth-test-free at
* every distance — grounded, low, and airborne alike. The photoreal mesh
@@ -157,7 +157,7 @@ const MODEL_MIN_PX = 24; // floor so distant models stay visible WITHOUT
// min-pixel blob (was 54 — far planes at the All radius became white
// star-bursts); ~matches the 2D icon size so the model↔billboard read is consistent
const TRACKED_MODEL_MIN_PX = 40; // keep the glTF silhouette comparable to the selected 2D glyph at handoff
export const TRACKED_MODEL_MAX_PX = 200; // selected close-range tracked-target feel
export const TRACKED_MODEL_MAX_PX = 200; // owner-selected close-range tracked-target feel
const MODEL_NATIVE_RADIUS_M = 34.41;
const MODEL_SCALE = 1; // airplane.glb is transform-applied and baked to real-world meters
// Per-mode caps. Each model is its own draw call (no instancing yet), so these bound the frame cost.
@@ -246,7 +246,7 @@ const _modelGen = new Map();
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
* against a new lifecycle's globals (which destroy cleared). Captured by _ensureModel. */
let _modelEpoch = 0;
/** DEFAULT-ON in PROXIMITY (product invariant 2026-08-22). A fresh boot never runs
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
* layer-state restoration, so this initializer — not the codec — is what the app
* actually starts with; it must stay in lockstep with the `models3d` default in
* `layerState.js` and `this._models3dEnabled` in ui.js, or the DISPLAY rail would
@@ -457,7 +457,7 @@ const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
);
function _normalBillboardScaleByDistance() {
// Preserve the established close-range 3× scale. Any smaller user-visible
// Preserve the established close-range 3× scale. Any smaller owner-visible
// default belongs in a separate evidence-backed proposal.
return new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5);
}
@@ -599,7 +599,7 @@ let _trailBackfillToken = 0;
const RENDER_DELAY_SEC = 30;
/** @constant {number} Polls an aircraft may miss before removal (transient OpenSky dropouts). */
const MISSING_POLL_LIMIT = 3;
// --- Landed-plane fast cull (field report 2026-07-02: "phantom" planes
// --- Landed-plane fast cull (owner field report 2026-07-02: "phantom" planes
// lingered ~2 min at airports after touchdown). OpenSky's on_ground flag LAGS
// the actual landing, so a landed plane's last airborne-classified fixes show
// it low + slow on the runway; when such a plane then drops out of the poll,
@@ -652,7 +652,8 @@ function _approxDistanceKm(lat1, lon1, lat2, lon2) {
function _likelyLanded(icao24) {
const info = _flightData.get(icao24);
if (!info) return false;
// Round 7: the fast cull only applies to contacts that were AIRBORNE
// Round 7 (owner: "fewer planes than OpenSky's own map" + "parked planes
// never heal"): the fast cull only applies to contacts that were AIRBORNE
// this session — its original target, the post-LANDING ghost. OpenSky's
// ground coverage flaps constantly, so fast-culling every grounded contact
// put parked planes in an evict/re-enter churn: each re-entry was a
@@ -1579,14 +1580,14 @@ function _noteTrackedModelLoadFailure(url, err) {
/**
* The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven
* (product invariant 2026-08-19). Unlike the fleet, this does NOT consult the
* (owner directive 2026-08-19). Unlike the fleet, this does NOT consult the
* DISPLAY-rail `models3d` toggle: the selected contact is a single model, it is
* what the camera is pointed at, and zooming in on a target should resolve it
* into an aircraft without the operator arming anything. The toggle keeps
* owning the FLEET (`_modelRegimeActive`), which is the draw-call budget.
*
* Thresholds + hysteresis live in trackedModelRegime.js: enter at
* TRACKED_MODEL_ENTER_ALT_M (150_000 m — the playtested swap distance,
* TRACKED_MODEL_ENTER_ALT_M (150_000 m — the owner's playtested swap distance,
* deliberately NEARER than the fleet's 800 km ceiling this used to inherit),
* hand back only above TRACKED_MODEL_EXIT_ALT_M, so orbiting AT the boundary
* cannot flap billboard↔model. See that module's header for why the tracked
@@ -1897,7 +1898,7 @@ const FLOOR_EASE_EPSILON_M = 0.02;
/**
* The floor to stand a grounded contact on while its own cell is unresolved.
*
* Product invariant (2026-08-21, after a Re:Earth outage buried a parked contact
* Owner directive (2026-08-21, after a Re:Earth outage buried a parked contact
* at a Texas field): "hold the last known altitude until a fresh one comes in.
* Never render otherwise." Two tiers, strongest first:
* own — a floor this contact's OWN cell resolved to while it stood there.
@@ -1976,7 +1977,7 @@ function _dropHeldFloor(state) {
/** @constant {number} How long a retired hold stays usable as a rehydration
* seed — three poll intervals.
*
* Deleting the state outright was the first cut, and a field observation found
* Deleting the state outright was the first cut, and an owner sighting found
* what that costs: VIR138M at JFK, 45 kt down the runway, "clearly on good
* ground, then suddenly popped below the ground, then popped back up".
* OpenSky's `on_ground` flag is not clean through a rotation — it flaps — and
@@ -2129,7 +2130,7 @@ function _floorGroundedDisplayPosition(icao24, info, pos, modelOwnsVisual, nowMs
next.heldActive = Number.isFinite(effective);
}
// The floor moved DOWN under a contact that was standing on a BORROWED one.
// Dropping it by that difference in a single tick is the snap product behavior requires
// Dropping it by that difference in a single tick is the snap the owner asked
// not to have, so approach it instead. Two ways in, and both need it:
// - the real floor arrives below the hold (releasing the hold);
// - a re-probe finds a LOWER neighbour than the one being held, which the
@@ -2309,12 +2310,6 @@ export function _clearDisplayFloorStateForTest() {
_displayFloorState.clear();
}
/** Test hook: drops cached model ground snaps so a browser-harness scenario
* cannot inherit another scenario's per-contact measurement. */
export function _clearGroundSnapStateForTest() {
_groundSnap.clear();
}
/** Spec identity for a LOADED model: URL and scale together (same-URL classes
* differ by scale — airliner vs quadjet both ship airplane.glb). */
const _specKeyFor = (klass) => {
@@ -2341,7 +2336,7 @@ function _syncModelToClass(icao24) {
}
}
/** IR hot-target mode (field test 2026-08-16): the NVG/FLIR post-styles
/** IR hot-target mode (owner playtest 2026-08-16): the NVG/FLIR post-styles
* map LUMINANCE, so mid-gray textured models read cold and vanish into
* terrain. While a boost style is active every model renders flat white
* (hottest); per-spec color/tint restores on style exit. Driven by ui.js
@@ -2679,7 +2674,7 @@ function _fleetTick() {
// not just at the handoff below, or accumulated conversions would starve
// ordinary contacts of 3D models. (The handoff guard stays as defence.)
if (isTr3b(icao)) continue;
// Ground planes compete for model slots like everyone else (product rule
// Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03: "3D mode is respected regardless of whether a plane is on the
// ground or in the air — no distinction"). The cap + nearest-first ordering
// below already bound airport clusters; grounded placement is handled by the
@@ -2782,7 +2777,7 @@ function _fleetTick() {
cameraHeightM: camera.positionCartographic?.height,
});
_billboardLimbScale.set(bb, treatment.factors.scale);
// Two-tier glyph raster (field test 2026-08-16): the billboard atlas
// Two-tier glyph raster (owner playtest 2026-08-16): the billboard atlas
// has no mipmaps, so no single texture stays crisp across the ~25–150
// device-px range scaleByDistance produces. Swap between the 64 px fleet
// raster and the 192 px close raster on the billboard's ACTUAL on-screen
@@ -3079,7 +3074,7 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
// and floor every waypoint at it so low baro segments never dive below the
// mesh; a no-baro waypoint (predominantly taxi/ground segments in /tracks)
// sits ON the surface when the floor is known.
// Round-2 fix: the
// Round-2 fix (owner: "trails suddenly much shorter / not loading"): the
// resolve is BOUNDED (≤1.2 s), not a blocking await — a cold Re:Earth
// lookup across a long path could stall the paint for seconds-to-timeout.
// Paint with whatever cells are warm; the resolve keeps filling the cache
@@ -3891,7 +3886,6 @@ const flightsLayer = {
// Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy.
_clearDisplayFloorStateForTest,
_clearGroundSnapStateForTest,
/** @type {number} Polling interval (ms) between update() calls */
updateInterval: 30000,
@@ -4053,7 +4047,7 @@ const flightsLayer = {
* reconcile them with the billboard collection.
*
* Handles HTTP 429 (rate-limit), 401/403 (auth), and transient errors
* with exponential-ish backoff. On success, adds, updates, or removes
* with exponential-ish backoff. On success, adds/updates/removes
* billboards and position history, triggers lerp blending for the
* tracked aircraft, and updates its label text.
*
@@ -4247,7 +4241,7 @@ const flightsLayer = {
// on_ground surface prior: ONLY synchronous warm-cache reads here —
// never a per-aircraft network fetch inside the poll loop (see the
// batch resolve call below, which fills this cache for NEXT poll). A
// Round 5 SIMPLIFICATION (product invariant: one floor, evenly applied):
// Round 5 SIMPLIFICATION (owner directive: one floor, evenly applied):
// the grounded surface is the round-4 choke point and nothing else —
// rendered-mesh cell first, real (never fallback-poisoned) DEM cell
// second. The old exact-5-decimal warm chain is GONE: it minted a new
@@ -4553,7 +4547,7 @@ const flightsLayer = {
// is about to delete (billboard restore, DR cache reset), and we must
// never leave the camera mid-follow with stale tracking state. The
// camera is then RELEASED IN PLACE — it stays where the follow left
// it, fully free (product rule 2026-07-02: no overview flyTo).
// it, fully free (owner decision 2026-07-02: no overview flyTo).
if (icao24 === _trackedIcao) {
_clearTracking(false, { evicted: true });
}
+4 -4
View File
@@ -885,13 +885,13 @@ test('display floor: the cached output is dropped when the floor changes under i
// --- F8: hold the last known floor through a floor-data gap ----------------
//
// Field incident 2026-08-21: four `[terrain-heights-proxy] refresh incomplete`
// Owner incident 2026-08-21: four `[terrain-heights-proxy] refresh incomplete`
// events in a row (Re:Earth timing out), and a parked contact at a Texas field
// popped BELOW the photoreal mesh for a few seconds. A cold cell used to mean
// "no clamp", which is only safe if the un-clamped height is a real reading —
// and for a grounded contact reporting no altitude it is the geoid, tens of
// metres under the mesh inland. The product must hold the last known altitude
// until fresh floor evidence arrives.
// metres under the mesh inland. Owner: "hold the last known altitude until a
// fresh one comes in. Never render otherwise."
test('display floor: a cold cell HOLDS the last floor that resolved for this contact', () => {
_clearDisplayFloorStateForTest();
@@ -1052,7 +1052,7 @@ test('display floor: an on_ground FLAP mid-takeoff-roll never dips below the run
// from the resolved surface to baro + geoid N — which at a sea-level field IS
// the geoid. Deleting the hold on the airborne poll made that switch visible:
// the contact came back grounded with no prior, outrunning its own floor
// cells at 23 m/s, and sat under the runway (field observation, VIR138M).
// cells at 23 m/s, and sat under the runway (owner sighting, VIR138M).
const GROUND = -28.5, GEOID = -32.5, LON = -73.78;
reportMeshFloorCell(40.64, LON, GROUND); // only the cell it STARTED on is warm
let lat = 40.64;
+5 -4
View File
@@ -5,14 +5,15 @@
// Caltrans/TfL camera priors) give ORTHOMETRIC height (H, "height above mean
// sea level"). N is the local geoid undulation — the gap between the WGS84
// ellipsoid and the geoid (~mean sea level) surface, ranging roughly
// -106..+85 m worldwide. See docs/CURRENT-STATE.md.
// -106..+85 m worldwide. See docs/plans/2026-07-05-entity-height-datum-fix.md.
//
// The implementation uses `egm96-universal` (npm, MIT, embeds the NGA
// Decision rule (task brief): try `egm96-universal` (npm, MIT, embeds the NGA
// EGM96 15' grid) as a lazy dynamic import so its ~2.7 MB grid data-chunk
// never lands in the eager Vite bundle. Only fall back to vendoring the NGA
// grid ourselves if the package fails tests, isn't browser-safe, or bloats
// the eager bundle. It passed the browser-safety, accuracy, and bundle checks,
// so this file is a thin wrapper around it — no vendored fallback is needed.
// the eager bundle. `egm96-universal` passed all three checks (see the
// task report), so this file is a thin wrapper around it — no vendored
// fallback was needed.
//
// egm96-universal's `meanSeaLevel(lat, lon)` already returns exactly N in
// metres (relative to WGS84 ellipsoid) with internal longitude
+3 -3
View File
@@ -1,4 +1,4 @@
// src/data/geoid.test.mjs — EGM96 geoid-undulation lookup.
// src/data/geoid.test.mjs — EGM96 geoid-undulation lookup (docs/plans/2026-07-05-entity-height-datum-fix.md Task 1).
//
// Locks the module's public interface (later tasks — aircraft altitude
// correction, CCTV terrain fallback — call this verbatim):
@@ -7,7 +7,7 @@
// orthometricToEllipsoidal(hMslM, latDeg, lonDeg): number hMslM + N
//
// Tolerance is loose (±2.5 m) by design: the bundled grid is EGM96 while the
// reference values are Re:Earth's EGM2008 — the two
// plan's "Verified facts" reference values are Re:Earth's EGM2008 — the two
// models differ by up to ~1 m, and the brief's own tolerance absorbs that
// spread rather than asserting exact agreement.
import { test } from 'node:test';
@@ -100,7 +100,7 @@ test('the reported SFO cockpit OSD height turns into a small positive MSL number
await ensureGeoidReady();
const n = geoidHeight(SFO.lat, SFO.lon);
assert.ok(n < -25 && n > -40, `SFO undulation should be strongly negative, got ${n}`);
// The screenshot showed ALT: -15m ellipsoidal over the SFO deck.
// The owner's screenshot: ALT: -15m ellipsoidal over the SFO deck.
const displayed = ellipsoidalToMslDisplayM(-15, n);
assert.ok(
displayed > 10 && displayed < 25,
+3 -3
View File
@@ -1,7 +1,7 @@
// src/data/groundFloor.js — coarse ground-floor clamp for entity render
// heights (field-test round 2026-07-06).
//
// Two field findings drove this module:
// Two owner findings drove this module:
// - RS46 (military H60): baro-only low-altitude contacts near steep terrain
// render INSIDE the hillside (no alt_geom → baro+N is off by more than the
// local relief).
@@ -118,7 +118,7 @@ export const NEIGHBOR_FLOOR_MIN_SAMPLES = 2;
*
* An earlier cut leaned HIGH, reasoning from the locked "never below the
* visible surface" principle. That principle is about a contact's OWN measured
* ground; applied to a BORROWED cell it inverts, and a field test found
* ground; applied to a BORROWED cell it inverts, and an owner playtest found
* why — planes floating in midair at terminal gates. The two errors are not
* symmetric:
* - Too LOW is inert. `displayFloorHeightM` only ever RAISES a position, so a
@@ -376,7 +376,7 @@ export function allocateCorridorCells(
return out;
}
// --- Mesh-floor cells (round 4, validated design) ---------------------
// --- Mesh-floor cells (round 4, owner-approved design) ---------------------
// The Re:Earth DEM is BARE EARTH; the visible world in the google-3d regime
// is the photogrammetric MESH, which sits above it (measured ~17 m at the
// Austin airport apron). DEM-flooring therefore still buried sprites/trails
+1 -1
View File
@@ -597,7 +597,7 @@ test('neighborFloorM takes the apron, not the roof, at a structure edge', () =>
reportMeshFloorCell(30.201, -97.66, 120); // the apron
reportMeshFloorCell(30.199, -97.66, 205); // the terminal roof next door
// A parked contact is on the apron; it is never on the roof. Leaning high
// here is what put planes in midair at gates during the field test.
// here is what put planes in midair at gates during the owner playtest.
assert.equal(neighborFloorM({ lat: 30.2, lon: -97.66 }), 120);
});
+1 -1
View File
@@ -1,4 +1,4 @@
// Mapped-installation proxy persistence (field test 2026-08-18: "search
// Mapped-installation proxy persistence (owner playtest 2026-08-18: "search
// nearby sites" was slow because every look around paid a live Overpass round
// trip, and the 5-minute memory tier died with the dev server).
//
+1 -1
View File
@@ -183,7 +183,7 @@ function integerOption(key, token, defaultValue) {
const OPTION_GROUPS = Object.freeze({
flights: Object.freeze([
// Product invariant 2026-08-22: the fleet's 3D models are DEFAULT-ON in
// Owner directive 2026-08-22: the fleet's 3D models are DEFAULT-ON in
// PROXIMITY mode. Proximity is itself the altitude/count gate — models only
// materialize once the camera is close enough and only for the nearest
// contacts in view — so "on" costs nothing at globe scale, and an operator
+1 -1
View File
@@ -332,7 +332,7 @@ test('compact URL omits absent-meaning option state and still resolves to it', (
});
test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, and the rail agree', async () => {
// Product invariant 2026-08-22: the DISPLAY-rail 3D toggle defaults ON with mode
// Owner directive 2026-08-22: the DISPLAY-rail 3D toggle defaults ON with mode
// `proximity`, because proximity is itself the budget — models materialize only
// below the fleet altitude ceiling and only for the nearest MODEL_MAX in view,
// so "on" costs nothing at globe scale and `all` stays a deliberate opt-in.
+2 -2
View File
@@ -51,7 +51,7 @@ const DEFAULT_OVERLAY_HOST = Object.freeze({
});
/**
* Build the validated local-infrastructure card copy.
* Build the owner-approved local-infrastructure card copy.
* @param {object} properties Unwrapped GeoJSON feature properties.
* @param {string} layerId Local layer id.
* @returns {{title:string,details:string[]}}
@@ -317,7 +317,7 @@ export function createLocalGeoJsonLayer({
* camera is parked. One timer for the whole layer (not per record) — the
* retry pass walks every record anyway. (perf rebase 2026-08-17)
*
* Two gates keep this from becoming an idle leak (second review):
* Two gates keep this from becoming an idle leak (review round 2):
* - CAPABILITY: without `scene.sampleHeightSupported` the sample can never
* succeed, so a timer here would re-arm on every requested frame,
* forever. Records simply stay at ellipsoid height — exactly the
+2 -2
View File
@@ -149,7 +149,7 @@ async function createRealLocalLayerHarness({
};
}
test('local infrastructure card copy uses the validated source fields', () => {
test('local infrastructure card copy uses the owner-approved source fields', () => {
assert.deepEqual(localInfrastructureOverlayCopy({
tags: {
name: 'DFW-1',
@@ -740,7 +740,7 @@ test('disable cancels a pending ground-retry render', async (t) => {
);
});
// ── The retry must be able to STOP (second review) ───────────────────────────
// ── The retry must be able to STOP (review round 2) ───────────────────────────
//
// The retry above arms itself off its own requested frame, so anything that
// makes the sample permanently impossible turns it into a perpetual-motion
+4 -3
View File
@@ -29,7 +29,7 @@ const MAX_SAMPLES_PER_CALL = 40;
/** @constant {number} Only cells within this range of the viewer subpoint are
* sampled. Round-5 hardening: tightened 40 → 15 km — beyond the streamed
* fine-LOD area a probe returns COARSE-tile heights (not undefined), and the
* one-shot latch made those permanent (follow-up round 5: grounded planes stuck
* one-shot latch made those permanent (owner round 5: grounded planes stuck
* at a uniform wrong height). */
const MAX_SAMPLE_DIST_KM = 15;
/** @constant {number} No sampling when the camera is above this height — the
@@ -155,8 +155,9 @@ export function sampleMeshFloorCells(scene, points, { excludeObjects = [], viewe
// the sample point: walk the tileset tree to the tile containing the
// coordinate and require its `geometricError` to be below a threshold tied
// to the accuracy the caller needs, before trusting the sample. That is
// feasible and deliberately deferred until rendered-mesh sampling can be
// validated consistently across all consumers.
// feasible and deliberately deferred — see the P2 in
// the project roadmap, which bundles it with the fly_route probe so
// one post-launch pass settles rendered-mesh sampling everywhere.
reportValidatedMeshFloorCell(cell.lat, cell.lon, height);
}
}
+2 -2
View File
@@ -28,7 +28,7 @@ const DEFERRED_DEPENDENCIES = ['ais-live-vessels', 'military-installations'];
const DEPENDENCIES = [...AIRCRAFT_DEPENDENCIES, ...DEFERRED_DEPENDENCIES];
const AWARENESS_REFRESH_MS = 750;
/** @constant {number} Refresh cadence while the camera pose is CHANGING.
* Field test 2026-08-18: the Contacts direction arrows and card readouts
* Owner playtest 2026-08-18: the Contacts direction arrows and card readouts
* "feel sluggish when you look around" — at the parked 750 ms cadence the
* arrows lag the view by up to three quarters of a second. */
const AWARENESS_MOTION_REFRESH_MS = 175;
@@ -444,7 +444,7 @@ export function buildAwarenessContextSnapshot(results, navigation = {}, { subjec
* They used to be separate. The panel read live billboard positions through
* `getNearby` with a 20 000 cap; the analyst re-derived its own answer from
* last-fix coordinates over a 2 000-record slice. Same question, same centre,
* two numbers — and in the live trial the spoken answer (15) and the panel
* two numbers — and in the owner's trial the spoken answer (15) and the panel
* (111) disagreed badly enough that the model narrated the difference away.
* Routing both through here makes them the same number BY CONSTRUCTION, so
* they cannot drift again.
+2 -2
View File
@@ -1080,7 +1080,7 @@ test('awareness clears are scoped to the selected source layer', () => {
});
// ===========================================================================
// BEGIN Contact-readout presence block.
// BEGIN Contact-readout presence block — fix/context-panel-next-subjects.
// Integrators: this whole delimited block belongs to the Contact-panel
// CONTACT LOST work. Keep it intact and keep any concurrent branch's own
// additions at the END of the file, so the two never collide.
@@ -1483,7 +1483,7 @@ test('production eviction sites actually tag their clears', () => {
});
// ===========================================================================
// END Contact-readout presence block.
// END Contact-readout presence block — fix/context-panel-next-subjects.
// ===========================================================================
test('cockpit blocks only non-aircraft Context camera flights', () => {
+16 -16
View File
@@ -95,7 +95,7 @@ const MIL_ICON_COLOR = Cesium.Color.fromCssColorString('#FFB800');
/** @constant {Cesium.Color} Lighter amber tint applied to the actively tracked aircraft */
const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
// --- Ground traffic (product change 2026-07-03; mirror of flights.js) ---------------
// --- Ground traffic (owner reversal 2026-07-03; mirror of flights.js) ---------------
// adsb.lol/readsb flags ground traffic with alt_baro === "ground" (no separate
// boolean). Such aircraft are RENDERED instead of floating at the 3 km altitude
// fallback: same silhouette + rotation pipeline, clickable/trackable/detectable,
@@ -105,7 +105,7 @@ const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
// decision 2026-07-03 — no air/ground distinction), placed by the one-shot
// ground snap (see _modelDisplayPosition).
//
// TINT: full-strength amber, same as airborne (validated behavior 2026-07-03 field
// TINT: full-strength amber, same as airborne (owner verdict 2026-07-03 field
// test: the day-1 slate-gray 50%-alpha muted tint was unreadable — "in NYC I can
// barely see them"). "On the ground" reads from the ×0.8 scale + missing trail;
// "feed-dropped, coasting" stays the 45%-alpha stale fade.
@@ -113,7 +113,7 @@ const TRACKED_ICON_COLOR = Cesium.Color.fromCssColorString('#FFD166');
const GROUND_SCALE = 0.8;
/** Depth-test policy for aircraft billboards (mirror of flights.js — see the
* full rationale there). Round 5 (product invariant 2026-07-06): EVERY contact
* full rationale there). Round 5 (owner directive 2026-07-06): EVERY contact
* renders depth-test-free at every distance — a uniform always-visible rule;
* the fleet tick's horizon occluder still removes far-side contacts. */
function _groundDepthDistance() {
@@ -131,7 +131,7 @@ const JET_MODEL_URL = '/models/jet.glb';
const MODEL_ALT_CEIL_M = 800000; // m: below this camera altitude, draw 3D models (raised so it's easy to trigger)
const MODEL_MIN_PX = 24; // floor so distant models stay visible without ballooning into a min-pixel blob (mirror of flights.js, whose models now share this layer's ~30 m world size)
const TRACKED_MODEL_MIN_PX = 40; // keep the glTF silhouette comparable to the selected 2D glyph at handoff
export const TRACKED_MODEL_MAX_PX = 200; // selected close-range tracked-target feel
export const TRACKED_MODEL_MAX_PX = 200; // owner-selected close-range tracked-target feel
const MODEL_NATIVE_RADIUS_M = 29.83;
// jet.glb is transform-applied at real-world scale — native bounding radius
// 29.83 m at scale 1. ×1 → ~22–43 m aircraft across CLASS_SCALE_3D, matching
@@ -164,14 +164,14 @@ const PLANE_MODEL_URL = '/models/airplane.glb';
const PLANE_MODEL_SCALE = 1;
const PLANE_NATIVE_RADIUS_M = 34.41;
const PLANE_BELLY_OFFSET_NATIVE = 6.719;
/** Per-class model spec for THIS layer (2026-08-16, field test ask:
/** Per-class model spec for THIS layer (2026-08-16, owner playtest ask:
* military contacts should read as their WEIGHT CLASS, always in this layer's
* amber). Real Hangar GLBs serve the classes they cover (meters, nose −X →
* 180° offset); airliner/quadjet/glider get the shared 747 silhouette
* (airplane.glb — C-5M/RC-135-style heavies stop rendering as bizjets);
* fastjet and unknown keep jet.glb with the same 180° offset. The MIX tint stays
* dominant everywhere — military is amber, tracked is TRACKED_ICON_COLOR,
* and the tint must dominate any livery. Specs are
* and the tint must dominate any livery (owner: "stays yellow"). Specs are
* static per class — memoized (the fleet pass asks at 12 Hz per model). */
const _specCache = new Map();
function _modelSpec(klass) {
@@ -213,7 +213,7 @@ const _modelGen = new Map();
/** Lifecycle epoch; bumped on destroy so an in-flight load from a PREVIOUS init can't settle
* against a new lifecycle's globals (which destroy cleared). Captured by _ensureModel. */
let _modelEpoch = 0;
/** DEFAULT-ON in PROXIMITY (product invariant 2026-08-22). A fresh boot never runs
/** DEFAULT-ON in PROXIMITY (owner directive 2026-08-22). A fresh boot never runs
* layer-state restoration, so this initializer — not the codec — is what the app
* actually starts with; it must stay in lockstep with the `models3d` default in
* `layerState.js` and `this._models3dEnabled` in ui.js, or the DISPLAY rail would
@@ -384,7 +384,7 @@ const TRACKED_BILLBOARD_SCALE_BY_DISTANCE = new Cesium.NearFarScalar(
function _normalBillboardScaleByDistance() {
// Match commercial flights and preserve the established close-range 3×
// default. Smaller user-visible scaling must be proposed separately.
// default. Smaller owner-visible scaling must be proposed separately.
return new Cesium.NearFarScalar(1000, 3.0, 8000000, 0.5);
}
@@ -515,7 +515,7 @@ let _trailBackfillToken = 0;
const RENDER_DELAY_SEC = 15;
/** @constant {number} Polls an aircraft may miss before removal (transient adsb.lol dropouts). */
const MISSING_POLL_LIMIT = 3;
// --- Landed-plane fast cull (mirror of flights.js; field report
// --- Landed-plane fast cull (mirror of flights.js; owner field report
// 2026-07-02: "phantom" planes lingered ~2 min at airports after touchdown).
// The feed's ground flag lags the actual landing, so a landed plane's last
// airborne fixes show it low + slow on the runway; when it then drops out of
@@ -1357,11 +1357,11 @@ function _noteTrackedModelLoadFailure(url, err) {
/**
* The TRACKED aircraft's own model regime — DEFAULT-ON, camera-distance driven
* (product invariant 2026-08-19). Mirror of flights.js: this does NOT consult the
* (owner directive 2026-08-19). Mirror of flights.js: this does NOT consult the
* DISPLAY-rail `models3d` toggle, which keeps owning the FLEET
* (`_modelRegimeActive`) and its draw-call budget. Thresholds + hysteresis live
* in trackedModelRegime.js — enter at TRACKED_MODEL_ENTER_ALT_M (150_000 m, the
* playtested swap distance, deliberately NEARER than the fleet's 800 km
* owner's playtested swap distance, deliberately NEARER than the fleet's 800 km
* ceiling this used to inherit), hand back only above
* TRACKED_MODEL_EXIT_ALT_M so a boundary orbit cannot flap billboard↔model.
* First-person means your own airframe is not drawn in cockpit — the eye sits
@@ -1839,7 +1839,7 @@ function _fleetTick() {
// 3D-model eligibility: by DISTANCE (mode's add/keep band) with ON-SCREEN PRIORITY under the cap —
// mirror of flights.js. FOUR visible-first passes: (1) KEEP on-screen modeled; (2) ADD on-screen
// new in add radius; (3) KEEP off-screen modeled; (4) ADD off-screen new with leftover slots. KEEP
// is split by frustum so an off-screen retained model can't starve an on-screen plane (review).
// is split by frustum so an off-screen retained model can't starve an on-screen plane (review finding).
let modelEligible = null;
if (useModels) {
const cap = _modelCap();
@@ -1856,7 +1856,7 @@ function _fleetTick() {
// it must not occupy a CAP SLOT either (mirror of flights.js) — excluded
// at selection time, not just at the handoff below.
if (isTr3b(icao)) continue;
// Ground planes compete for model slots like everyone else (product rule
// Ground planes compete for model slots like everyone else (owner decision
// 2026-07-03, mirror of flights.js — no air/ground distinction; grounded
// placement is handled by the one-shot ground snap in _modelDisplayPosition).
const d2 = Cesium.Cartesian3.distanceSquared(camPos, bb.position);
@@ -2235,7 +2235,7 @@ async function _backfillTrail(icao24, token, oldestFixEpochSec) {
// 'ground'/null point sits ON the local surface — the old fixed 50 m
// sentinel rendered ~1.5 km underground at Kirtland AFB (field ~1590 m
// ellipsoidal) and dragged the whole pattern-work loop with it.
// Round-2 fix: the
// Round-2 fix (owner: "trails suddenly much shorter / not loading"): the
// resolve is BOUNDED (≤1.2 s), not a blocking await — a cold Re:Earth
// lookup across a long path could stall the paint for seconds-to-timeout.
// Paint with whatever cells are warm; the resolve keeps filling the cache
@@ -3193,7 +3193,7 @@ const militaryFlightsLayer = {
// is about to delete (billboard restore, DR cache reset), and we must
// never leave the camera mid-follow with stale tracking state. The
// camera is then RELEASED IN PLACE — it stays where the follow left
// it, fully free (product rule 2026-07-02: no overview flyTo).
// it, fully free (owner decision 2026-07-02: no overview flyTo).
if (icao24 === _trackedIcao) {
_clearTracking(false, { evicted: true });
}
@@ -3549,7 +3549,7 @@ const militaryFlightsLayer = {
// card, in the analyst's answer, and in the Contacts list. Matching only
// callsigns meant "follow 6606" — and the analyst → track_entity handoff
// the tool instructions prescribe — answered "nothing matched" for the
// very identity the app had just shown (field session 2026-08-21,
// very identity the app had just shown (owner field session 2026-08-21,
// 23:48: three failed track_entity retries before a fallback stuck).
// Ranking is shared with the flights layer (contactMatch.js) so the two
// cannot disagree, and it is strictly tiered so a registration can never
+1 -1
View File
@@ -449,7 +449,7 @@ test('real military track path creates no native label and publishes every cache
* and then hand that identity to track_entity. The analyst's `id` is a DISPLAY
* label — callsign, else registration, else hex — while the lookup matched
* callsigns and hex only, so a callsign-less contact came back as its tail
* number and "Nothing matched" (field session 2026-08-21, 23:48: three
* number and "Nothing matched" (owner field session 2026-08-21, 23:48: three
* failed retries before a fallback stuck).
*/
test('a callsign-less contact is findable by the tail number the app displays', () => {
+1 -1
View File
@@ -13,7 +13,7 @@ const CLASS_BY_MILITARY_TAG = {
};
/**
* How an UNNAMED feature reads on the map (field test 2026-08-18: the old
* How an UNNAMED feature reads on the map (owner playtest 2026-08-18: the old
* fallback surfaced "range (10981656305)" — an OSM primary key shown to a human
* as if it were a place name).
*
+1 -1
View File
@@ -309,7 +309,7 @@ function renderRecords() {
* `resolveGroundFloorCellsBounded` gives up after FLOOR_RESOLVE_DEADLINE_MS so
* a cold DEM can never hold the dots hostage — but the resolve keeps running
* and lands seconds later, and without this the records it covers stay pinned
* at ellipsoid height 0, sitting visibly under the 3D tiles (field test
* at ellipsoid height 0, sitting visibly under the 3D tiles (owner playtest
* 2026-08-18: "orange dots at the bottom").
*
* This is the render -> warm -> re-render chain FIRMS already uses, with one
+2 -2
View File
@@ -763,8 +763,8 @@ test('the trail head grows continuously across the envelope, never in one step',
// heading then rotated into world space, putting the trail out to one side and
// flipping which side as the course changed.
//
// A single-heading harness cannot catch this: at one heading a wrongly-framed
// offset can coincidentally point
// A single-heading harness cannot catch this, which is exactly how it reached
// the owner: at one heading a wrongly-framed offset can coincidentally point
// aft. So this sweeps headings and every shipped asset, and asserts the
// property directly — the anchor lies in the model's longitudinal/vertical
// plane, with NO lateral component.
+1 -1
View File
@@ -88,7 +88,7 @@ export function courseBetweenCartesians(from, to, minChordM = 25) {
* track only (chord weight 0). ≈30 kt. */
export const COURSE_TRACK_ONLY_MPS = 15.4;
/** At/above this displayed ground speed the course source is the chord only
* (weight 1) — the field-validated regime; behavior is unchanged there.
* (weight 1) — the regime the owner field-approved; behavior unchanged there.
* ≈50 kt. */
export const COURSE_CHORD_ONLY_MPS = 25.7;
/** Below this displayed speed neither chord nor reported track means anything
+1 -1
View File
@@ -3,7 +3,7 @@
* neighborhood boundaries that OSM tags as label-nodes-only (Chinatown, the Marina, the
* Mission, …). It sits AHEAD of the live-Overpass / synthesis path in the resolver, so
* covered neighborhoods resolve instantly to a REAL boundary with no network dependency
* because the live Overpass path is slow and inconsistent for neighborhoods.
* (the live-Overpass path is slow/flaky for neighborhoods — see docs/field-test-2-analysis.md).
*
* Source-agnostic: each city is a `{name, geometry}` GeoJSON file in
* `local_data/neighborhoods/`. Swap the file (e.g. to public-domain DataSF) without
+13
View File
@@ -4,6 +4,7 @@ import createViteConfig, {
adsbLolFallbackAnchor,
coalesceProxyRequest,
launchLibraryRequestHeaders,
keylessGooglePlacesResponse,
LL2_CACHE_TTL_MS,
readResponseJsonCapped,
regionalBriefHasAnySource,
@@ -11,6 +12,18 @@ import createViteConfig, {
validRegionalPoint,
} from '../../vite.config.js';
test('missing Google place context is a quiet keyless capability, not a 503', () => {
assert.deepEqual(keylessGooglePlacesResponse(undefined), {
statusCode: 200,
payload: { configured: false, error: null, places: [] },
});
assert.deepEqual(keylessGooglePlacesResponse(' '), {
statusCode: 200,
payload: { configured: false, error: null, places: [] },
});
assert.equal(keylessGooglePlacesResponse('configured-key'), null);
});
test('regional proxy rejects absent and blank coordinates instead of coercing them to zero', () => {
assert.equal(validRegionalPoint(new URLSearchParams('longitude=12.5')), null);
assert.equal(validRegionalPoint(new URLSearchParams('latitude=12.5')), null);
+2 -2
View File
@@ -1,5 +1,5 @@
// src/data/renderAltitude.js — pure priority-chain helper for aircraft render
// altitude.
// altitude (docs/plans/2026-07-05-entity-height-datum-fix.md Task 6).
//
// The globe needs ELLIPSOIDAL height (h = H + N). OpenSky's `geo_altitude`
// (state-vector index 13) is already WGS84 geometric/ellipsoidal — the
@@ -78,7 +78,7 @@ export function reuseGroundedSurfaceM(currentM, previousM) {
* contact's cells went cold, and this guess overwrote a height that had been
* sitting correctly on the mesh — dropping it through the ground until the
* proxy recovered. With `priorRenderM` present the caller's existing sticky
* fallback holds that height instead, which is what product behavior requires for:
* fallback holds that height instead, which is what the owner asked for:
* "hold the last known altitude until a fresh one comes in."
*
* @param {object} params
+2 -1
View File
@@ -1,4 +1,5 @@
// src/data/renderAltitude.test.mjs
// docs/plans/2026-07-05-entity-height-datum-fix.md Task 6.
//
// Locks pickRenderAltitudeM's priority chain — the SINGLE source of truth for
// where a flights-layer aircraft (and its dead-reckoned/tracked-camera
@@ -182,7 +183,7 @@ test('taxiing aircraft over nonzero terrain resolves every poll after the first
// ---------------------------------------------------------------------------
// geoidSurfaceLastResortM — the geoid guess must never outrank what the
// contact already knows (field incident 2026-08-21: a Re:Earth outage plus
// contact already knows (owner incident 2026-08-21: a Re:Earth outage plus
// this guess dropped a parked contact through the mesh at a Texas field).
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -162,7 +162,7 @@ test('selected satellite params survive delayed arrival and yield to newer expli
test('a tracked docked cluster consolidates its companions onto one card', () => {
// ISS and everything berthed to it are separate real tracks at one position,
// so their ambient labels stack underneath the tracked card. Product decision:
// so their ambient labels stack underneath the tracked card. Owner ruling:
// consolidate them as secondary info on that card, and suppress only those
// members — never unrelated satellites that merely happen to be nearby.
const entity = { gevLabelModel: { title: 'OLD', details: ['? km'] } };
+1 -1
View File
@@ -30,7 +30,7 @@ test('the guard is an Earth-sized band, not just a non-zero check', () => {
// Earth actually occupies, because the values BETWEEN "zero" and "the globe"
// are the ones that fail quietly.
const belowTheFloor = [
// the regression probe: finite, non-zero, and 6,378 km underground. A bare
// The adversarial probe: finite, non-zero, and 6,378 km underground. A bare
// non-zero check accepts it, and the shipped path then reverse-geocodes
// 0°, 0° as if the operator were looking at the Gulf of Guinea.
new Cesium.Cartesian3(500, 0, 0),
+5 -3
View File
@@ -54,8 +54,10 @@ export const CABLE_SWEEP_MOTION_EPSILON_M = 250;
* layer alone cost ~9.5 ms/frame during camera motion. The depth cue this
* trades away (photoreal tiles occluding label TEXT at low, city-level
* cameras) matches the sibling dams/datacenters sources, which shipped
* host-composited under the same rule; the anchor points/stems remain
* Cesium-native and depth-tested.
* host-composited under the same ruling; the anchor points/stems remain
* Cesium-native and depth-tested. See
* the world-overlay consolidation design notes ("Depth-testing
* decision") for both dated decisions.
*/
export const CABLE_LABEL_DEPTH_DECISION = Object.freeze({
option: 2,
@@ -207,7 +209,7 @@ export function createCableOverlayPublisher({
* real `MAP_STACKS` so the omission is caught loudly.
*/
const CABLE_GLOBE_STACK_IDS = Object.freeze(
new Set(['bing-aerial', 'bing-labels', 'osm']),
new Set(['bing-aerial', 'bing-labels', 'esri-imagery', 'osm']),
);
/**
@@ -151,7 +151,7 @@ test('cable ground lines classify against exactly the active surface on every st
Cesium.ClassificationType.CESIUM_3D_TILE,
);
// Every globe stack renders imagery on the shown globe — terrain pass only.
for (const stackId of ['bing-aerial', 'bing-labels', 'osm']) {
for (const stackId of ['bing-aerial', 'bing-labels', 'esri-imagery', 'osm']) {
assert.equal(
cableClassificationTypeForStack(stackId),
Cesium.ClassificationType.TERRAIN,
+2 -1
View File
@@ -1,4 +1,5 @@
// src/data/terrainHeights.js — batched, cached client terrain-height resolver.
// src/data/terrainHeights.js — batched, cached client terrain-height resolver
// (docs/plans/2026-07-05-entity-height-datum-fix.md Task 3).
//
// Resolves ELLIPSOIDAL ground height per (lat, lon) via the server-side
// `/api/terrain/heights` proxy (Task 2 — Re:Earth `heights.json`, disk-cached,
+2 -2
View File
@@ -1,5 +1,5 @@
// src/data/terrainHeights.test.mjs — batched, cached client terrain-height
// resolver.
// resolver (docs/plans/2026-07-05-entity-height-datum-fix.md Task 3).
//
// Locks the module's public interface (Task 5/6 call it verbatim):
// resolveEllipsoidalGround(coords: [{lat, lon, sourceOrthometricM?}])
@@ -22,7 +22,7 @@ import {
} from './terrainHeights.js';
const AUSTIN = { lat: 30.2672, lon: -97.7431 };
const AUSTIN_GEOID_N = -26.9; // Cross-checked against Re:Earth reference data.
const AUSTIN_GEOID_N = -26.9; // docs/plans/2026-07-05-entity-height-datum-fix.md verified facts
/** Installs a fake fetch for the duration of `fn`, restoring the original after. */
async function withFakeFetch(fakeFetch, fn) {
+4 -4
View File
@@ -3,18 +3,18 @@
*
* The fleet's 3D models stay behind the DISPLAY-rail "3D" toggle (`models3d`)
* — hundreds of GLBs are a draw-call budget decision the operator owns. Since
* 2026-08-22 that toggle DEFAULTS ON in `proximity` (product invariant), so the
* 2026-08-22 that toggle DEFAULTS ON in `proximity` (owner directive), so the
* fleet is armed on a fresh boot; proximity is itself the budget, admitting only
* the nearest MODEL_MAX in view below the fleet ceiling. The contact you have
* SELECTED is still a separate case: it is exactly one model, it is what the
* camera is pointed at, and the operator's expectation is that zooming in on a
* target resolves it into an aircraft. So the tracked contact's handoff is
* DEFAULT behaviour, driven purely by camera distance and never consulting the
* toggle at all (product invariant, 2026-08-19).
* toggle at all (owner directive, 2026-08-19).
*
* Threshold history — the tracked contact used to inherit the fleet ceiling
* `MODEL_ALT_CEIL_M = 800_000` m, and a first pass raised it to 1_000_000 m to
* make the airframe arrive sooner. Field test 2026-08-20 rejected that: the
* make the airframe arrive sooner. Owner playtest 2026-08-20 rejected that: the
* model "pops to 3D far too early" — a 26 m airframe held at its minimum pixel
* size from ~1 Mm out reads as a floating toy, not an aircraft. The ruling is a
* MUCH closer swap: 2D is correct at ~600_000 m, and the handoff belongs at
@@ -49,7 +49,7 @@
export const FLEET_MODEL_ALT_CEIL_M = 800_000;
/** Camera altitude (m) at or below which the TRACKED contact takes its 3D model.
* Field test ruling 2026-08-20: 2D still reads correctly at ~600_000 m, and
* Owner playtest ruling 2026-08-20: 2D still reads correctly at ~600_000 m, and
* the swap belongs at ~150_000 m. */
export const TRACKED_MODEL_ENTER_ALT_M = 150_000;
+5 -5
View File
@@ -1,11 +1,11 @@
// src/data/trackedModelRegime.test.mjs
//
// Zoom-driven 2D↔3D for the TRACKED contact (product invariant 2026-08-19).
// Zoom-driven 2D↔3D for the TRACKED contact (owner directive 2026-08-19).
//
// Two things are pinned here, and both are behavioural rather than cosmetic:
//
// 1. The POLICY — thresholds and hysteresis — as pure math in
// trackedModelRegime.js. The enter ceiling is the playtested swap
// trackedModelRegime.js. The enter ceiling is the owner's playtested swap
// distance, deliberately much NEARER than the fleet ceiling, and the exit
// ceiling is deliberately higher than the enter ceiling. A regression that
// collapsed the two thresholds back into one would silently reintroduce
@@ -62,8 +62,8 @@ const WELL_INSIDE_M = TRACKED_MODEL_ENTER_ALT_M / 2;
// 1. Policy: thresholds + hysteresis math
// ---------------------------------------------------------------------------
test('the tracked 3D takeover sits at the playtested swap distance', () => {
// Field test 2026-08-20: an earlier 1_000_000 m ceiling "pops to 3D far
test('the tracked 3D takeover sits at the owner-playtested swap distance', () => {
// Owner playtest 2026-08-20: an earlier 1_000_000 m ceiling "pops to 3D far
// too early" — 2D still reads correctly at ~600_000 m and the swap belongs at
// ~150_000 m. These are the numbers the operator judged by eye, so they are
// pinned literally rather than derived from anything.
@@ -75,7 +75,7 @@ test('the tracked 3D takeover sits at the playtested swap distance', () => {
});
test('the tracked contact swaps NEARER than the fleet — a recorded inversion, not a bug', () => {
// Consequence of the selected threshold, spelled out so it cannot be "tidied
// Consequence of the owner's number, spelled out so it cannot be "tidied
// away": with the DISPLAY-rail 3D toggle ON, camera altitudes between the
// tracked ceiling and the fleet ceiling draw surrounding contacts as models
// while the SELECTED one is still a billboard. The fleet pass skips the
+1 -1
View File
@@ -124,7 +124,7 @@ test('civilian and military click handlers apply duration only at the deselect b
);
});
test('civilian and military tracked model caps both expose the selected 200 px feel', () => {
test('civilian and military tracked model caps both expose the owner-selected 200 px feel', () => {
assert.equal(CIVIL_TRACKED_MODEL_MAX_PX, 200);
assert.equal(MILITARY_TRACKED_MODEL_MAX_PX, 200);
});
+7 -7
View File
@@ -223,7 +223,7 @@ let _uncoveredMode = 'sim';
* + stop-and-go creep; 'heatline' = congestion corridor polylines; 'both';
* 'none' = shipped main behavior. Live mode only — the keyless simulation
* never has `road.flow`, so every jamViz path is unreachable there.
* Default 'density' — A/B verdict 2026-07-23 (heatline stays available
* Default 'density' — owner A/B verdict 2026-07-23 (heatline stays available
* via setParams).
* @type {'none'|'density'|'heatline'|'both'}
*/
@@ -270,7 +270,7 @@ let _activeBucketColors = { ...FLOW_BUCKET_COLORS };
/**
* @const {number} Minimum base pixel size for COLORED dots while a styled
* preset is active — residential-road dots spawn at 4 px and vanish into
* post-FX pixelation; presence is the dots' whole job there (follow-up round
* post-FX pixelation; presence is the dots' whole job there (owner round
* 2). Sim dots and the normal profile keep SIZE_BY_TYPE untouched.
*/
const STYLED_MIN_BASE_PX = 5;
@@ -1253,7 +1253,7 @@ export function trafficFeedPresentation({
const mode = liveMode ? 'live' : 'sim';
if (liveMode && flowError) {
// One string for both fields. The manager's meta line renders `error` and
// drops `loadingLabel` in its error branch, so the SIMULATED copy
// drops `loadingLabel` in its error branch, so the owner's SIMULATED copy
// has to BE the error text or the steady state reverts to a bare
// "TomTom daily budget reached" that never says what is on screen.
const degraded = `SIMULATED — ${flowError}`;
@@ -1269,7 +1269,7 @@ export function trafficFeedPresentation({
};
}
// Keyless simulation — one terse line that names the mode and the remedy
// (the copy shape). The chip's own progress text carries "working";
// (owner's copy shape). The chip's own progress text carries "working";
// this line must never imply a live feed.
return {
mode,
@@ -2351,13 +2351,13 @@ const trafficLayer = {
if (params.uncoveredRoads === 'sim' || params.uncoveredRoads === 'hide') {
_uncoveredMode = params.uncoveredRoads;
}
// Jam-viz prototype toggle (A/B): 'none' = shipped main behavior;
// Jam-viz prototype toggle (owner A/B): 'none' = shipped main behavior;
// live mode only, applies on the next camera-driven load like the
// uncoveredRoads param above.
if (['none', 'density', 'heatline', 'both'].includes(params.jamViz)) {
_jamViz = params.jamViz;
}
// Preset-aware dot styling kill switch (A/B): 'off' forces the
// Preset-aware dot styling kill switch (owner A/B): 'off' forces the
// shipped palette under every post-FX preset. Applies immediately via
// in-place restyle — no refetch — so A/B legs share identical dots.
if (params.presetDots === 'on' || params.presetDots === 'off') {
@@ -2415,7 +2415,7 @@ const trafficLayer = {
};
// Live mode: the detection bracket carries the congestion signal —
// its canvas sits ABOVE the post-FX chain, so tier colors survive
// every preset (follow-up round 2: "bounding boxes do the heavy
// every preset (owner round 2: "bounding boxes do the heavy
// lifting"). Keyless mode sets no tier: contacts keep the stock
// 'vehicle' bracket and the keyless experience stays untouched.
if (_liveMode) {
+1 -1
View File
@@ -10,7 +10,7 @@
* 0.72 / jam 0.49 — under NVG a jam renders DIMMER than free flow. CRT
* keeps hue but its 5-px pixelation + dithering shred 4–6 px dots.
*
* Encoding per profile (validated behavior 2026-07-23 round 2: "just bright
* Encoding per profile (owner verdict 2026-07-23 round 2: "just bright
* dots" — a luminance RAMP failed in the field because dim free-flow dots
* read as dark holes on NVG-bright roads; classification is the detection
* brackets' job via `trafficBucketTier`, presence is the dots' job):
+1 -1
View File
@@ -39,7 +39,7 @@ test('sim/uncovered dots (null bucket) are untouched under EVERY style — keyle
}
});
test('mono: EVERY colored dot is a bright white core (follow-up round 2: "just bright dots")', () => {
test('mono: EVERY colored dot is a bright white core (owner round 2: "just bright dots")', () => {
for (const s of MONO_STYLES) {
for (const b of BUCKETS) {
const rgba = presetDotRgba(s, b);
+5 -4
View File
@@ -4,7 +4,7 @@
* 2026-07-06).
*
* One trail = one ENTITY polyline. Round 6 replaced the faded per-vertex
* Primitive for two product invariants from the field:
* Primitive for two owner directives from the field:
* - "the line must ALWAYS be visible": the Primitive's depthFailAppearance
* did not reliably render segments below the photoreal mesh — entity
* polylines with `depthFailMaterial` DO (in-repo proof: CCTV's frustum
@@ -36,7 +36,7 @@ let _trailSeq = 0;
/** @constant {number} Alpha where the trail passes the depth test. */
const TRAIL_ALPHA = 0.85;
/** @constant {number} Alpha where the trail is behind/below scene geometry —
* still visible, but readable as occluded. */
* visible (owner: never vanish) but readable as occluded. */
const TRAIL_OCCLUDED_ALPHA = 0.4;
/** @constant {number} Squared distance (m^2) below which consecutive points are merged. */
const MIN_SEGMENT_DISTANCE_SQ = 0.01;
@@ -72,10 +72,11 @@ export function createTrail(viewer, { color, width = 2.5 }) {
positions: new Cesium.CallbackProperty(() => current, false),
width,
material: baseColor.withAlpha(TRAIL_ALPHA),
// The locked rule (round 6): a segment below the photoreal
// The owner-locked rule (round 6): a segment below the photoreal
// mesh renders dimmed — it must never disappear into the ground.
depthFailMaterial: baseColor.withAlpha(TRAIL_OCCLUDED_ALPHA),
// Round 8: NONE draws straight 3D chords between waypoints — over a
// Round 8 (owner: UAL1104's Pacific trail "cutting through the
// globe"): NONE draws straight 3D chords between waypoints — over a
// sparse trans-oceanic trace a single segment spans hundreds of km
// and tunnels through the planet. GEODESIC subdivides each segment
// along the curved surface (heights interpolated), so long legs hug
+23
View File
@@ -56,3 +56,26 @@ test('an inherited export never masks the value written in the file', async () =
await fs.rm(root, { recursive: true, force: true });
}
});
test('dev-fresh gives explicit or dotenv Google configuration precedence over Keychain', async () => {
const source = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8');
const precedence = source.indexOf('if [[ -n "${GOOGLE_MAPS_API_KEY_ENV}" ]]');
const keychainFallback = source.indexOf('elif [[ -n "${GOOGLE_MAPS_API_KEY_KEYCHAIN}" ]]');
assert.ok(precedence >= 0 && keychainFallback > precedence);
});
test('dev-fresh passes names-only boot provenance before resolving file fallbacks', async () => {
const source = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8');
const capture = source.indexOf('KEY_SETUP_EXTERNAL_KEYS=()');
const dotenvResolution = source.indexOf('GOOGLE_MAPS_API_KEY_ENV="${GOOGLE_MAPS_API_KEY:-}"');
assert.ok(capture >= 0 && capture < dotenvResolution, 'parent-shell provenance must be captured first');
for (const name of [
'GOOGLE_MAPS_API_KEY', 'CESIUM_ION_TOKEN', 'OPENAI_API_KEY', 'AISSTREAM_API_KEY',
'FIRMS_MAP_KEY', 'TOMTOM_API_KEY', 'OPENSKY_CLIENT_ID',
'OPENSKY_CLIENT_SECRET', 'LL2_API_TOKEN',
]) {
assert.match(source, new RegExp(`KEY_SETUP_EXTERNAL_KEYS\\+=\\(${name}\\)`));
}
assert.match(source, /put_env GEV_LAUNCHER "dev-fresh"/);
assert.match(source, /put_env GEV_KEY_SETUP_EXTERNAL_KEYS "\$\{KEY_SETUP_EXTERNAL_KEYS_CSV\}"/);
});
+5 -5
View File
@@ -4,7 +4,7 @@
// would spend optional API quotas, surprise returning operators, and fight share
// links. A new visitor instead gets one compact, explicit choice after startup.
//
// SHOW POLICY (product decision, 2026-08-23). The launcher is NOT one-shot. A new
// SHOW POLICY (owner ruling, 2026-08-23). The launcher is NOT one-shot. A new
// operator needs the map explained more than once, so it returns every fresh
// browser session until they say otherwise:
//
@@ -25,7 +25,7 @@ export const FIRST_RUN_STORAGE_KEY = 'gev:first-run-mission:v1';
export const FIRST_RUN_SESSION_KEY = 'gev:first-run-mission-session:v1';
/**
* Configurable name for the fires/quakes mission. Flip this ONE constant to
* Owner-selectable name for the fires/quakes mission. Flip this ONE constant to
* re-label the tile; the alternates are pre-written so the choice is a taste
* call at review time, not an edit.
* @type {'ENVIRONMENTAL'|'EARTH_WATCH'|'ACTIVE_EVENTS'}
@@ -49,7 +49,7 @@ export function environmentalLabel(choice = ENVIRONMENTAL_LABEL_CHOICE) {
/*
* MISSION → APP STATE, AND WHAT IT IS ALLOWED TO PERSIST
* ─────────────────────────────────────────────────────────────────────────────
* Product decision: picking a mission carries the same weight as clicking the
* Owner ruling: picking a mission carries the same weight as clicking the
* toggles it represents — durable where those clicks are durable — but it must
* never write a preference the visitor did not effectively choose by picking it.
* Layer enablement IS durable in this app (`gev:layer-state:v2`, written by
@@ -100,7 +100,7 @@ export const FIRST_RUN_MISSIONS = Object.freeze({
environmental: Object.freeze({
kind: 'globe',
// Live USGS earthquakes AND NASA FIRMS active fires. The launcher optimizes
// for the FULLY CONFIGURED experience (product decision, 2026-08-23): the tile
// for the FULLY CONFIGURED experience (owner ruling, 2026-08-23): the tile
// promises both, so it turns on both, and the subcopy in index.html says so.
//
// Keyless, FIRMS is honest where it counts — its own layer row reads
@@ -333,7 +333,7 @@ export function initFirstRunExperience({
return null;
}
// The tile name is configurable from one constant, so paint it from the
// The tile name is owner-switchable from one constant, so paint it from the
// module rather than trusting the markup to have been edited to match.
const environmentalTitle = root.querySelector('[data-first-run-environmental-title]');
if (environmentalTitle) environmentalTitle.textContent = environmentalLabel().title;
+13 -6
View File
@@ -396,7 +396,7 @@ function missionSpy({ contextOk = true, layerResult = () => true, globe = async
}
test('the menu is the four owner-ordered missions', () => {
// INFRASTRUCTURE was removed after the field tested it: enabling all
// INFRASTRUCTURE was removed after the owner playtested it: enabling all
// three bundled layers at once put ~5,700 entities on a full-earth view and
// tanked the frame rate. The layers stay reachable by hand and by voice; what
// went is the one-click globe-scale dump. Restoring the tile needs the
@@ -429,7 +429,7 @@ test('Environmental enables BOTH its feeds and pulls out to the globe', async ()
});
test('the tile is the FULLY CONFIGURED experience: quakes and fires together', () => {
// Product decision, 2026-08-23: the launcher optimizes for the configured app, so
// Owner ruling, 2026-08-23: the launcher optimizes for the configured app, so
// ENVIRONMENTAL means live USGS earthquakes AND NASA FIRMS active fires.
const environmental = FIRST_RUN_MISSIONS.environmental;
assert.deepEqual(environmental.layerIds, ['earthquakes', 'local-firms']);
@@ -568,7 +568,7 @@ test('markup, startup ordering and accessibility remain pinned', () => {
assert.ok(
html.includes('<p id="first-run-description">It feels like a forbidden cockpit'
+ '—then you realize the sources are public and the data is real.</p>'),
'the final first-run line must ship exactly as written',
'the owner-authored first-run line must ship exactly as written',
);
// Menu order is the owner's, read straight off the markup.
@@ -605,7 +605,9 @@ test('markup, startup ordering and accessibility remain pinned', () => {
assert.match(css, /#first-run-launcher\[hidden\] \{\s*display: none;\s*\}/);
// Only the mission list may scroll: the heading, checkbox and status line
// have to stay on screen at every height.
assert.match(css, /\.first-run-choices \{[\s\S]*?min-height: 0;[\s\S]*?overflow-y: auto/);
const choicesBlock = css.match(/\.first-run-choices \{([^}]*)\}/)?.[1] || '';
assert.match(choicesBlock, /min-height: 0;/);
assert.match(choicesBlock, /overflow-y: auto;/);
});
test('the launcher keeps focus, restores it, and never disables the focused button', () => {
@@ -653,10 +655,15 @@ test('the voice TOOL SCHEMA is byte-identical to main — the mission mapping is
const end = src.indexOf('\n];\n', start);
const block = src.slice(start, end + 4);
assert.equal(block.length, 31104, 'tool schema byte length drifted from the frozen baseline');
// Re-pinned 2026-08-28: the Provider Settings / Esri release DELIBERATELY
// extends set_map_stack's enum with 'esri-imagery' (a real new basemap —
// exactly the kind of schema change this pin exists to make loud). The
// guarded claim is unchanged: first-run missions ride existing tools, and
// any NEW drift from this recorded schema still fails here.
assert.equal(block.length, 31189, 'tool schema byte length drifted from the pinned release schema');
assert.equal(
crypto.createHash('sha256').update(block).digest('hex'),
'3ace199727934e851902e4899c423d549d34d3f53469dcb56f07fc070d3f9d66',
'73aaabdb169a5478893d28688f327a21edd32ed3ec16fc6287bd944ed77beecf',
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
);
+89
View File
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { googlePlacesContextProxy, keylessGooglePlacesResponse } from '../vite.config.js';
const KEYLESS_PAYLOAD = { configured: false, error: null, places: [] };
function installGooglePlacesRoutes() {
const routes = new Map();
googlePlacesContextProxy().configureServer({
middlewares: {
use(path, handler) {
routes.set(path, handler);
},
},
});
return routes;
}
function invokeRoute(handler, { method = 'GET', url = '/', remoteAddress = '127.0.0.1' } = {}) {
return new Promise((resolve, reject) => {
const headers = new Map();
const req = {
method,
url,
headers: {},
socket: { remoteAddress },
};
const res = {
statusCode: 200,
setHeader(name, value) {
headers.set(String(name).toLowerCase(), String(value));
},
end(body = '') {
resolve({
statusCode: this.statusCode,
headers: Object.fromEntries(headers),
body: body ? JSON.parse(String(body)) : null,
});
},
};
Promise.resolve(handler(req, res)).catch(reject);
});
}
test('builds the keyless capability response only for a blank key', () => {
assert.deepEqual(keylessGooglePlacesResponse(undefined), {
statusCode: 200,
payload: KEYLESS_PAYLOAD,
});
assert.deepEqual(keylessGooglePlacesResponse(' '), {
statusCode: 200,
payload: KEYLESS_PAYLOAD,
});
assert.equal(keylessGooglePlacesResponse('configured-key'), null);
});
test('keyless Places routes stay successful after the Google quota is exhausted', async () => {
const previousKey = process.env.GOOGLE_MAPS_API_KEY;
const previousLimit = process.env.GEV_RATELIMIT_GOOGLE_PER_MIN;
process.env.GOOGLE_MAPS_API_KEY = '';
process.env.GEV_RATELIMIT_GOOGLE_PER_MIN = '1';
try {
const routes = installGooglePlacesRoutes();
const nearby = routes.get('/api/google/nearby-places');
const textSearch = routes.get('/api/google/text-search');
assert.equal(typeof nearby, 'function');
assert.equal(typeof textSearch, 'function');
// The limiter allows one request per minute; a keyless capability response
// must never consume that quota, so every request on both routes — well
// past the limit — stays the same deliberate 200.
for (let attempt = 0; attempt < 3; attempt += 1) {
const nearbyResponse = await invokeRoute(nearby, { url: '/?lat=30.27&lon=-97.74' });
assert.equal(nearbyResponse.statusCode, 200);
assert.equal(nearbyResponse.headers['cache-control'], 'no-store');
assert.deepEqual(nearbyResponse.body, KEYLESS_PAYLOAD);
const textResponse = await invokeRoute(textSearch, { url: '/?q=capitol&lat=30.27&lon=-97.74' });
assert.equal(textResponse.statusCode, 200);
assert.equal(textResponse.headers['cache-control'], 'no-store');
assert.deepEqual(textResponse.body, KEYLESS_PAYLOAD);
}
} finally {
if (previousKey === undefined) delete process.env.GOOGLE_MAPS_API_KEY;
else process.env.GOOGLE_MAPS_API_KEY = previousKey;
if (previousLimit === undefined) delete process.env.GEV_RATELIMIT_GOOGLE_PER_MIN;
else process.env.GEV_RATELIMIT_GOOGLE_PER_MIN = previousLimit;
}
});
+6 -1
View File
@@ -19,6 +19,7 @@ import { CITY_POIS } from './locations.js';
import { composeLocalityTag } from './hudLocality.js';
import { ellipsoidalToMslDisplayM, ensureGeoidReady, geoidHeight } from './data/geoid.js';
import { getBasemapLabelContext } from './voice/gevActions.js';
import { isHudSummaryUnconfigured } from './hudSummaryResponse.js';
/** Color palettes keyed by shader mode; applied as CSS custom properties. */
const HUD_COLORS = {
@@ -665,10 +666,14 @@ export class IntelHUD {
signal: controller.signal,
});
const data = await response.json().catch(() => null);
if (revision !== this._summaryRevision) return;
if (isHudSummaryUnconfigured(response.status, data)) {
this._setSummaryText(fallbackText, animate);
return;
}
if (!response.ok || !data?.summary) {
throw new Error(data?.error || `HTTP ${response.status}`);
}
if (revision !== this._summaryRevision) return;
this._setSummaryText(data.summary, animate);
} catch (error) {
if (error?.name !== 'AbortError') {
+1 -1
View File
@@ -49,7 +49,7 @@ const has = (pattern) => pattern.test(source);
/** SFO runway 28R touchdown area — the field report's coordinates. */
const SFO = { latDeg: 37.616, lonDeg: -122.368 };
/** The ellipsoidal camera height the screenshot reported. */
/** The ellipsoidal camera height the owner's screenshot reported. */
const SFO_ELLIPSOIDAL_M = -15;
/**
+35
View File
@@ -0,0 +1,35 @@
export const HUD_SUMMARY_UNCONFIGURED_CODE = 'OPENAI_NOT_CONFIGURED';
/**
* Describe the optional HUD summary capability without turning a deliberately
* keyless boot into an HTTP failure.
*
* @param {unknown} apiKey - Candidate server-side OpenAI credential.
* @returns {{ statusCode: 200, payload: { configured: false, code: string, error: null, summary: null } }|null}
* A graceful unconfigured response, or null when the provider is configured.
*/
export function keylessHudSummaryResponse(apiKey) {
if (String(apiKey ?? '').trim()) return null;
return {
statusCode: 200,
payload: {
configured: false,
code: HUD_SUMMARY_UNCONFIGURED_CODE,
error: null,
summary: null,
},
};
}
/** Return true only for the deliberate, successful no-key capability response. */
export function isHudSummaryUnconfigured(status, data) {
const keys = data !== null && typeof data === 'object' && !Array.isArray(data)
? Object.keys(data)
: [];
return keys.length === 4
&& status === 200
&& data?.configured === false
&& data?.code === HUD_SUMMARY_UNCONFIGURED_CODE
&& data?.error === null
&& data?.summary === null;
}
+130
View File
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
HUD_SUMMARY_UNCONFIGURED_CODE,
isHudSummaryUnconfigured,
keylessHudSummaryResponse,
} from './hudSummaryResponse.js';
import { openAiRealtimeProxy } from '../vite.config.js';
const UNCONFIGURED_PAYLOAD = {
configured: false,
code: HUD_SUMMARY_UNCONFIGURED_CODE,
error: null,
summary: null,
};
function installOpenAiRoutes() {
const routes = new Map();
openAiRealtimeProxy().configureServer({
middlewares: {
use(path, handler) {
routes.set(path, handler);
},
},
});
return routes;
}
function invokeRoute(handler, { method = 'GET', url = '/', remoteAddress = '127.0.0.1' } = {}) {
return new Promise((resolve, reject) => {
const headers = new Map();
const req = {
method,
url,
headers: {},
socket: { remoteAddress },
};
const res = {
statusCode: 200,
setHeader(name, value) {
headers.set(String(name).toLowerCase(), String(value));
},
end(body = '') {
resolve({
statusCode: this.statusCode,
headers: Object.fromEntries(headers),
body: body ? JSON.parse(String(body)) : null,
});
},
};
Promise.resolve(handler(req, res)).catch(reject);
});
}
test('builds an HTTP-success capability response only for a blank key', () => {
assert.deepEqual(keylessHudSummaryResponse(undefined), {
statusCode: 200,
payload: UNCONFIGURED_PAYLOAD,
});
assert.deepEqual(keylessHudSummaryResponse(' '), {
statusCode: 200,
payload: UNCONFIGURED_PAYLOAD,
});
assert.equal(keylessHudSummaryResponse('configured-key'), null);
});
test('recognizes only the exact deliberate no-key fallback response', () => {
assert.equal(isHudSummaryUnconfigured(200, UNCONFIGURED_PAYLOAD), true);
assert.equal(isHudSummaryUnconfigured(503, UNCONFIGURED_PAYLOAD), false);
assert.equal(isHudSummaryUnconfigured(200, {
...UNCONFIGURED_PAYLOAD,
error: 'provider failed',
}), false);
assert.equal(isHudSummaryUnconfigured(200, {
...UNCONFIGURED_PAYLOAD,
summary: 'Unexpected provider output',
}), false);
assert.equal(isHudSummaryUnconfigured(200, {
...UNCONFIGURED_PAYLOAD,
configured: true,
}), false);
assert.equal(isHudSummaryUnconfigured(200, {
...UNCONFIGURED_PAYLOAD,
unexpected: true,
}), false);
assert.equal(isHudSummaryUnconfigured(200, {
code: HUD_SUMMARY_UNCONFIGURED_CODE,
}), false);
});
test('does not hide real provider and HTTP failures', () => {
assert.equal(isHudSummaryUnconfigured(502, {
code: HUD_SUMMARY_UNCONFIGURED_CODE,
}), false);
assert.equal(isHudSummaryUnconfigured(503, UNCONFIGURED_PAYLOAD), false);
assert.equal(isHudSummaryUnconfigured(200, { error: 'provider failed' }), false);
});
test('the installed keyless HUD route stays successful after the voice quota is exhausted', async () => {
const previousKey = process.env.OPENAI_API_KEY;
const previousLimit = process.env.GEV_RATELIMIT_OPENAI_PER_MIN;
process.env.OPENAI_API_KEY = '';
process.env.GEV_RATELIMIT_OPENAI_PER_MIN = '1';
try {
const routes = installOpenAiRoutes();
const token = routes.get('/api/realtime/token');
const hud = routes.get('/api/openai/hud-summary');
assert.equal(typeof token, 'function');
assert.equal(typeof hud, 'function');
const firstToken = await invokeRoute(token);
const secondToken = await invokeRoute(token);
assert.equal(firstToken.statusCode, 503);
assert.deepEqual(firstToken.body, { error: 'OPENAI_API_KEY is not set' });
assert.equal(secondToken.statusCode, 429);
for (let attempt = 0; attempt < 2; attempt += 1) {
const response = await invokeRoute(hud, { method: 'POST' });
assert.equal(response.statusCode, 200);
assert.equal(response.headers['content-type'], 'application/json; charset=utf-8');
assert.equal(response.headers['cache-control'], 'no-store');
assert.deepEqual(response.body, UNCONFIGURED_PAYLOAD);
}
} finally {
if (previousKey === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = previousKey;
if (previousLimit === undefined) delete process.env.GEV_RATELIMIT_OPENAI_PER_MIN;
else process.env.GEV_RATELIMIT_OPENAI_PER_MIN = previousLimit;
}
});
+350
View File
@@ -0,0 +1,350 @@
/**
* The POWER UP surface — paste a key, get a power.
*
* A small chip sits bottom-right whenever the app is running under the dev
* server with keys still missing. It opens a dialog rendered ENTIRELY from
* GET /api/setup/status (the registry lives in src/keySetupCore.mjs and this
* module never duplicates it): one row per key, what it unlocks, where to get
* it, and a paste field. SAVE posts to /api/setup/keys, which writes the
* repo-root .env and restarts the dev server — Vite's client then reloads the
* page itself, and the pasted key is simply *on*. No hand-edited env files.
*
* The surface self-destructs where it cannot work: a prod build (no endpoint)
* or a LAN visitor (loopback-only endpoint) fails the status fetch, and both
* the chip and the dialog are removed outright.
*/
/** Chip label — pure, exported for tests. */
export function keySetupChipLabel(status) {
const missing = Math.max(0, (status?.total || 0) - (status?.setCount || 0));
return missing > 0 ? `POWER UP · ${missing} ${missing === 1 ? 'KEY' : 'KEYS'} WAITING` : 'POWERED UP';
}
/**
* Collect a POST body from field descriptors — pure, exported for tests.
* @param {Array<{envVar: string, value: string}>} fields
* @returns {Record<string, string>} non-empty trimmed values only
*/
export function collectKeyUpdates(fields) {
const updates = {};
for (const field of fields || []) {
const value = String(field?.value ?? '').trim();
if (value && field?.envVar) updates[field.envVar] = value;
}
return updates;
}
/**
* After the FIRST Google key lands, the restart's reload should boot the
* photoreal default — not faithfully restore the auto-selected keyless OSM
* basemap from the URL's live share hash. Strips only `map=osm`: a stack under
* any other name was chosen or shared on purpose and survives, and so does
* everything else in the hash (camera, style, layers). Pure, exported for tests.
* @param {string} hash Location hash without the leading '#'.
* @returns {string|null} The rewritten hash, or null when there is nothing to strip.
*/
export function stripKeylessBasemapFromHash(hash) {
if (!hash) return null;
try {
const params = new URLSearchParams(hash);
if (!['osm', 'esri-imagery'].includes(params.get('map'))) return null;
params.delete('map');
return params.toString();
} catch {
return null;
}
}
const TIER_DOTS = Object.freeze({ metered: '🔴', free: '🟡' });
/** Build one key row. All content is our own registry text, set via textContent. */
function buildRow(documentRef, key) {
const row = documentRef.createElement('section');
row.className = 'key-setup-row';
row.dataset.keyId = key.id;
row.dataset.set = String(Boolean(key.set));
if (key.managed) row.dataset.managed = key.managed;
const external = key.managed === 'external';
const head = documentRef.createElement('div');
head.className = 'key-setup-row-head';
const led = documentRef.createElement('span');
led.className = 'key-setup-led';
led.setAttribute('aria-hidden', 'true');
const title = documentRef.createElement('strong');
title.textContent = key.title;
const tier = documentRef.createElement('span');
tier.className = 'key-setup-tier';
tier.textContent = TIER_DOTS[key.tier] || '';
tier.title = key.tier === 'metered' ? 'Metered — a billing-enabled account' : 'Free key — register, paste, done';
head.append(led, title, tier);
if (key.clientExposed) {
const exposed = documentRef.createElement('span');
exposed.className = 'key-setup-exposed';
exposed.textContent = 'browser-side';
exposed.title = 'This key runs in the browser by design — restrict it at the provider (see SECURITY.md)';
head.append(exposed);
}
if (external) {
// Externally supplied credentials (shell env, Keychain, a launcher) are
// facts this panel reports, never values it rewrites or deletes.
const badge = documentRef.createElement('span');
badge.className = 'key-setup-external';
badge.textContent = 'configured externally';
badge.title = 'Supplied by your environment, Keychain, or launcher — change it where it was set';
head.append(badge);
}
const get = documentRef.createElement('a');
get.className = 'key-setup-get';
get.href = key.getUrl;
get.target = '_blank';
get.rel = 'noopener noreferrer';
get.textContent = key.set ? 'MANAGE ↗' : 'GET KEY ↗';
head.append(get);
const unlocks = documentRef.createElement('p');
unlocks.className = 'key-setup-unlocks';
unlocks.textContent = key.unlocks;
row.append(head, unlocks);
if (!external) {
const fields = documentRef.createElement('div');
fields.className = 'key-setup-fields';
for (const envVar of key.envVars) {
const input = documentRef.createElement('input');
// Passwords-style so a pasted key never shows on a shared or recorded
// screen — this app gets screen-recorded a lot.
input.type = 'password';
input.autocomplete = 'off';
input.spellcheck = false;
input.dataset.envVar = envVar;
input.setAttribute('aria-label', envVar);
input.placeholder = key.set
? `${envVar} saved — paste to replace`
: `paste ${envVar}`;
fields.append(input);
}
if (key.managed === 'file') {
const remove = documentRef.createElement('button');
remove.type = 'button';
remove.className = 'key-setup-remove';
remove.dataset.keySetupRemove = JSON.stringify(key.envVars);
remove.textContent = 'REMOVE';
remove.title = `Remove ${key.title} from this app's saved keys`;
fields.append(remove);
}
row.append(fields);
}
return row;
}
/**
* Wire the chip + dialog. Fire-and-forget from main.js; resolves to null when
* the surface has no business existing (prod build, LAN visitor, no markup).
*/
export async function initKeySetup({ documentRef = globalThis.document, fetchImpl } = {}) {
const chip = documentRef?.getElementById?.('key-setup-chip');
const root = documentRef?.getElementById?.('key-setup');
if (!chip || !root || root.dataset.initialized === 'true') return null;
root.dataset.initialized = 'true';
const doFetch = fetchImpl || globalThis.fetch?.bind(globalThis);
let status = null;
try {
const response = await doFetch('/api/setup/status', { cache: 'no-store' });
if (!response.ok) throw new Error(String(response.status));
status = await response.json();
} catch {
// Prod build or non-loopback visitor: the surface cannot function, so it
// does not exist. (The README covers .env for headless/self-host setups.)
chip.remove();
root.remove();
return null;
}
const rowsHost = root.querySelector('[data-key-setup-rows]');
const applyButton = root.querySelector('[data-key-setup-apply]');
const closeButton = root.querySelector('[data-key-setup-close]');
const chipLabel = chip.querySelector('[data-key-setup-chip-label]') || chip;
const statusLine = root.querySelector('[data-key-setup-status]');
const defaultStatusText = statusLine?.textContent || '';
let busy = false;
let open = false;
let previouslyFocused = null;
const render = (nextStatus) => {
status = nextStatus;
chipLabel.textContent = keySetupChipLabel(status);
// Fully powered is the owner's clean screen: the chip retires. The dialog
// stays reachable this session (and via ?setup=1) to swap or verify keys.
chip.hidden = status.setCount >= status.total;
if (!rowsHost) return;
rowsHost.textContent = '';
for (const key of status.keys || []) rowsHost.append(buildRow(documentRef, key));
};
const visible = () => root.isConnected
&& root.classList.contains('visible')
&& root.getClientRects().length > 0;
const focusables = () => [
...root.querySelectorAll('button, input, [href], [tabindex]:not([tabindex="-1"])'),
].filter((node) => !node.hasAttribute('disabled') && node.getClientRects().length > 0);
const onKeyDown = (event) => {
if (!open || !visible()) return;
// Cooperative ESC contract (see firstRunExperience.js): whoever handles a
// key first marks it, and everyone else honours the mark.
if (event.defaultPrevented) return;
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
close();
return;
}
if (event.key !== 'Tab') return;
const order = focusables();
if (!order.length) return;
const first = order[0];
const last = order[order.length - 1];
const active = documentRef.activeElement;
if (!root.contains(active)) {
event.preventDefault();
(event.shiftKey ? last : first).focus();
return;
}
if (event.shiftKey && active === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && active === last) {
event.preventDefault();
first.focus();
}
};
const openDialog = () => {
if (open) return;
open = true;
previouslyFocused = documentRef.activeElement;
root.hidden = false;
documentRef.addEventListener('keydown', onKeyDown, true);
globalThis.requestAnimationFrame?.(() => {
if (!open) return;
root.classList.add('visible');
root.querySelector('input')?.focus?.({ preventScroll: true });
});
};
const close = () => {
if (!open) return;
open = false;
documentRef.removeEventListener('keydown', onKeyDown, true);
root.classList.remove('visible');
const hide = () => { if (!open) root.hidden = true; };
root.addEventListener('transitionend', hide, { once: true });
globalThis.setTimeout?.(hide, 400);
if (statusLine) statusLine.textContent = defaultStatusText;
if (typeof previouslyFocused?.focus === 'function' && previouslyFocused.isConnected) {
previouslyFocused.focus({ preventScroll: true });
}
};
const say = (text) => { if (statusLine) statusLine.textContent = text; };
const storeLabel = () => (status?.store === 'pinokio-environment'
? 'your app configuration'
: 'your local .env');
const submitUpdates = async (updates, doneVerb) => {
if (busy) return;
const googleWasUnset = !status?.keys?.find((key) => key.id === 'google-maps')?.set;
busy = true;
applyButton?.setAttribute('aria-disabled', 'true');
say('Saving…');
try {
const response = await doFetch('/api/setup/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload.ok) {
say(payload.error || `Save failed (${response.status}).`);
return;
}
for (const input of root.querySelectorAll('input[data-env-var]')) input.value = '';
render(payload.status);
if (googleWasUnset && payload.saved?.includes('GOOGLE_MAPS_API_KEY')) {
const strip = () => {
try {
const next = stripKeylessBasemapFromHash(globalThis.location?.hash?.slice(1) || '');
if (next !== null) globalThis.history?.replaceState?.(null, '', `#${next}`);
} catch {
// Continuity is a nicety, never a blocker.
}
};
strip();
// The live share writer may re-serialize the still-OSM stack before
// the restart's reload lands, so strip again at the door.
globalThis.addEventListener?.('pagehide', strip, { once: true });
}
say(`${doneVerb} ${storeLabel()}. Restarting — this page reloads itself.`);
} catch (error) {
say(`Save failed: ${error?.message || error}`);
} finally {
busy = false;
applyButton?.setAttribute('aria-disabled', 'false');
}
};
const onApply = async () => {
if (busy) return;
const inputs = [...root.querySelectorAll('input[data-env-var]')];
const updates = collectKeyUpdates(
inputs.map((input) => ({ envVar: input.dataset.envVar, value: input.value })),
);
if (!Object.keys(updates).length) {
say('Paste at least one key first.');
return;
}
await submitUpdates(updates, 'Saved to');
};
chip.addEventListener('click', openDialog);
closeButton?.addEventListener('click', close);
applyButton?.addEventListener('click', onApply);
// Remove buttons are rendered per row; delegate so re-renders stay wired.
rowsHost?.addEventListener('click', (event) => {
const button = event.target?.closest?.('[data-key-setup-remove]');
if (!button || busy) return;
let envVars = [];
try {
envVars = JSON.parse(button.dataset.keySetupRemove || '[]');
} catch {
return;
}
if (!Array.isArray(envVars) || !envVars.length) return;
// Removal is destructive and — behind a framing defense that should already
// stop it — a clickjack target. A confirm turns a single aligned click into
// a deliberate two-step the lure cannot pre-satisfy.
const ok = typeof globalThis.confirm !== 'function'
|| globalThis.confirm('Remove this key from your saved configuration?');
if (!ok) return;
void submitUpdates(
Object.fromEntries(envVars.map((name) => [name, null])),
'Removed from',
);
});
render(status);
// Re-entry for a fully-keyed setup, demos, and support: ?setup=1 opens the
// dialog even though the chip has retired.
try {
if (new URLSearchParams(globalThis.location?.search || '').get('setup') === '1') openDialog();
} catch {
// An unparsable location never blocks init.
}
return { open: openDialog, close, render };
}
+41
View File
@@ -0,0 +1,41 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
collectKeyUpdates,
keySetupChipLabel,
stripKeylessBasemapFromHash,
} from './keySetup.js';
test('the chip counts what is missing, and retires the count at zero', () => {
assert.equal(keySetupChipLabel({ setCount: 0, total: 8 }), 'POWER UP · 8 KEYS WAITING');
assert.equal(keySetupChipLabel({ setCount: 7, total: 8 }), 'POWER UP · 1 KEY WAITING');
assert.equal(keySetupChipLabel({ setCount: 8, total: 8 }), 'POWERED UP');
assert.equal(keySetupChipLabel(null), 'POWERED UP', 'no status is not a broken label');
});
test('collectKeyUpdates keeps only non-empty trimmed values', () => {
const updates = collectKeyUpdates([
{ envVar: 'OPENAI_API_KEY', value: ' sk-abc ' },
{ envVar: 'FIRMS_MAP_KEY', value: '' },
{ envVar: 'TOMTOM_API_KEY', value: ' ' },
{ envVar: '', value: 'orphan' },
null,
]);
assert.deepEqual(updates, { OPENAI_API_KEY: 'sk-abc' });
assert.deepEqual(collectKeyUpdates([]), {});
assert.deepEqual(collectKeyUpdates(null), {});
});
test('the first Google key strips ONLY the keyless OSM basemap from the share hash', () => {
const stripped = stripKeylessBasemapFromHash('lat=30.2&lon=-97.7&map=osm&style=normal');
assert.ok(stripped !== null);
const params = new URLSearchParams(stripped);
assert.equal(params.get('map'), null, 'osm basemap removed');
assert.equal(params.get('lat'), '30.2', 'camera survives');
assert.equal(params.get('style'), 'normal', 'style survives');
// A stack under any other name was chosen or shared on purpose.
assert.equal(stripKeylessBasemapFromHash('map=bing-aerial&lat=1'), null);
assert.equal(stripKeylessBasemapFromHash('lat=1&lon=2'), null, 'no stack, nothing to do');
assert.equal(stripKeylessBasemapFromHash(''), null);
assert.equal(stripKeylessBasemapFromHash(undefined), null);
});
+411
View File
@@ -0,0 +1,411 @@
/**
* Key setup ("POWER UP") — the pure core.
*
* One registry, three pure functions, zero dependencies. The dev server's
* /api/setup endpoints (vite.config.js) and the in-app panel (keySetup.js)
* are both thin shells over this module, so what a key is called, what it
* unlocks, and how a .env line is written each live in exactly one place.
*
* Nothing here touches the filesystem, the network, or process.env — callers
* pass environments in and write text out, which is also what makes every
* behavior below unit-testable.
*/
/** Longest accepted key/token value. Real provider keys are all far shorter. */
export const KEY_SETUP_VALUE_LIMIT = 512;
/** Most env vars accepted in one save. The registry defines nine. */
export const KEY_SETUP_UPDATE_LIMIT = 16;
/** Header line written above keys the panel appends to a .env file. */
export const KEY_SETUP_APPEND_HEADER = '# Keys added by the in-app POWER UP panel';
/**
* Every key the panel offers, in the order it offers them — most magic per
* minute first. `tier` mirrors the README's color legend: 'metered' (🔴) is a
* billing-enabled account, 'free' (🟡) is a register-and-paste key.
* `clientExposed` marks the two keys that are injected into the browser
* bundle by design (restrict them at the provider, per SECURITY.md).
*/
export const KEY_SETUP_KEYS = Object.freeze([
Object.freeze({
id: 'google-maps',
title: 'GOOGLE MAPS',
unlocks: 'The photorealistic 3D planet + place search',
getUrl: 'https://developers.google.com/maps/documentation/tile/get-api-key',
envVars: Object.freeze(['GOOGLE_MAPS_API_KEY']),
tier: 'metered',
clientExposed: true,
}),
Object.freeze({
id: 'openai',
title: 'OPENAI',
unlocks: 'Voice control — talk to the planet',
getUrl: 'https://platform.openai.com/api-keys',
envVars: Object.freeze(['OPENAI_API_KEY']),
tier: 'metered',
}),
Object.freeze({
id: 'aisstream',
title: 'AISSTREAM',
unlocks: 'Live ships, worldwide',
getUrl: 'https://aisstream.io',
envVars: Object.freeze(['AISSTREAM_API_KEY']),
tier: 'free',
}),
Object.freeze({
id: 'firms',
title: 'NASA FIRMS',
unlocks: 'Live active-fire detections',
getUrl: 'https://firms.modaps.eosdis.nasa.gov/api/map_key/',
envVars: Object.freeze(['FIRMS_MAP_KEY']),
tier: 'free',
}),
Object.freeze({
id: 'tomtom',
title: 'TOMTOM',
unlocks: 'Real live traffic (keyless runs a simulation)',
getUrl: 'https://developer.tomtom.com',
envVars: Object.freeze(['TOMTOM_API_KEY']),
tier: 'free',
}),
Object.freeze({
id: 'cesium-ion',
title: 'CESIUM ION',
unlocks: 'Bing imagery map stacks + world terrain',
getUrl: 'https://ion.cesium.com/tokens',
envVars: Object.freeze(['CESIUM_ION_TOKEN']),
tier: 'free',
clientExposed: true,
}),
Object.freeze({
id: 'opensky',
title: 'OPENSKY',
unlocks: 'More flight-polling credits (anonymous works without)',
getUrl: 'https://opensky-network.org',
envVars: Object.freeze(['OPENSKY_CLIENT_ID', 'OPENSKY_CLIENT_SECRET']),
tier: 'free',
}),
Object.freeze({
id: 'launch-library',
title: 'LAUNCH LIBRARY',
unlocks: 'Higher space-missions request allowance',
getUrl: 'https://thespacedevs.com',
envVars: Object.freeze(['LL2_API_TOKEN']),
tier: 'free',
}),
]);
/** Hostnames a Provider Settings request may arrive under or originate from. */
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
/** Socket addresses that count as this machine. */
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
/** Parse an exact local request authority from a Host header. */
function localAuthority(hostHeader, protocol) {
const raw = String(hostHeader || '').trim().toLowerCase();
const scheme = String(protocol || '').toLowerCase();
if (!raw || !['http:', 'https:'].includes(scheme) || /[\s/?#@]/.test(raw)) return null;
try {
const parsed = new URL(`${scheme}//${raw}`);
return LOCAL_HOSTNAMES.has(parsed.hostname.toLowerCase()) ? parsed.origin : null;
} catch {
return null;
}
}
/** True only for a subprocess that exited normally and successfully. */
export function commandCompletedSuccessfully(result) {
return !!result && !result.error && !result.signal && result.status === 0;
}
/** Parse one RFC-4180-shaped CSV record, sufficient for `whoami /fo csv`. */
function parseCsvRecord(text) {
const source = String(text || '').replace(/^\uFEFF/, '').trim();
if (!source || /[\r\n]/.test(source)) return null;
const fields = [];
let field = '';
let quoted = false;
for (let i = 0; i < source.length; i += 1) {
const char = source[i];
if (quoted) {
if (char === '"' && source[i + 1] === '"') {
field += '"';
i += 1;
} else if (char === '"') {
quoted = false;
} else {
field += char;
}
} else if (char === '"' && field === '') {
quoted = true;
} else if (char === ',') {
fields.push(field);
field = '';
} else {
field += char;
}
}
if (quoted) return null;
fields.push(field);
return fields;
}
/**
* Extract the current token's user SID from `whoami /user /fo csv /nh`.
* The SID must be the second CSV field and a user-shaped local/domain or Entra
* SID; matching an SID-looking account name or a broad group SID is forbidden.
*/
export function parseWindowsUserSid(stdout) {
const fields = parseCsvRecord(stdout);
if (!fields || fields.length !== 2) return null;
const sid = fields[1].trim();
return /^(?:S-1-5-21-(?:\d+-){3}\d+|S-1-12-1-(?:\d+-){3}\d+)$/i.test(sid)
? sid
: null;
}
/**
* The admission gate for the Provider Settings endpoints — pure, exported so
* every refusal below is pinned by a unit assertion rather than a review note.
*
* Why each check exists:
* - sharing signals: any tunnel/LAN sharing mode disables the surface
* outright — a credential-writing endpoint has no business existing on a
* shared instance, and tunnel traffic reaches the server FROM loopback, so
* the socket check below cannot carry that boundary alone;
* - loopback socket: refuses LAN peers when the server is bound wide;
* - local Host header: tunnel and DNS-rebinding traffic carries a foreign
* Host even when the socket says loopback;
* - exact same Origin on POST: a hostile web page can make a browser POST to
* localhost, and a non-browser caller must not bypass that boundary merely
* by omitting the header;
* - JSON Content-Type on POST: forces cross-origin browsers into a CORS
* preflight this server never answers, closing the simple-request CSRF
* write primitive.
*
* @returns {{ok: true} | {ok: false, status: number, error: string}}
*/
export function admitKeySetupRequest({
method,
remoteAddress,
hostHeader,
protocol = 'http:',
origin,
contentType,
proxyHeaders = {},
env = {},
} = {}) {
// A request carrying reverse-proxy / CDN forwarding headers did not originate
// on this machine, whatever its socket says. Refuse them outright as defense
// in depth — the shipped tunnel (Pinokio) is force-closed at boot, so these
// only appear when someone has deliberately fronted the dev server.
const PROXY_SIGNALS = ['forwarded', 'via', 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto', 'x-real-ip', 'cf-connecting-ip', 'cf-ray'];
if (PROXY_SIGNALS.some((name) => String(proxyHeaders[name] || '').trim() !== '')) {
return { ok: false, status: 403, error: 'Provider Settings does not answer proxied requests' };
}
// Every sharing signal the launcher recognizes (scripts/pinokio-preflight.mjs)
// also disables this surface — so the gate's set is complete, not a subset the
// two files could drift apart on. One DELIBERATE divergence: preflight is a
// boot check that treats an empty PINOKIO_SHARE_VAR as sharing-on (fail closed
// before Start), but here an empty/unset value is the NORMAL git-clone and
// Pinokio state — treating it as sharing would disable Provider Settings for
// every ordinary launch. So a bare/sentinel value is not sharing; only a real
// tunnel var is. This is defense in depth regardless: the loopback+Host checks
// below independently refuse LAN/tunnel traffic, and under Pinokio the launcher
// refuses to boot at all when sharing is genuinely on.
const shareVar = String(env.PINOKIO_SHARE_VAR ?? '').trim();
const sharingEnabled = ['PINOKIO_SHARE_CLOUDFLARE', 'PINOKIO_SHARE_LOCAL']
.some((name) => /^(1|true)$/i.test(String(env[name] || '').trim()))
|| (shareVar !== '' && shareVar !== '__gev_sharing_disabled__');
if (sharingEnabled) {
return { ok: false, status: 403, error: 'Provider Settings is disabled while sharing is enabled' };
}
if (!LOOPBACK_ADDRESSES.has(String(remoteAddress || ''))) {
return { ok: false, status: 403, error: 'Provider Settings answers only the machine running the server' };
}
const authority = localAuthority(hostHeader, protocol);
if (!authority) {
return { ok: false, status: 403, error: 'Provider Settings answers only local hostnames' };
}
if (method === 'POST' && (origin === undefined || origin === null || origin === '')) {
return { ok: false, status: 403, error: 'Provider Settings requires an exact local Origin' };
}
if (origin !== undefined && origin !== null && origin !== '') {
let parsedOrigin;
try {
parsedOrigin = new URL(String(origin));
} catch {
return { ok: false, status: 403, error: 'Unrecognized Origin refused' };
}
const exactOrigin = parsedOrigin.username === ''
&& parsedOrigin.password === ''
&& parsedOrigin.pathname === '/'
&& parsedOrigin.search === ''
&& parsedOrigin.hash === ''
&& parsedOrigin.origin === authority;
if (!exactOrigin) {
return { ok: false, status: 403, error: 'Cross-origin requests are refused' };
}
}
if (method === 'POST' && !String(contentType || '').toLowerCase().startsWith('application/json')) {
return { ok: false, status: 415, error: 'Content-Type must be application/json' };
}
return { ok: true };
}
/** @returns {Set<string>} every env var the panel is allowed to write. */
export function knownKeySetupEnvVars() {
const names = new Set();
for (const entry of KEY_SETUP_KEYS) {
for (const envVar of entry.envVars) names.add(envVar);
}
return names;
}
/**
* Decide whether a live provider value belongs to a source outside the store
* Provider Settings is allowed to edit. `wasExternalAtBoot` carries source
* provenance without carrying the credential itself; it closes the otherwise
* undecidable case where an exported value and a dotenv assignment happen to
* contain the same bytes.
* @param {{effectiveValue: unknown, storedValue: unknown, wasExternalAtBoot?: boolean}} input
*/
export function isKeySetupExternallyManaged({
effectiveValue,
storedValue,
wasExternalAtBoot = false,
} = {}) {
const effective = String(effectiveValue ?? '').trim();
const stored = String(storedValue ?? '').trim();
return effective !== '' && (wasExternalAtBoot || effective !== stored);
}
/**
* Build the status payload the panel renders from: the registry, plus
* per-entry `set` resolved against the given environment. It never includes
* a value, suffix, or other credential material.
* @param {Record<string, string|undefined>} env e.g. process.env
*/
export function keySetupStatus(env = {}) {
const keys = KEY_SETUP_KEYS.map((entry) => {
const values = entry.envVars.map((name) => String(env[name] ?? '').trim());
const set = values.every((value) => value.length > 0);
return {
id: entry.id,
title: entry.title,
unlocks: entry.unlocks,
getUrl: entry.getUrl,
envVars: [...entry.envVars],
tier: entry.tier,
clientExposed: Boolean(entry.clientExposed),
set,
};
});
return {
keys,
setCount: keys.filter((key) => key.set).length,
total: keys.length,
};
}
/**
* Validate a POST body into a clean {ENV_VAR: value} map, or say exactly why
* not. Values must be single-line printable ASCII with no spaces — every real
* provider credential is — which is also what makes the raw `KEY=value` line
* below safe to write without quoting rules. A `null` value means REMOVE:
* the writer comments the assignment back out, returning the file to its
* template state for that key.
* @param {unknown} body Parsed JSON from the request.
*/
export function validateKeySetupUpdates(body) {
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return { ok: false, error: 'Body must be a JSON object of {ENV_VAR: value}' };
}
const entries = Object.entries(body);
if (entries.length === 0) return { ok: false, error: 'No keys provided' };
if (entries.length > KEY_SETUP_UPDATE_LIMIT) {
return { ok: false, error: `At most ${KEY_SETUP_UPDATE_LIMIT} keys per save` };
}
const known = knownKeySetupEnvVars();
const updates = {};
for (const [name, raw] of entries) {
if (!known.has(name)) return { ok: false, error: `Unknown key: ${name}` };
if (raw === null) {
updates[name] = null;
continue;
}
if (typeof raw !== 'string') return { ok: false, error: `${name} must be a string` };
const value = raw.trim();
if (!value) return { ok: false, error: `${name} is empty` };
if (value.length > KEY_SETUP_VALUE_LIMIT) {
return { ok: false, error: `${name} is longer than any real key (${KEY_SETUP_VALUE_LIMIT} max)` };
}
if (!/^[\x21-\x7e]+$/.test(value)) {
return { ok: false, error: `${name} may only contain printable characters with no spaces` };
}
// Reject the dotenv metacharacters that would round-trip WRONG when written
// unquoted (# starts a comment, quotes redelimit, $ expands, backslash and
// backtick are escapes) — so a saved value can never differ from what Node's
// parseEnv and Vite's expansion read back. Real provider keys never contain
// these; they are base64url / hex / JWT alphabets.
if (/[#"'$\\`]/.test(value)) {
return { ok: false, error: `${name} contains a character that is not valid in a key (#, quotes, $, \\, or backtick)` };
}
updates[name] = value;
}
return { ok: true, updates };
}
/**
* Upsert `KEY=value` lines into dotenv text, disturbing nothing else.
*
* Placement, per key: the LAST active assignment is replaced in place (last
* is what dotenv parsing lets win); failing that, the last commented-out
* assignment is uncommented in place, so a file copied from .env.example
* keeps its curated shape; failing both, the line is appended at the end
* under one shared header. Every untouched line — comments, blanks, other
* keys — survives byte for byte, and the result always ends in a newline.
*
* A `null` value REMOVES: every active assignment for that key is commented
* back out (`# KEY=`), returning the file to its template shape; a key with
* no active assignment is left untouched.
* @param {string} text Existing file content ('' births a new file).
* @param {Record<string, string|null>} updates Validated {ENV_VAR: value} map.
*/
export function upsertDotenvValues(text, updates) {
const source = typeof text === 'string' ? text : '';
const lines = source.length ? source.split(/\r?\n/) : [];
const additions = [];
for (const [name, value] of Object.entries(updates)) {
const active = new RegExp(`^\\s*(?:export\\s+)?${name}\\s*=`);
const commented = new RegExp(`^\\s*#\\s*(?:export\\s+)?${name}\\s*=`);
if (value === null) {
lines.forEach((line, index) => {
if (active.test(line)) lines[index] = `# ${name}=`;
});
continue;
}
const assignment = `${name}=${value}`;
let lastActive = -1;
let lastCommented = -1;
lines.forEach((line, index) => {
if (active.test(line)) lastActive = index;
else if (commented.test(line)) lastCommented = index;
});
if (lastActive >= 0) lines[lastActive] = assignment;
else if (lastCommented >= 0) lines[lastCommented] = assignment;
else additions.push(assignment);
}
if (additions.length) {
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
if (!lines.some((line) => line.trim() === KEY_SETUP_APPEND_HEADER)) {
if (lines.length) lines.push('');
lines.push(KEY_SETUP_APPEND_HEADER);
}
lines.push(...additions);
}
const joined = lines.join('\n');
if (!joined) return '';
return joined.endsWith('\n') ? joined : `${joined}\n`;
}
+334
View File
@@ -0,0 +1,334 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
KEY_SETUP_APPEND_HEADER,
KEY_SETUP_KEYS,
KEY_SETUP_VALUE_LIMIT,
commandCompletedSuccessfully,
isKeySetupExternallyManaged,
keySetupStatus,
knownKeySetupEnvVars,
parseWindowsUserSid,
upsertDotenvValues,
validateKeySetupUpdates,
} from './keySetupCore.mjs';
test('the boot provenance snapshot survives in-process Vite config re-evaluation', () => {
// server.restart() re-evaluates vite.config.js in the SAME process after a
// panel save has already set its values live on process.env. A recomputed
// snapshot would classify the panel's own keys as external (read-only) until
// a full process relaunch, so the first evaluation's snapshot must win.
const source = readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
assert.match(
source,
/const PROVIDER_ENV_AT_BOOT = globalThis\.__GEV_PROVIDER_ENV_AT_BOOT \?\?= Object\.freeze\(/,
);
});
test('external ownership uses boot provenance even when store and shell values match', () => {
assert.equal(isKeySetupExternallyManaged({
effectiveValue: 'same-value',
storedValue: 'same-value',
wasExternalAtBoot: true,
}), true, 'equal bytes cannot turn a shell/Keychain value into a file-owned value');
assert.equal(isKeySetupExternallyManaged({
effectiveValue: 'file-value',
storedValue: 'file-value',
}), false, 'a value loaded only from the owned store remains editable');
assert.equal(isKeySetupExternallyManaged({
effectiveValue: 'shell-value',
storedValue: 'stale-file-value',
}), true, 'a differing live value remains external');
assert.equal(isKeySetupExternallyManaged({
effectiveValue: '',
storedValue: 'stale-file-value',
wasExternalAtBoot: true,
}), false, 'an absent live credential has no external owner');
});
test('the status payload reports presence without any credential material', () => {
const env = {
GOOGLE_MAPS_API_KEY: 'AIzaSyFakeFakeFakeFake1234',
OPENSKY_CLIENT_ID: 'client-id-abcdef',
// Secret missing: the OpenSky pair must read as NOT set.
};
const status = keySetupStatus(env);
assert.equal(status.total, KEY_SETUP_KEYS.length);
const google = status.keys.find((key) => key.id === 'google-maps');
assert.equal(google.set, true);
const opensky = status.keys.find((key) => key.id === 'opensky');
assert.equal(opensky.set, false, 'half a credential pair is not configured');
const serialized = JSON.stringify(status);
assert.ok(!serialized.includes('AIzaSyFakeFakeFakeFake1234'), 'a value leaked into status');
assert.ok(!serialized.includes('client-id-abcdef'), 'a value leaked into status');
assert.ok(!serialized.includes('1234'), 'a credential suffix leaked into status');
assert.ok(!serialized.includes('abcdef'), 'a credential suffix leaked into status');
assert.ok(!serialized.includes('tails'), 'status must not expose a credential-tail field');
assert.equal(status.setCount, 1);
});
test('whitespace-only env values do not count as configured', () => {
const status = keySetupStatus({ OPENAI_API_KEY: ' ' });
assert.equal(status.keys.find((key) => key.id === 'openai').set, false);
});
test('subprocess success requires a clean zero exit', () => {
assert.equal(commandCompletedSuccessfully({ status: 0, signal: null }), true);
assert.equal(commandCompletedSuccessfully({ status: 1, signal: null }), false);
assert.equal(commandCompletedSuccessfully({ status: 0, signal: 'SIGTERM' }), false);
assert.equal(commandCompletedSuccessfully({ status: 0, signal: null, error: new Error('spawn failed') }), false);
assert.equal(commandCompletedSuccessfully(null), false);
});
test('Windows owner SID parsing reads only the structured user-SID CSV field', () => {
const localUser = 'S-1-5-21-1111111111-2222222222-3333333333-1001';
const entraUser = 'S-1-12-1-1111111111-2222222222-3333333333-4444444444';
assert.equal(parseWindowsUserSid(`"WORKSTATION\\alice","${localUser}"\r\n`), localUser);
assert.equal(parseWindowsUserSid(`"AzureAD\\alice","${entraUser}"`), entraUser);
assert.equal(
parseWindowsUserSid(`"${localUser}","S-1-5-32-545"`),
null,
'an SID-looking account name must never be mistaken for the token SID',
);
assert.equal(parseWindowsUserSid('"WORKSTATION\\alice","S-1-5-32-545"'), null, 'broad group SID refused');
assert.equal(parseWindowsUserSid(`"WORKSTATION\\alice","${localUser}"\n"extra","${localUser}"`), null);
assert.equal(parseWindowsUserSid(`"WORKSTATION\\alice","${localUser}`), null, 'unterminated CSV refused');
});
test('validation accepts every registry env var and only those', () => {
const known = knownKeySetupEnvVars();
for (const name of known) {
const verdict = validateKeySetupUpdates({ [name]: 'valid-value-123' });
assert.equal(verdict.ok, true, `${name} should validate`);
assert.equal(verdict.updates[name], 'valid-value-123');
}
assert.equal(validateKeySetupUpdates({ PATH: '/usr/bin' }).ok, false, 'PATH must be refused');
assert.equal(validateKeySetupUpdates({ NODE_OPTIONS: '--x' }).ok, false, 'NODE_OPTIONS must be refused');
});
test('validation trims, and refuses empties, newlines, spaces, and oversize values', () => {
const trimmed = validateKeySetupUpdates({ OPENAI_API_KEY: ' sk-abc123 ' });
assert.equal(trimmed.ok, true);
assert.equal(trimmed.updates.OPENAI_API_KEY, 'sk-abc123');
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: '' }).ok, false);
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: ' ' }).ok, false);
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: 'a\nb' }).ok, false, 'newline injection');
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: 'a b' }).ok, false, 'inner space');
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: 'kéy' }).ok, false, 'non-ASCII');
assert.equal(
validateKeySetupUpdates({ OPENAI_API_KEY: 'x'.repeat(KEY_SETUP_VALUE_LIMIT + 1) }).ok,
false,
);
assert.equal(validateKeySetupUpdates(null).ok, false);
assert.equal(validateKeySetupUpdates([]).ok, false);
assert.equal(validateKeySetupUpdates({}).ok, false);
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: 42 }).ok, false);
});
test('upsert replaces the last active assignment in place', () => {
const text = [
'# comment stays',
'OPENAI_API_KEY=old-one',
'PORT=4173',
'OPENAI_API_KEY=old-two',
'',
].join('\n');
const next = upsertDotenvValues(text, { OPENAI_API_KEY: 'new-key' });
assert.equal(next, [
'# comment stays',
'OPENAI_API_KEY=old-one',
'PORT=4173',
'OPENAI_API_KEY=new-key',
'',
].join('\n'));
});
test('upsert uncomments a commented assignment in place, keeping file shape', () => {
const text = [
'# Optional: NASA FIRMS live active fires.',
'# FIRMS_MAP_KEY=',
'',
'PORT=4173',
].join('\n');
const next = upsertDotenvValues(text, { FIRMS_MAP_KEY: 'firms-123' });
assert.equal(next, [
'# Optional: NASA FIRMS live active fires.',
'FIRMS_MAP_KEY=firms-123',
'',
'PORT=4173',
].join('\n') + '\n');
});
test('upsert appends unknown keys under one shared header, once', () => {
const first = upsertDotenvValues('PORT=4173\n', { OPENAI_API_KEY: 'sk-1' });
assert.equal(first, [
'PORT=4173',
'',
KEY_SETUP_APPEND_HEADER,
'OPENAI_API_KEY=sk-1',
].join('\n') + '\n');
const second = upsertDotenvValues(first, { FIRMS_MAP_KEY: 'f-2' });
assert.equal(second, [
'PORT=4173',
'',
KEY_SETUP_APPEND_HEADER,
'OPENAI_API_KEY=sk-1',
'FIRMS_MAP_KEY=f-2',
].join('\n') + '\n');
assert.equal(second.split(KEY_SETUP_APPEND_HEADER).length, 2, 'header written once');
});
test('upsert births a well-formed file from nothing', () => {
const next = upsertDotenvValues('', { GOOGLE_MAPS_API_KEY: 'AIza-x' });
assert.equal(next, `${KEY_SETUP_APPEND_HEADER}\nGOOGLE_MAPS_API_KEY=AIza-x\n`);
});
test('upsert handles export-prefixed lines and never touches lookalike keys', () => {
const text = [
'export OPENAI_API_KEY=old',
'NOT_OPENAI_API_KEY=keep-me',
'OPENAI_API_KEY_MINI=keep-me-too',
].join('\n');
const next = upsertDotenvValues(text, { OPENAI_API_KEY: 'new' });
const lines = next.split('\n');
assert.equal(lines[0], 'OPENAI_API_KEY=new');
assert.equal(lines[1], 'NOT_OPENAI_API_KEY=keep-me');
assert.equal(lines[2], 'OPENAI_API_KEY_MINI=keep-me-too');
});
test('upsert is idempotent for a repeated save', () => {
const once = upsertDotenvValues('', { OPENAI_API_KEY: 'sk-1', FIRMS_MAP_KEY: 'f-1' });
const twice = upsertDotenvValues(once, { OPENAI_API_KEY: 'sk-1', FIRMS_MAP_KEY: 'f-1' });
assert.equal(once, twice);
});
test('a real .env.example round-trip: the curated file keeps its shape', () => {
// A representative slice of the shipped .env.example.
const example = [
'# God\'s Eye View — environment variables',
'GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here',
'',
'# Optional: OpenAI Realtime voice control. Do not prefix with VITE_.',
'OPENAI_API_KEY=',
'OPENAI_REALTIME_MODEL=gpt-realtime-2',
'',
'# TOMTOM_API_KEY=',
].join('\n');
const next = upsertDotenvValues(example, {
GOOGLE_MAPS_API_KEY: 'AIza-real',
OPENAI_API_KEY: 'sk-real',
TOMTOM_API_KEY: 'tt-real',
});
const lines = next.split('\n');
assert.equal(lines[1], 'GOOGLE_MAPS_API_KEY=AIza-real');
assert.equal(lines[4], 'OPENAI_API_KEY=sk-real');
assert.equal(lines[5], 'OPENAI_REALTIME_MODEL=gpt-realtime-2', 'sibling key untouched');
assert.equal(lines[7], 'TOMTOM_API_KEY=tt-real', 'commented key uncommented in place');
});
test('the admission gate refuses every non-local shape, one assertion per refusal', async () => {
const { admitKeySetupRequest } = await import('./keySetupCore.mjs');
const local = {
method: 'POST',
remoteAddress: '127.0.0.1',
hostHeader: 'localhost:4173',
origin: 'http://localhost:4173',
contentType: 'application/json',
env: {},
};
assert.equal(admitKeySetupRequest(local).ok, true, 'the honest local request is admitted');
assert.equal(admitKeySetupRequest({ ...local, method: 'GET', contentType: undefined }).ok, true, 'local GET needs no content type');
assert.equal(admitKeySetupRequest({ ...local, origin: undefined }).ok, false, 'POST without Origin is refused');
assert.equal(admitKeySetupRequest({ ...local, method: 'GET', origin: undefined, contentType: undefined }).ok, true, 'local GET may omit Origin');
assert.equal(admitKeySetupRequest({ ...local, remoteAddress: '::ffff:127.0.0.1', hostHeader: '[::1]:4173', origin: 'http://[::1]:4173' }).ok, true, 'IPv6 loopback forms are local');
// Tunnel/LAN sharing of any kind removes the surface outright — tunnel
// traffic arrives FROM loopback, so no socket check can carry this boundary.
assert.equal(admitKeySetupRequest({ ...local, env: { PINOKIO_SHARE_CLOUDFLARE: 'true' } }).ok, false, 'sharing disables the surface');
assert.equal(admitKeySetupRequest({ ...local, env: { PINOKIO_SHARE_LOCAL: '1' } }).ok, false, 'LAN sharing disables the surface');
// A LAN peer reaching a wide-bound server.
assert.equal(admitKeySetupRequest({ ...local, remoteAddress: '192.168.1.20' }).ok, false, 'non-loopback socket refused');
// Tunnel and DNS-rebinding traffic carries a foreign Host over a loopback socket.
assert.equal(admitKeySetupRequest({ ...local, hostHeader: 'abc.trycloudflare.com' }).ok, false, 'foreign Host refused');
assert.equal(admitKeySetupRequest({ ...local, hostHeader: 'workstation.local:4173' }).ok, false, 'non-localhost hostnames refused');
assert.equal(admitKeySetupRequest({ ...local, hostHeader: '' }).ok, false, 'missing Host refused');
assert.equal(admitKeySetupRequest({ ...local, hostHeader: '[::1].evil:4173' }).ok, false, 'malformed bracketed Host refused');
// A hostile web page POSTing at localhost carries its own Origin.
assert.equal(admitKeySetupRequest({ ...local, origin: 'https://evil.example' }).ok, false, 'cross-origin refused');
assert.equal(admitKeySetupRequest({ ...local, origin: 'not a url' }).ok, false, 'unparseable Origin refused');
assert.equal(admitKeySetupRequest({ ...local, origin: 'http://localhost:4174' }).ok, false, 'cross-port Origin refused');
assert.equal(admitKeySetupRequest({ ...local, origin: 'https://localhost:4173' }).ok, false, 'cross-scheme Origin refused');
assert.equal(admitKeySetupRequest({ ...local, origin: 'http://127.0.0.1:4173' }).ok, false, 'different loopback host Origin refused');
// A simple-request POST (no JSON content type) is the CSRF write shape.
const noJson = admitKeySetupRequest({ ...local, contentType: 'text/plain' });
assert.equal(noJson.ok, false, 'non-JSON POST refused');
assert.equal(noJson.status, 415);
});
test('a null value validates as a removal; an empty string still does not', () => {
const removal = validateKeySetupUpdates({ OPENAI_API_KEY: null });
assert.equal(removal.ok, true);
assert.equal(removal.updates.OPENAI_API_KEY, null);
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: '' }).ok, false, 'empty is a mistake, not a removal');
assert.equal(validateKeySetupUpdates({ PATH: null }).ok, false, 'removal is registry-bound too');
});
test('removal comments the assignment back out, returning the file to template shape', () => {
const text = [
'# Optional: OpenAI Realtime voice control.',
'OPENAI_API_KEY=sk-live',
'PORT=4173',
].join('\n');
const next = upsertDotenvValues(text, { OPENAI_API_KEY: null });
const lines = next.split('\n');
assert.equal(lines[1], '# OPENAI_API_KEY=', 'active line commented out, not deleted');
assert.equal(lines[2], 'PORT=4173', 'neighbors untouched');
// Removing a key with no active assignment changes nothing.
assert.equal(upsertDotenvValues(next, { FIRMS_MAP_KEY: null }), next);
// The commented-out line is reusable: a later save uncomments it in place.
const again = upsertDotenvValues(next, { OPENAI_API_KEY: 'sk-new' });
assert.equal(again.split('\n')[1], 'OPENAI_API_KEY=sk-new');
});
test('the sharing gate treats a real PINOKIO_SHARE_VAR as sharing, but not the empty/sentinel normal state', async () => {
const { admitKeySetupRequest } = await import('./keySetupCore.mjs');
const base = {
method: 'POST', remoteAddress: '127.0.0.1', hostHeader: 'localhost:4173',
origin: 'http://localhost:4173', contentType: 'application/json',
};
// The ordinary launch states: unset, empty, or the explicit disabled sentinel.
assert.equal(admitKeySetupRequest({ ...base, env: {} }).ok, true, 'unset SHARE_VAR is normal');
assert.equal(admitKeySetupRequest({ ...base, env: { PINOKIO_SHARE_VAR: '' } }).ok, true, 'empty SHARE_VAR is normal');
assert.equal(admitKeySetupRequest({ ...base, env: { PINOKIO_SHARE_VAR: '__gev_sharing_disabled__' } }).ok, true, 'the disabled sentinel is normal');
// A real tunnel var disables the surface.
assert.equal(admitKeySetupRequest({ ...base, env: { PINOKIO_SHARE_VAR: 'MY_TUNNEL_TOKEN' } }).ok, false, 'a real share var is sharing');
});
test('the gate refuses proxied requests even from a loopback socket with local headers', async () => {
const { admitKeySetupRequest } = await import('./keySetupCore.mjs');
const base = {
method: 'POST', remoteAddress: '127.0.0.1', hostHeader: 'localhost:4173',
origin: 'http://localhost:4173', contentType: 'application/json', env: {},
};
assert.equal(admitKeySetupRequest(base).ok, true, 'no proxy headers → admitted');
for (const header of ['x-forwarded-for', 'forwarded', 'via', 'cf-connecting-ip', 'cf-ray', 'x-real-ip', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto']) {
assert.equal(
admitKeySetupRequest({ ...base, proxyHeaders: { [header]: 'anything' } }).ok,
false,
`${header} present → refused`,
);
}
// An empty forwarding header is not a proxy signal.
assert.equal(admitKeySetupRequest({ ...base, proxyHeaders: { 'x-forwarded-for': '' } }).ok, true);
});
test('validation rejects dotenv metacharacters that would round-trip wrong', () => {
for (const bad of ['abc#def', 'ab"cd', "ab'cd", 'ab$cd', 'ab\\cd', 'ab`cd']) {
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: bad }).ok, false, `${JSON.stringify(bad)} refused`);
}
// Real key alphabets still pass: base64url, JWT dots, hex, plus/slash.
for (const good of ['sk-AbC0-9_x', 'eyJhbGc.eyJzdWI.QWxpY2U', 'a1b2c3d4e5f6', 'AB+cd/ef=']) {
assert.equal(validateKeySetupUpdates({ OPENAI_API_KEY: good }).ok, true, `${good} accepted`);
}
});
+163
View File
@@ -0,0 +1,163 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
commandCompletedSuccessfully,
parseWindowsUserSid,
} from './keySetupCore.mjs';
/** PowerShell verification for the exact owner-only Windows credential DACL. */
const WINDOWS_ACL_VERIFY_SCRIPT = [
"$ErrorActionPreference = 'Stop'",
'$acl = Get-Acl -LiteralPath $env:GEV_ACL_FILE',
'if (-not $acl.AreAccessRulesProtected) { exit 2 }',
"$allowed = @($env:GEV_ACL_USER_SID, 'S-1-5-18', 'S-1-5-32-544')",
'$seen = @{}',
'$rules = @($acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))',
'if ($rules.Count -ne 3) { exit 7 }',
'foreach ($rule in $rules) {',
' $ruleSid = $rule.IdentityReference.Value',
' if ($rule.IsInherited) { exit 3 }',
' if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { exit 4 }',
' if ($allowed -notcontains $ruleSid) { exit 5 }',
' if ($seen.ContainsKey($ruleSid)) { exit 8 }',
' $full = [System.Security.AccessControl.FileSystemRights]::FullControl',
' if ($rule.FileSystemRights -ne $full) { exit 6 }',
' $seen[$ruleSid] = $true',
'}',
'if ($seen.Count -ne 3) { exit 9 }',
].join('; ');
/**
* Resolve the native Windows ACL tools without consulting PATH.
*
* Provider Settings supports the standard Windows installation layout only:
* a local drive root named `Windows` (for example C:\\Windows or D:\\Windows).
* Requiring consistent aliases, canonical paths, and regular files prevents an
* inherited environment override, UNC share, device path, junction, or PATH
* shim from being treated as an operating-system security tool.
*/
function resolveWindowsNativeTools(environment, fileSystem, architecture) {
const aliases = ['SystemRoot', 'SYSTEMROOT', 'WINDIR', 'windir'];
const configured = aliases
.map((name) => environment[name])
.filter((value) => typeof value === 'string' && value.length > 0);
if (configured.length === 0) return null;
const roots = configured.map((value) => {
if (value !== value.trim() || !/^[A-Za-z]:\\Windows\\?$/i.test(value)) return null;
return value.endsWith('\\') ? value.slice(0, -1) : value;
});
if (roots.some((root) => !root)) return null;
if (roots.some((root) => root.toLowerCase() !== roots[0].toLowerCase())) return null;
const systemRoot = roots[0];
const systemDirectory = architecture === 'ia32' ? 'Sysnative' : 'System32';
const expected = {
whoami: path.win32.join(systemRoot, systemDirectory, 'whoami.exe'),
icacls: path.win32.join(systemRoot, systemDirectory, 'icacls.exe'),
powershell: path.win32.join(
systemRoot,
systemDirectory,
'WindowsPowerShell',
'v1.0',
'powershell.exe',
),
};
try {
const realpath = fileSystem.realpathSync.native || fileSystem.realpathSync;
const rootEntry = fileSystem.lstatSync(systemRoot);
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) return null;
const canonicalRoot = realpath.call(fileSystem.realpathSync, systemRoot);
if (canonicalRoot.toLowerCase() !== systemRoot.toLowerCase()) return null;
for (const executable of Object.values(expected)) {
const entry = fileSystem.lstatSync(executable);
if (!entry.isFile() || entry.isSymbolicLink()) return null;
const canonicalExecutable = realpath.call(fileSystem.realpathSync, executable);
const canonicalCandidates = [executable];
if (architecture === 'ia32') {
canonicalCandidates.push(executable.replace('\\Sysnative\\', '\\System32\\'));
}
if (!canonicalCandidates.some(
(candidate) => candidate.toLowerCase() === canonicalExecutable.toLowerCase(),
)) return null;
}
} catch {
return null;
}
return expected;
}
/**
* Restrict a credential file before any secret is written to it.
* Dependencies are injectable so every fail-closed branch is unit-testable.
*/
export function hardenCredentialFile(filepath, {
platform = process.platform,
architecture = process.arch,
spawn = spawnSync,
fileSystem = fs,
environment = process.env,
} = {}) {
if (platform !== 'win32') {
try {
if (platform === 'darwin') {
const aclRemoval = spawn('chmod', ['-N', filepath], { stdio: 'ignore' });
if (!commandCompletedSuccessfully(aclRemoval)) return false;
}
fileSystem.chmodSync(filepath, 0o600);
return (fileSystem.statSync(filepath).mode & 0o777) === 0o600;
} catch {
return false;
}
}
const tools = resolveWindowsNativeTools(environment, fileSystem, architecture);
if (!tools) return false;
try {
// Grant by the CURRENT PROCESS TOKEN'S SID, never a bare username. Parsing
// the second CSV field structurally prevents an SID-looking account name or
// a broad group SID from becoming the credential owner.
const whoami = spawn(tools.whoami, ['/user', '/fo', 'csv', '/nh'], {
encoding: 'utf8',
windowsHide: true,
});
const sid = commandCompletedSuccessfully(whoami)
? parseWindowsUserSid(whoami.stdout)
: null;
if (!sid) return false;
const applied = spawn(tools.icacls, [
filepath,
'/inheritance:r',
'/grant:r',
`*${sid}:F`,
'*S-1-5-18:F',
'*S-1-5-32-544:F',
], { stdio: 'ignore', windowsHide: true });
if (!commandCompletedSuccessfully(applied)) return false;
// Command success is not proof of the resulting DACL. Query it back and
// accept only three explicit FullControl allow principals, with inheritance
// disabled. Any unexpected rule, right, command error, or missing principal
// fails closed before the secret reaches disk.
const verified = spawn(tools.powershell, [
'-NoProfile',
'-NonInteractive',
'-Command', WINDOWS_ACL_VERIFY_SCRIPT,
], {
env: {
...environment,
GEV_ACL_FILE: filepath,
GEV_ACL_USER_SID: sid,
},
stdio: 'ignore',
windowsHide: true,
});
return commandCompletedSuccessfully(verified);
} catch {
return false;
}
}
+274
View File
@@ -0,0 +1,274 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { hardenCredentialFile } from './keySetupHardening.mjs';
const FILE = path.join(os.tmpdir(), 'provider-settings-test');
const USER_SID = 'S-1-5-21-1111111111-2222222222-3333333333-1001';
const WINDOWS_ROOT = 'C:\\Windows';
function windowsFileSystem({
root = WINDOWS_ROOT,
systemDirectory = 'System32',
realpaths = {},
missing = [],
symlinks = [],
} = {}) {
const tools = [
`${root}\\${systemDirectory}\\whoami.exe`,
`${root}\\${systemDirectory}\\icacls.exe`,
`${root}\\${systemDirectory}\\WindowsPowerShell\\v1.0\\powershell.exe`,
];
const realpathSync = (filepath) => realpaths[filepath] || filepath;
realpathSync.native = realpathSync;
return {
realpathSync,
lstatSync(filepath) {
return {
isDirectory: () => filepath === root,
isFile: () => tools.includes(filepath) && !missing.includes(filepath),
isSymbolicLink: () => symlinks.includes(filepath),
};
},
};
}
function fileSystemWithMode(mode = 0o600) {
const calls = [];
return {
calls,
chmodSync(filepath, nextMode) { calls.push(['chmod', filepath, nextMode]); },
statSync(filepath) { calls.push(['stat', filepath]); return { mode }; },
};
}
test('macOS ACL removal failure stops before chmod and fails closed', () => {
const fileSystem = fileSystemWithMode();
const result = hardenCredentialFile(FILE, {
platform: 'darwin',
fileSystem,
spawn: () => ({ status: 1, signal: null }),
});
assert.equal(result, false);
assert.deepEqual(fileSystem.calls, [], 'mode bits must not disguise an ACL-removal failure');
});
test('POSIX hardening verifies the resulting 0600 mode', () => {
const goodFs = fileSystemWithMode(0o100600);
assert.equal(hardenCredentialFile(FILE, {
platform: 'darwin',
fileSystem: goodFs,
spawn: () => ({ status: 0, signal: null }),
}), true);
assert.deepEqual(goodFs.calls, [['chmod', FILE, 0o600], ['stat', FILE]]);
const broadFs = fileSystemWithMode(0o100640);
assert.equal(hardenCredentialFile(FILE, {
platform: 'linux',
fileSystem: broadFs,
spawn: () => { throw new Error('Linux must not spawn chmod'); },
}), false);
});
test('Windows hardening refuses an unstructured or broad owner SID before icacls', () => {
const commands = [];
const result = hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem: windowsFileSystem(),
spawn(command) {
commands.push(command);
return { status: 0, signal: null, stdout: '"S-1-5-21-1-2-3-1001","S-1-5-32-545"' };
},
});
assert.equal(result, false);
assert.deepEqual(commands, [`${WINDOWS_ROOT}\\System32\\whoami.exe`]);
});
test('Windows hardening applies and then verifies the exact restricted DACL', () => {
const calls = [];
const filepath = 'C:\\GEV App\\pinokio\\ENVIRONMENT.tmp';
const result = hardenCredentialFile(filepath, {
platform: 'win32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem: windowsFileSystem(),
spawn(command, args, options) {
calls.push({ command, args, options });
if (command.endsWith('\\whoami.exe')) {
return { status: 0, signal: null, stdout: `"WORKSTATION\\alice","${USER_SID}"\r\n` };
}
return { status: 0, signal: null };
},
});
assert.equal(result, true);
assert.deepEqual(calls.map(({ command }) => command), [
`${WINDOWS_ROOT}\\System32\\whoami.exe`,
`${WINDOWS_ROOT}\\System32\\icacls.exe`,
`${WINDOWS_ROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`,
]);
assert.deepEqual(calls[1].args, [
filepath,
'/inheritance:r',
'/grant:r',
`*${USER_SID}:F`,
'*S-1-5-18:F',
'*S-1-5-32-544:F',
]);
assert.equal(calls[1].args.filter((arg) => arg === '/grant:r').length, 1);
assert.equal(calls[2].options.env.GEV_ACL_FILE, filepath);
assert.equal(calls[2].options.env.GEV_ACL_USER_SID, USER_SID);
assert.match(calls[2].args.at(-1), /AreAccessRulesProtected/);
assert.match(calls[2].args.at(-1), /rules\.Count -ne 3/);
assert.match(calls[2].args.at(-1), /seen\.ContainsKey/);
assert.match(calls[2].args.at(-1), /FileSystemRights -ne \$full/);
assert.match(calls[2].args.at(-1), /seen\.Count -ne 3/);
});
test('Windows hardening bypasses PATH-shadowed native ACL tools', () => {
const commands = [];
const result = hardenCredentialFile('D:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: {
PATH: 'D:\\pinokio\\bin;C:\\Windows\\System32',
SystemRoot: WINDOWS_ROOT,
WINDIR: 'c:\\windows\\',
},
fileSystem: windowsFileSystem(),
spawn(command) {
commands.push(command);
if (command.endsWith('\\whoami.exe')) {
return { status: 0, signal: null, stdout: `"WORKSTATION\\alice","${USER_SID}"` };
}
return { status: 0, signal: null };
},
});
assert.equal(result, true);
assert.equal(commands.some((command) => !command.startsWith(`${WINDOWS_ROOT}\\System32\\`)), false);
});
test('Windows hardening rejects redirected or ambiguous system roots before spawning', () => {
const rejected = [
{},
{ SYSTEMROOT: 'Windows' },
{ SYSTEMROOT: '\\Windows' },
{ SYSTEMROOT: '\\\\server\\share\\Windows' },
{ SYSTEMROOT: '\\\\?\\C:\\Windows' },
{ SYSTEMROOT: 'C:\\Temp\\..\\Windows' },
{ SYSTEMROOT: 'C:\\attacker\\Windows' },
{ SYSTEMROOT: WINDOWS_ROOT, WINDIR: 'D:\\Windows' },
];
for (const environment of rejected) {
let spawned = false;
const result = hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment,
fileSystem: windowsFileSystem(),
spawn() { spawned = true; return { status: 0, signal: null }; },
});
assert.equal(result, false, JSON.stringify(environment));
assert.equal(spawned, false, JSON.stringify(environment));
}
});
test('Windows hardening accepts a canonical Windows root on a non-default drive', () => {
const root = 'D:\\Windows';
const commands = [];
const result = hardenCredentialFile('D:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: { SYSTEMROOT: root },
fileSystem: windowsFileSystem({ root }),
spawn(command) {
commands.push(command);
if (command.endsWith('\\whoami.exe')) {
return { status: 0, signal: null, stdout: `"WORKSTATION\\alice","${USER_SID}"` };
}
return { status: 0, signal: null };
},
});
assert.equal(result, true);
assert.equal(commands.every((command) => command.startsWith('D:\\Windows\\System32\\')), true);
});
test('32-bit Windows hardening uses the native Sysnative bridge', () => {
const commands = [];
const result = hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
architecture: 'ia32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem: windowsFileSystem({ systemDirectory: 'Sysnative' }),
spawn(command) {
commands.push(command);
if (command.endsWith('\\whoami.exe')) {
return { status: 0, signal: null, stdout: `"WORKSTATION\\alice","${USER_SID}"` };
}
return { status: 0, signal: null };
},
});
assert.equal(result, true);
assert.equal(commands.every((command) => command.includes('\\Sysnative\\')), true);
});
test('Windows hardening rejects missing, redirected, or non-file native tools', () => {
const whoami = `${WINDOWS_ROOT}\\System32\\whoami.exe`;
const cases = [
windowsFileSystem({ missing: [whoami] }),
windowsFileSystem({ realpaths: { [WINDOWS_ROOT]: 'C:\\RedirectedWindows' } }),
windowsFileSystem({ realpaths: { [whoami]: 'C:\\attacker\\whoami.exe' } }),
windowsFileSystem({ realpaths: { [whoami]: 'C:\\Windows\\Temp\\evil-whoami.exe' } }),
windowsFileSystem({ symlinks: [whoami] }),
];
for (const fileSystem of cases) {
let spawned = false;
assert.equal(hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem,
spawn() { spawned = true; return { status: 0, signal: null }; },
}), false);
assert.equal(spawned, false);
}
});
test('Windows hardening fails closed when ACL application or verification fails', () => {
for (const failingCommand of ['icacls.exe', 'powershell.exe']) {
const calls = [];
const result = hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem: windowsFileSystem(),
spawn(command) {
calls.push(command);
if (command.endsWith('\\whoami.exe')) {
return { status: 0, signal: null, stdout: `"WORKSTATION\\alice","${USER_SID}"` };
}
return { status: command.endsWith(`\\${failingCommand}`) ? 1 : 0, signal: null };
},
});
assert.equal(result, false, `${failingCommand} failure must refuse the write`);
assert.equal(calls.at(-1).endsWith(`\\${failingCommand}`), true);
}
});
test('Windows hardening converts subprocess exceptions into a fail-closed result', () => {
assert.equal(hardenCredentialFile('C:\\GEV\\ENVIRONMENT.tmp', {
platform: 'win32',
environment: { SYSTEMROOT: WINDOWS_ROOT },
fileSystem: windowsFileSystem(),
spawn() { throw new Error('subprocess unavailable'); },
}), false);
});
test('Windows production hardener applies its exact DACL with native tools', {
skip: process.platform !== 'win32',
}, () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'gev-provider-acl-'));
const filepath = path.join(directory, 'ENVIRONMENT.tmp');
try {
fs.writeFileSync(filepath, '');
assert.equal(hardenCredentialFile(filepath), true);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});
+1 -6
View File
@@ -44,12 +44,7 @@ function terminalFromParticipantStats(summary, participantIds) {
if (!participantIds?.length) return null;
const participants = new Set(participantIds);
return summary.records.some((record) => participants.has(record.id)
// A deliberately unconfigured optional provider is a truthful terminal
// state for that row, not a failed multi-layer mission. The layer keeps
// owning its KEY REQUIRED copy; explicit lifecycle failure events still
// merge in below and retain error priority.
&& !record.keyRequired
&& (record.error || record.unavailable))
&& (record.error || record.unavailable || record.keyRequired))
? 'error'
: null;
}
+1 -26
View File
@@ -286,7 +286,7 @@ test('AIS first-connect grace expiry reports failure even without a terminal man
assert.equal(presentLoadingFeedback(state, unavailable, 300).label, 'LOAD FAILED');
});
test('an explicitly missing optional key completes the global loading batch honestly', () => {
test('participant stats failure outranks a simultaneous visibility completion', () => {
const enabling = aggregateLayerLoading([{
id: 'ais-live-vessels',
name: 'AIS Vessels',
@@ -307,31 +307,6 @@ test('an explicitly missing optional key completes the global loading batch hone
type: 'visibility', layerId: 'ais-live-vessels', enabled: true,
});
assert.equal(state.terminal, 'complete');
assert.equal(presentLoadingFeedback(state, missingKey, 250).label, 'LOAD COMPLETE');
});
test('an explicit lifecycle failure still outranks a key-required row', () => {
const enabling = aggregateLayerLoading([{
id: 'local-firms',
name: 'FIRMS Active Fires',
lifecycleState: 'enabling',
stats: { loading: true },
}]);
let state = reduceLoadingFeedback(createLoadingFeedbackState(), enabling, 0);
state = reduceLoadingFeedback(state, enabling, 200);
const missingKey = aggregateLayerLoading([{
id: 'local-firms',
name: 'FIRMS Active Fires',
enabled: true,
lifecycleState: 'enabled',
stats: { loading: false, keyRequired: true, error: 'KEY REQUIRED' },
}]);
state = reduceLoadingFeedback(state, missingKey, 250, {
type: 'visibility-failed', layerId: 'local-firms', error: new Error('lifecycle failed'),
});
assert.equal(state.terminal, 'error');
});
+1 -1
View File
@@ -696,7 +696,7 @@ export function placeFramingViewport(viewport, anchorLat, anchorLng, types = [])
}
/**
* Natural-region framing heuristic (field test 2026-07-23). Pure — exported
* Natural-region framing heuristic (owner field test 2026-07-23). Pure — exported
* for unit tests. Given geocode {southwest,northeast} bounds, decide whether to
* frame the full viewport or a capped oblique swath over the feature's center,
* looking along the feature's long axis.
+2 -2
View File
@@ -116,7 +116,7 @@ test('admin types win over area types when both present', () => {
assert.equal(geocodeNavigationMode(['locality', 'park', 'political']), 'city-overview');
});
// Natural-region framing heuristic (field test 2026-07-23): "take me to the
// Natural-region framing heuristic (owner field test 2026-07-23): "take me to the
// Rocky Mountains" geocodes as natural_feature with a ~2,700 km viewport — framing
// the whole box flies the camera to space. Region-scale viewports get a capped
// oblique swath instead; ordinary parks/lakes keep full-viewport framing.
@@ -474,7 +474,7 @@ test('regionFramingPlan: invalid viewports return null', () => {
assert.equal(regionFramingPlan({ southwest: { lat: NaN, lng: 0 }, northeast: { lat: 1, lng: 1 } }), null);
});
// Globe-view preset (field test 2026-07-23): "zoom out to a globe view" needs an
// Globe-view preset (owner field test 2026-07-23): "zoom out to a globe view" needs an
// ABSOLUTE full-earth framing — the relative zoom tool can never reach it. The preset
// must sit inside the app's own 'global' view-scale band (>12,000 km camera height,
// classifyViewScale in gevActions.js) and under the fly_to rangeM ceiling (20,000 km).
+30 -28
View File
@@ -32,6 +32,8 @@ import {
} from './renderGovernor.js';
import { installScopeMask } from './scopeMask.js';
import { initFirstRunExperience } from './firstRunExperience.js';
import { initKeySetup } from './keySetup.js';
import { loadPhotorealisticTileset } from './mapStartup.js';
initLogoGaze();
@@ -73,21 +75,11 @@ async function init() {
try {
loaderStatus.textContent = 'Configuring viewer...';
// Set Cesium Ion token for World Terrain
// A direct Google key provides Google 3D plus GEV place search. Cesium ion
// can host the same 3D tiles and also powers Bing/world-terrain stacks.
const cesiumToken = import.meta.env.CESIUM_ION_TOKEN;
if (cesiumToken) {
Cesium.Ion.defaultAccessToken = cesiumToken;
}
// Set Google Maps API key for 3D Tiles
const googleApiKey = import.meta.env.GOOGLE_MAPS_API_KEY;
if (!googleApiKey) {
throw new Error('GOOGLE_MAPS_API_KEY not found. Set it as an environment variable.');
}
Cesium.GoogleMaps.defaultApiKey = googleApiKey;
// Expose API key globally for geocoding in locations.js
window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
if (googleApiKey) window.__GOOGLE_MAPS_API_KEY__ = googleApiKey;
// Create the Cesium viewer with minimal chrome
const viewer = new Cesium.Viewer('cesiumContainer', {
@@ -137,7 +129,7 @@ async function init() {
// Required by each source's license (ODbL, CC BY-NC-SA, NASA FIRMS, etc.);
// strings are verbatim from DATA_SOURCES.md. Static + always-present in the
// expandable bottom-left credit lightbox (showOnScreen=false), so they never
// clutter the on-globe attribution line.
// clutter the on-globe line. See docs/pre-ship-audit-2026-07-01.md H11.
registerDataCredits(viewer);
// Hide Cesium's default globe — Google Photorealistic 3D Tiles provide their own
@@ -153,22 +145,27 @@ async function init() {
viewer.scene.skyAtmosphere.saturationShift = -0.12;
viewer.scene.skyAtmosphere.brightnessShift = -0.08;
loaderStatus.textContent = 'Loading Google 3D Tiles...';
let tileset = null;
try {
// Load Google Photorealistic 3D Tiles
tileset = await Cesium.createGooglePhotorealistic3DTileset({
onlyUsingWithGoogleGeocoder: true,
});
loaderStatus.textContent = googleApiKey || cesiumToken
? 'Loading Google 3D Tiles...'
: 'Loading the keyless globe...';
const photoreal = await loadPhotorealisticTileset(Cesium, {
googleApiKey,
cesiumToken,
});
const tileset = photoreal.tileset;
if (tileset) {
viewer.scene.primitives.add(tileset);
// NOTE: Cesium World Terrain intentionally disabled — conflicts with Google 3D Tiles at high zoom.
// Google Photorealistic 3D Tiles provide their own terrain/elevation.
viewer.scene.globe.show = false;
} catch (tileError) {
console.warn('[Init] Google 3D Tiles unavailable, falling back to Cesium globe:', tileError);
const tileErrorDetail = describeError(tileError);
loaderStatus.textContent = `Google 3D Tiles unavailable (${tileErrorDetail}). Continuing in fallback mode...`;
// Keep Cesium globe visible as fallback instead of aborting the app.
console.info(`[Init] Google 3D Tiles loaded via ${photoreal.route}.`);
} else {
if (photoreal.errors.length) {
const tileError = photoreal.errors.at(-1);
console.warn('[Init] Google 3D Tiles unavailable, using the keyless globe:', tileError);
const tileErrorDetail = describeError(tileError);
loaderStatus.textContent = `Google 3D Tiles unavailable (${tileErrorDetail}). Loading the keyless globe...`;
}
viewer.scene.globe.show = true;
}
@@ -177,7 +174,7 @@ async function init() {
const mapStackController = new MapStackController(viewer, {
googleTileset: tileset,
cesiumToken,
initialStack: tileset ? 'photoreal' : 'osm',
initialStack: tileset ? 'photoreal' : 'esri-imagery',
// Task 5 (height-datum fix): rebroadcast stack changes as a window
// CustomEvent so data layers (CCTV per-regime ground resolution) can
// react without coupling MapStackController to layer modules. Fires on
@@ -188,7 +185,7 @@ async function init() {
},
onError: (message) => console.warn('[MapStack]', message),
});
await mapStackController.setStack(tileset ? 'photoreal' : 'osm', { silent: true });
await mapStackController.setStack(tileset ? 'photoreal' : 'esri-imagery', { silent: true });
// Initialize the style manager (post-processing, HUD, locations, share links)
const styleManager = new StyleManager(viewer, { mapStackController });
@@ -270,6 +267,11 @@ async function init() {
setTimeout(revealFirstRun, 900);
});
// Provider Settings (the POWER UP chip + dialog). Fire-and-forget: the
// module removes its own surface when the dev-server endpoint is absent
// (prod builds, non-local visitors), so this costs prod exactly nothing.
void initKeySetup();
// Expose for debugging
// Idle render governor: flips the scene into requestRenderMode whenever
// nothing animates per frame. Installed AFTER every module above has had
+2 -1
View File
@@ -1,6 +1,6 @@
// MAP STACK source chips — the always-visible replacement for the `<select>`
// that used to sit in the Map Stack panel. One button per stack, rendered from
// `MapStackController.getStacks()`. The four accepted sources below are
// `MapStackController.getStacks()`. The four owner-approved sources below are
// the whole shipped set; keeping the allowlist explicit means a stack added to
// `MAP_STACKS` for internal use cannot reach the tray until someone names it
// here.
@@ -15,6 +15,7 @@ export const PRESENTED_MAP_STACK_IDS = Object.freeze([
'photoreal',
'bing-aerial',
'bing-labels',
'esri-imagery',
'osm',
]);
+56 -12
View File
@@ -1,8 +1,8 @@
// MAP STACK chip row — the dropdown's replacement control surface.
//
// The product issue was two clicks (open panel → open dropdown) to change
// The owner's complaint was two clicks (open panel → open dropdown) to change
// basemap. These tests pin the three things that make the row a faithful swap:
// it projects the accepted four-source allowlist from the controller's
// it projects the owner-approved four-source allowlist from the controller's
// stack data, a click dispatches the same selection the `change` handler used
// to, and the lit chip tracks controller state rather than the click. Run with:
// npm test
@@ -66,20 +66,21 @@ const CONTROLLER_STACKS = [
{ id: 'photoreal', label: 'Google 3D', requiresIon: false, available: true, unavailableReason: null },
{ id: 'bing-aerial', label: 'Bing Aerial', requiresIon: true, available: true, unavailableReason: null },
{ id: 'bing-labels', label: 'Bing Labels', requiresIon: true, available: true, unavailableReason: null },
{ id: 'esri-imagery', label: 'Esri Satellite', requiresIon: false, available: true, unavailableReason: null },
{ id: 'osm', label: 'OSM', requiresIon: false, available: true, unavailableReason: null },
];
test('the row renders exactly the four accepted sources', () => {
test('the row renders exactly the five owner-approved sources', () => {
const container = makeElement();
renderMapStackChips(container, CONTROLLER_STACKS, { activeId: 'photoreal', doc });
assert.deepEqual(container.children.map((chip) => chip.dataset.stackId), [
'photoreal', 'bing-aerial', 'bing-labels', 'osm',
'photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm',
]);
assert.deepEqual(container.children.map(chipText), [
'Google 3D', 'Bing Aerial', 'Bing Labels', 'OSM',
'Google 3D', 'Bing Aerial', 'Bing Labels', 'Esri Satellite', 'OSM',
]);
assert.deepEqual(PRESENTED_MAP_STACK_IDS, ['photoreal', 'bing-aerial', 'bing-labels', 'osm']);
assert.deepEqual(PRESENTED_MAP_STACK_IDS, ['photoreal', 'bing-aerial', 'bing-labels', 'esri-imagery', 'osm']);
assert.ok(container.children.every((chip) => chip.tagName === 'button' && chip.type === 'button'));
assert.ok(container.children.every((chip) => chip.classList.contains(MAP_STACK_CHIP_CLASS)));
});
@@ -87,11 +88,11 @@ test('the row renders exactly the four accepted sources', () => {
test('internal and future stacks stay outside the approved presentation set', () => {
const container = makeElement();
// A future Hybrid stack may land in the controller, but it must not appear
// until the accepted presentation allowlist explicitly includes it.
// until the owner-approved presentation allowlist explicitly includes it.
const withHybrid = [...CONTROLLER_STACKS, { id: 'hybrid', label: 'Hybrid', available: true }];
renderMapStackChips(container, withHybrid, { activeId: 'photoreal', doc });
assert.equal(container.children.length, 4);
assert.equal(container.children.length, 5);
assert.doesNotMatch(container.children.map(chipText).join(' '), /Hybrid/);
});
@@ -112,9 +113,10 @@ test('clicking a chip dispatches that stack id — the same selection the dropdo
doc,
});
container.children[4].click();
container.children[3].click();
container.children[1].click();
assert.deepEqual(selected, ['osm', 'bing-aerial']);
assert.deepEqual(selected, ['osm', 'esri-imagery', 'bing-aerial']);
});
test('the active chip is the pressed chip, and exactly one is pressed', () => {
@@ -136,12 +138,12 @@ test('the lit chip tracks controller state, not the click', () => {
// A rejected/superseded switch reports the stack that is genuinely active.
syncMapStackChips(container, 'photoreal');
assert.ok(container.children[0].classList.contains('active'));
assert.equal(container.children[3].getAttribute('aria-pressed'), 'false');
assert.equal(container.children[4].getAttribute('aria-pressed'), 'false');
// A landed switch moves both the class and the pressed state.
syncMapStackChips(container, 'osm');
assert.ok(container.children[3].classList.contains('active'));
assert.equal(container.children[3].getAttribute('aria-pressed'), 'true');
assert.ok(container.children[4].classList.contains('active'));
assert.equal(container.children[4].getAttribute('aria-pressed'), 'true');
assert.ok(!container.children[0].classList.contains('active'));
assert.equal(container.children[0].getAttribute('aria-pressed'), 'false');
});
@@ -354,4 +356,46 @@ test('the Visual Presets tray owns Map Source and the retired left panel is abse
/_renderMapStackState\(state\) \{[\s\S]*?syncMapStackChips\(this\._mapStackChips, state\.activeId\)/,
'the active chip must be re-synced from controller state',
);
assert.match(
ui,
/window\.addEventListener\('gev:map-stack-changed', this\._mapStackChangeHandler\)/,
'provider-driven fallback must re-sync the UI without a user click',
);
assert.match(
ui,
/window\.removeEventListener\('gev:map-stack-changed', this\._mapStackChangeHandler\)/,
'the provider-driven state listener must be released with StyleManager',
);
});
test('Esri fallbacks report and attribute the imagery source actually rendered', () => {
const controller = readFileSync(new URL('./mapStackController.js', import.meta.url), 'utf8');
assert.match(
controller,
/effectiveStackId = 'osm';[\s\S]*?fallbackMessage = 'Esri Satellite is unavailable; using OSM'/,
'construction failure must resolve truthfully to OSM',
);
assert.match(
controller,
/this\._activeId = activation\?\.effectiveStackId \|\| stack\.id/,
'the active chip must follow the effective provider, not the requested stack',
);
assert.match(
controller,
/this\._syncEsriAttribution\(resolution\.effectiveStackId\)/,
'Esri credit must follow the effective provider',
);
});
test('repeated active Esri tile failures fall back to OSM and one transient does not', () => {
const controller = readFileSync(new URL('./mapStackController.js', import.meta.url), 'utf8');
assert.match(controller, /let failures = 0/);
assert.match(controller, /if \(failures < 2 \|\| this\._esriFallbackPending\) return/);
assert.match(controller, /this\.setStack\('osm', \{ silent: true \}\)/);
assert.match(controller, /state\?\.activeId === 'osm'[\s\S]*?this\._emitChange\('error'\)/);
assert.match(
controller,
/gen !== this\._switchGen \|\| this\._activeImageryProvider !== resolution\.provider/,
'a stale provider error must not replace a newer user selection',
);
});
+135 -10
View File
@@ -25,6 +25,13 @@ export const MAP_STACKS = [
style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
requiresIon: true,
},
{
id: 'esri-imagery',
label: 'Esri Satellite',
shortLabel: 'SAT',
kind: 'esri-imagery',
requiresIon: false,
},
{
id: 'osm',
label: 'OSM',
@@ -36,12 +43,27 @@ export const MAP_STACKS = [
const DEFAULT_OSM_CREDIT = '© OpenStreetMap contributors';
// Esri World Imagery — the keyless satellite basemap and the default keyless
// landing (a spy-satellite simulator should open on satellite imagery, not a
// street map). The classic ArcGIS Online tile service answers without a key;
// attribution is required and the provider carries the service's own credit
// line. Terms note in DATA_SOURCES.md.
const ESRI_WORLD_IMAGERY_URL =
'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer';
const ESRI_IMAGERY_CREDIT =
'Powered by Esri — Source: Esri, Maxar, Earthstar Geographics, and the GIS User Community';
// The on-screen notice Esri requires when a third-party library draws its
// service. Rendered via an explicit static credit (see _syncEsriAttribution) —
// the provider's own `credit` option is ignored for tiled ArcGIS servers.
const ESRI_ATTRIBUTION_HTML =
'<a href="https://www.esri.com" target="_blank" rel="noopener">Powered by Esri</a>';
// Keyless global ellipsoidal terrain (Re:Earth Terrain / Mapterhorn, CC BY 4.0,
// EGM2008 geoid via NGA) — quantized-mesh 1.0, `ellipsoid` data-type. Fixes
// regime C (keyless globe stacks previously rendered a flat
// EllipsoidTerrainProvider — see the height-datum contract in docs/CURRENT-STATE.md
// EllipsoidTerrainProvider — see docs/superpowers/specs/2026-07-05-entity-height-datum-design.md
// §1a). Constructed via `.fromUrl()`, never a hand-built `{z}/{x}/{y}.terrain`
// URL (spec correction, spec §1a).
// URL (review correction, spec §1a).
const REEARTH_TERRAIN_URL = 'https://terrain.reearth.land/cesium-mesh/ellipsoid';
/**
@@ -62,8 +84,11 @@ export class MapStackController {
this.cesiumToken = String(cesiumToken || '').trim();
this._onChange = onChange;
this._onError = onError;
this._activeId = googleTileset ? initialStack : 'osm';
this._activeId = googleTileset ? initialStack : 'esri-imagery';
this._imageryLayer = null;
this._activeImageryProvider = null;
this._removeImageryErrorListener = null;
this._esriFallbackPending = false;
this._imageryProviders = new Map();
this._isSwitching = false;
this._lastError = null;
@@ -89,7 +114,7 @@ export class MapStackController {
this._switchGen = 0;
if (!this.getStack(this._activeId) || !this.isStackAvailable(this._activeId)) {
this._activeId = googleTileset ? 'photoreal' : 'osm';
this._activeId = googleTileset ? 'photoreal' : 'esri-imagery';
}
}
@@ -171,15 +196,20 @@ export class MapStackController {
if (!silent) this._emitChange('switching');
try {
let activation = null;
if (stack.kind === 'photoreal') {
await this._activatePhotoreal(gen);
} else {
await this._activateGlobeStack(stack, gen);
activation = await this._activateGlobeStack(stack, gen);
}
// A newer switch started while we were awaiting the provider — that call
// owns the final state now, so don't commit ours or emit a stale 'ready'.
if (gen !== this._switchGen) return this.getState();
this._activeId = stack.id;
this._activeId = activation?.effectiveStackId || stack.id;
if (activation?.fallbackMessage) {
this._lastError = activation.fallbackMessage;
this._onError?.(activation.fallbackMessage, stack);
}
// Show/hide of tilesets + imagery swaps need a frame in idle mode;
// subsequent tile loads self-request via Cesium. (perf wave 2)
governorRequestRender('map-stack');
@@ -217,6 +247,7 @@ export class MapStackController {
async _activatePhotoreal(gen) {
this._removeImageryLayer();
this._syncEsriAttribution(null); // Esri is no longer on screen.
if (this.googleTileset) this.googleTileset.show = true;
this.viewer.scene.globe.show = false;
// Terrain is left UNTOUCHED here. The photoreal globe is hidden
@@ -234,18 +265,51 @@ export class MapStackController {
}
async _activateGlobeStack(stack, gen) {
const provider = await this._getImageryProvider(stack);
const resolution = await this._getImageryProvider(stack);
// A newer switch started while the provider was resolving — don't touch the
// scene's imagery layers, the winning switch already owns them (M7).
if (gen != null && gen !== this._switchGen) return;
this._removeImageryLayer();
this._imageryLayer = new Cesium.ImageryLayer(provider);
this._imageryLayer = new Cesium.ImageryLayer(resolution.provider);
this._activeImageryProvider = resolution.provider;
this.viewer.imageryLayers.add(this._imageryLayer, 0);
this._syncEsriAttribution(resolution.effectiveStackId);
this._watchEsriProvider(resolution, gen);
if (this.googleTileset) this.googleTileset.show = false;
this.viewer.scene.globe.show = true;
await this._setWorldTerrainEnabled(!!this.cesiumToken, gen);
return resolution;
}
/**
* Show or hide the required "Powered by Esri" notice with the Esri layer's
* own lifecycle.
*
* This cannot ride on the provider's `credit` option: Cesium IGNORES that
* option for tiled ArcGIS MapServer sources, so passing it there displays
* nothing and the app would be using the service without the attribution
* Esri requires of third-party libraries. It is an ON-SCREEN credit (not the
* lightbox, where per-layer data credits live) because that is what the
* requirement asks for, and it is removed when another stack takes over so
* the globe never claims a source it is not showing.
*/
_syncEsriAttribution(activeStackId) {
const creditDisplay = this.viewer?.scene?.frameState?.creditDisplay;
if (!creditDisplay) return;
const wanted = activeStackId === 'esri-imagery';
if (wanted === !!this._esriCreditShown) return;
if (!this._esriCredit) {
this._esriCredit = new Cesium.Credit(ESRI_ATTRIBUTION_HTML, true);
}
try {
if (wanted) creditDisplay.addStaticCredit(this._esriCredit);
else creditDisplay.removeStaticCredit(this._esriCredit);
this._esriCreditShown = wanted;
} catch {
// A Cesium build without static-credit removal must not break switching.
}
}
async _getImageryProvider(stack) {
@@ -254,8 +318,29 @@ export class MapStackController {
}
let provider;
let effectiveStackId = stack.id;
let fallbackMessage = null;
if (stack.kind === 'ion') {
provider = await Cesium.createWorldImageryAsync({ style: stack.style });
} else if (stack.kind === 'esri-imagery') {
try {
provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(ESRI_WORLD_IMAGERY_URL, {
credit: ESRI_IMAGERY_CREDIT,
enablePickFeatures: false,
});
} catch (error) {
// The keyless DEFAULT landing must never strand a first run on a blank
// globe because Esri is unreachable — fall back to OSM tiles for this
// session. (The fallback is cached under this stack id like any other
// provider, so the session won't re-probe Esri; a restart does.)
console.warn('[MapStack] Esri World Imagery unavailable, falling back to OSM:', error?.message || error);
provider = new Cesium.OpenStreetMapImageryProvider({
url: 'https://tile.openstreetmap.org/',
credit: DEFAULT_OSM_CREDIT,
});
effectiveStackId = 'osm';
fallbackMessage = 'Esri Satellite is unavailable; using OSM';
}
} else if (stack.kind === 'osm') {
provider = new Cesium.OpenStreetMapImageryProvider({
url: 'https://tile.openstreetmap.org/',
@@ -265,14 +350,54 @@ export class MapStackController {
throw new Error(`Unsupported map stack: ${stack.id}`);
}
this._imageryProviders.set(stack.id, provider);
return provider;
const resolution = { provider, effectiveStackId, fallbackMessage };
this._imageryProviders.set(stack.id, resolution);
if (effectiveStackId === 'osm' && !this._imageryProviders.has('osm')) {
this._imageryProviders.set('osm', { provider, effectiveStackId: 'osm', fallbackMessage: null });
}
return resolution;
}
/**
* Esri provider construction can succeed while its first tile requests fail.
* Two failures for the active provider trigger the same truthful OSM fallback
* as a construction failure; one transient error is left to Cesium's retry.
*/
_watchEsriProvider(resolution, gen) {
if (resolution.effectiveStackId !== 'esri-imagery') return;
const errorEvent = resolution.provider?.errorEvent;
if (!errorEvent?.addEventListener) return;
let failures = 0;
this._removeImageryErrorListener = errorEvent.addEventListener((error) => {
if (gen !== this._switchGen || this._activeImageryProvider !== resolution.provider) return;
const retryCount = Number(error?.timesRetried);
failures = Number.isInteger(retryCount) && retryCount >= 0
? Math.max(failures + 1, retryCount + 1)
: failures + 1;
if (failures < 2 || this._esriFallbackPending) return;
this._esriFallbackPending = true;
const message = 'Esri Satellite tile requests failed; using OSM';
this._onError?.(message, this.getStack('esri-imagery'));
void this.setStack('osm', { silent: true }).then((state) => {
if (state?.activeId === 'osm') {
this._lastError = message;
this._emitChange('error');
}
}).finally(() => {
this._esriFallbackPending = false;
});
});
}
_removeImageryLayer() {
if (this._removeImageryErrorListener) {
this._removeImageryErrorListener();
this._removeImageryErrorListener = null;
}
if (!this._imageryLayer) return;
this.viewer.imageryLayers.remove(this._imageryLayer, false);
this._imageryLayer = null;
this._activeImageryProvider = null;
}
/**
+51
View File
@@ -0,0 +1,51 @@
const clean = (value) => String(value || '').trim();
/**
* Decide which map provider can deliver the best startup experience.
* @param {{googleApiKey?: string, cesiumToken?: string}} credentials
* @returns {'google-direct'|'google-ion'|'osm'}
*/
export function selectMapStartupRoute({ googleApiKey = '', cesiumToken = '' } = {}) {
if (clean(googleApiKey)) return 'google-direct';
if (clean(cesiumToken)) return 'google-ion';
return 'osm';
}
/**
* Load Google Photorealistic 3D Tiles through direct Google access when
* configured, otherwise through Cesium ion's hosted Google asset. If the
* direct request fails and an ion token is available, ion is the recovery path.
*
* @param {object} Cesium
* @param {{googleApiKey?: string, cesiumToken?: string}} credentials
* @returns {Promise<{tileset: object|null, route: 'google-direct'|'google-ion'|'osm', errors: Error[]}>}
*/
export async function loadPhotorealisticTileset(
Cesium,
{ googleApiKey = '', cesiumToken = '' } = {},
) {
const googleKey = clean(googleApiKey);
const ionToken = clean(cesiumToken);
const errors = [];
if (ionToken) Cesium.Ion.defaultAccessToken = ionToken;
const attempts = [];
if (googleKey) attempts.push({ route: 'google-direct', googleKey });
if (ionToken) attempts.push({ route: 'google-ion', googleKey: undefined });
for (const attempt of attempts) {
Cesium.GoogleMaps.defaultApiKey = attempt.googleKey;
try {
const tileset = await Cesium.createGooglePhotorealistic3DTileset({
onlyUsingWithGoogleGeocoder: true,
});
return { tileset, route: attempt.route, errors };
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
Cesium.GoogleMaps.defaultApiKey = undefined;
return { tileset: null, route: 'osm', errors };
}
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more