From be7187cea7ed771e7461eb2bc41a3310ecb9b4ff Mon Sep 17 00:00:00 2001 From: daikaginza Date: Mon, 21 Sep 2026 10:02:19 +0700 Subject: [PATCH 01/50] fix(server): bound the terrain-heights cache and its request rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every distinct 5-decimal point was a permanent in-memory entry, a permanent line in an uncapped `.gev-cache/terrain-heights.json`, and a live upstream call. Nothing limited the rate, and coordinates were checked for being finite rather than for being on Earth, so an out-of-range pair was still a valid cache key. Four bounds, each following a pattern this repo already uses: - WGS-84 range is enforced in `parseTerrainPoints`, at the edge, so no later stage has to. Poles and the antimeridian still parse. - The point cache holds at most 50,000 entries and evicts oldest-first, the shape `trackBackfillProxies` uses. Sized from measurement: this checkout's cache file held 6,189 entries in 1.0 MB of real use, so the ceiling is eight times that working set. Terrain does not move, so an evicted point costs one refetch and never a wrong answer. - A cache file above 24 MB is not read back, mirroring `rocketLaunchesProxy`. An unmeasurable file is not treated as an oversized one: without stat, readFile decides, exactly as before. - The route carries an always-on limiter of 90/min per client, 270 global — the `/api/overpass` band, and an order of magnitude above measured demand (a full test:track run drove 10 requests, peak 8/min). The ceilings are constructor options so they can be exercised as behaviour rather than asserted as constants; production passes none. They are destructured in the body because `proxyErrorResponses.test.mjs` slices these proxies to the first column-zero closing brace. --- CHANGELOG.md | 11 ++ docs/CURRENT-STATE.md | 11 ++ scripts/format-scope.json | 3 +- scripts/package-boundaries.json | 4 +- server/providers/terrain.js | 93 +++++++++++- src/data/terrainHeightsProxy.js | 8 ++ src/proxyErrorResponses.test.mjs | 4 + src/terrainHeightsBounds.test.mjs | 232 ++++++++++++++++++++++++++++++ 8 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 src/terrainHeightsBounds.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 29dbedfaa..f59c0c97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +- Bound what a caller can grow through `/api/terrain/heights`. Every distinct + 5-decimal point was a permanent in-memory entry, a permanent line in an + uncapped `.gev-cache/terrain-heights.json`, and a live upstream call, with + nothing limiting the rate and only a finite-number check on the coordinates. + Coordinates outside WGS-84 range are now rejected with `400`, the point cache + holds at most 50,000 entries and evicts oldest-first, a cache file above + 24 MB is not read back, and the route is limited to 90 requests/minute per + client (270 global). Evicting a point costs one refetch, never a wrong + answer, since terrain does not move. Ordinary use is unaffected: a full + `test:track` run drives 10 requests, peaking at 8/min. + - CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index fdb42fc0c..adf2e7995 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2064,6 +2064,17 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > tunings. Ambient overlap includes each sprite's own rendered extent. The > gated AIS, CCTV, and satellite passes retain an active-emphasis count so a > settled dim contact always completes its release after tracking ends. +> - **Terrain-height bounds:** because every distinct 5-decimal point is its own +> cache key, its own line in `.gev-cache/terrain-heights.json`, and its own +> upstream call, four limits bound what a caller can grow. Coordinates outside +> WGS-84 range are rejected with `400` at parse (finite is not the same as on +> Earth); the point cache holds at most 50,000 entries, evicting oldest-first +> — terrain does not move, so an evicted point costs one refetch and never a +> wrong answer; a cache file above 24 MB is not read back into memory (the +> ceiling `rocketLaunchesProxy` already applies); and the route carries an +> always-on limiter of 90 requests/minute/client, 270 global. That cap is an +> order of magnitude above measured demand — a full `npm run test:track` run +> drove 10 requests, peaking at 8/min. > - **Terrain-height resilience:** `/api/terrain/heights` caches canonical > 5-decimal points individually, reconstructs reordered/overlapping batches > in exact request order, and refreshes only missing or stale points. Upstream diff --git a/scripts/format-scope.json b/scripts/format-scope.json index eee546dd5..ca43cbbb3 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -839,5 +839,6 @@ "src/ui/imageryBoxTool.test.mjs", "src/ui/recentImagery.test.mjs", "src/layers/recentImagery/testDoubles.mjs", - "scripts/qa-recent-imagery.mjs" + "scripts/qa-recent-imagery.mjs", + "src/terrainHeightsBounds.test.mjs" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 189192f10..d12fc42ea 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -101,7 +101,9 @@ "exports": ["./server/providers/terrain"], "modules": [ "server/providers/terrain.js", - "src/data/terrainHeightsProxy.js" + "src/data/terrainHeightsProxy.js", + "server/providers/common/rate-limit.js", + "src/sources/rateLimit.js" ], "external": [] }, diff --git a/server/providers/terrain.js b/server/providers/terrain.js index e58a63ea5..ca9442db1 100644 --- a/server/providers/terrain.js +++ b/server/providers/terrain.js @@ -8,6 +8,7 @@ import { terrainPointKey, validTerrainResult, } from '../../src/data/terrainHeightsProxy.js'; +import { makeRateLimiter, clientKey } from './common/rate-limit.js'; /** * Re:Earth terrain point-height proxy: batched lon/lat → ellipsoidal height @@ -20,11 +21,27 @@ import { * Only missing/stale points go upstream; the response is rebuilt in exact * request order. Larger requests are chunked sequentially, and one failing * chunk does not discard the chunks that resolved. + * + * Bounds, because every distinct 5dp point is its own key: coordinates must be + * in WGS-84 range (rejected at parse), the point cache has an entry ceiling + * with oldest-first eviction, an over-large cache file is not read back, and + * the route carries an always-on per-client limiter. */ -export function terrainHeightsProxy() { +export function terrainHeightsProxy(options = {}) { + // Destructured in the body, not the signature: `proxyErrorResponses.test.mjs` + // extracts these proxies by slicing to the first closing brace in column + // zero, so a multi-line parameter list would cut the function in half. + // Overridable so the ceilings can be exercised as behaviour rather than + // asserted as constants; production constructs this with no arguments. + const { + cacheMaxEntries = 50_000, + cacheMaxDiskBytes = 24 * 1024 * 1024, + cachePath, + } = options; const TTL_MS = 30 * 24 * 3600_000; - const CACHE_DIR = path.join(process.cwd(), '.gev-cache'); - const CACHE_PATH = path.join(CACHE_DIR, 'terrain-heights.json'); + const CACHE_PATH = + cachePath || path.join(process.cwd(), '.gev-cache', 'terrain-heights.json'); + const CACHE_DIR = path.dirname(CACHE_PATH); // Sized against measured upstream latency, not the documented page cap. // Re:Earth serves 87-186 ms/point depending on load (observed 2026-09-12, // a 2x swing within one hour). 256 points therefore costs 22-48 s and blows @@ -34,9 +51,50 @@ export function terrainHeightsProxy() { // that range. const UPSTREAM_CHUNK = 64; const MAX_POINTS = 2000; + /** + * Entry ceiling for the point cache, and with it the file written from it. + * + * Every distinct 5dp point is a separate key, so without a ceiling the map + * — and the JSON dumped from it verbatim — grow for as long as a caller + * supplies new coordinates. Measured against real use rather than guessed: + * this checkout's `.gev-cache/terrain-heights.json` held 6,189 entries in + * 1.0 MB after ordinary sessions, i.e. ~168 bytes/entry. 50,000 entries is + * eight times that working set and ~8 MB on disk, comfortably inside the + * 24 MB ceiling `rocketLaunchesProxy` already applies to its own cache file. + */ + const CACHE_MAX_ENTRIES = cacheMaxEntries; + /** Refuse to load a cache file larger than this (mirrors rocketLaunchesProxy). */ + const CACHE_MAX_DISK_BYTES = cacheMaxDiskBytes; + /** + * Requests/min/client. Terrain is keyless and free, so this guards the point + * cache and the upstream community service rather than a bill. Sized from + * measurement: across a full `npm run test:track` run — the heaviest + * exercise this app has — the route served 10 requests, peaking at 8/min. + * 90/min is an order of magnitude above that, and the band `/api/overpass` + * and `/api/military-installations` already use. + */ + const RATE_PER_MIN = 90; + const allow = makeRateLimiter({ + windowMs: 60_000, + max: RATE_PER_MIN, + globalMax: RATE_PER_MIN * 3, + }); /** @type {Map} keyed by canonical 5dp lon/lat. */ const mem = new Map(); + + /** + * Drop the oldest entries until the map is back inside its ceiling. + * Oldest-first by fetch time, the same shape `trackBackfillProxies` uses. + * Terrain does not move, so evicting an old point costs one refetch, never + * a wrong answer. + */ + function evictOverflow() { + if (mem.size <= CACHE_MAX_ENTRIES) return; + const byAge = [...mem.entries()].sort((a, b) => a[1].at - b[1].at); + for (let i = 0; i < byAge.length && mem.size > CACHE_MAX_ENTRIES; i += 1) + mem.delete(byAge[i][0]); + } /** @type {Map>>} single-flight per missing-point subset. */ const inflight = new Map(); let diskLoaded = false; @@ -47,6 +105,21 @@ export function terrainHeightsProxy() { if (diskLoaded) return; diskLoaded = true; try { + // A cache file past the ceiling is not read back into memory: loading it + // would reinstate exactly the unbounded map the ceiling exists to stop. + // The next flush rewrites a bounded one. + // + // "Cannot measure it" is not "too large": if stat is unavailable, fall + // through and let readFile decide, so an absent file behaves exactly as + // it did before this guard existed. + let sizeBytes = null; + try { + sizeBytes = (await fsp.stat(CACHE_PATH)).size; + } catch { + sizeBytes = null; + } + if (sizeBytes !== null && sizeBytes > CACHE_MAX_DISK_BYTES) + throw new Error('cache file too large'); const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8')); const pointEntries = parsed?.version === 2 && @@ -87,6 +160,7 @@ export function terrainHeightsProxy() { } diskDirty = mem.size > 0; } + evictOverflow(); } catch { /* no disk cache yet */ } @@ -163,6 +237,14 @@ export function terrainHeightsProxy() { res.end(JSON.stringify(bodyObj)); }; try { + if (!allow(clientKey(req))) { + res.writeHead(429, { + 'Content-Type': 'application/json', + 'Retry-After': '10', + }); + res.end(JSON.stringify({ error: 'Rate limit exceeded' })); + return; + } await loadDiskOnce(); const parsedUrl = new URL(req.url || '', 'http://internal'); const rawPoints = parsedUrl.searchParams.get('points'); @@ -187,7 +269,10 @@ export function terrainHeightsProxy() { fetchMissing: fetchMissingSingleFlight, ttlMs: TTL_MS, }); - if (outcome.cacheChanged) diskDirty = true; + if (outcome.cacheChanged) { + evictOverflow(); + diskDirty = true; + } if (outcome.upstreamError) { console.warn( '[terrain-heights-proxy] refresh incomplete' + diff --git a/src/data/terrainHeightsProxy.js b/src/data/terrainHeightsProxy.js index bfb058693..11b8e0037 100644 --- a/src/data/terrainHeightsProxy.js +++ b/src/data/terrainHeightsProxy.js @@ -18,6 +18,13 @@ function defaultSleep(ms) { /** * Parse the proxy's `points=lon,lat;...` query parameter. + * + * Finite is not the same as on Earth. A coordinate outside WGS-84 range can + * never resolve to a height, but it is still a distinct 5dp cache key, so + * accepting it meant an out-of-range point became a permanent cache entry and + * a live upstream call. Range is checked here, at the edge, so no later stage + * has to. + * * @param {string|null|undefined} raw * @returns {Array<[number, number]>|null} */ @@ -36,6 +43,7 @@ export function parseTerrainPoints(raw) { const lon = Number(parts[0]); const lat = Number(parts[1]); if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null; + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null; points.push([lon, lat]); } return points; diff --git a/src/proxyErrorResponses.test.mjs b/src/proxyErrorResponses.test.mjs index 2b45a3b9e..9cc1ff9c7 100644 --- a/src/proxyErrorResponses.test.mjs +++ b/src/proxyErrorResponses.test.mjs @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { readResponseTextCapped, coalesceProxyRequest } from './sources/httpBody.js'; +import { makeRateLimiter, clientKey } from '../server/providers/common/rate-limit.js'; const source = ['local.js', 'common/http.js', 'aircraft/enrichment.js', 'terrain.js', 'space/celestrak.js', 'space/launch-library.js', '../../src/data/spaceProviderRequests.js'] .map(file => readFileSync(new URL(`../server/providers/${file}`, import.meta.url), 'utf8')) @@ -22,6 +23,9 @@ function fixture(name, overrides = {}, preview = false) { const logs = []; const deps = { readResponseTextCapped, coalesceProxyRequest, + // The rate-limited proxies reach for these; give them the real ones so a + // fixture request is throttled exactly as production would be. + makeRateLimiter, clientKey, path, process: { cwd: () => '/fixture', env: {} }, fsp: { readFile: async () => { throw new Error('cache absent'); }, diff --git a/src/terrainHeightsBounds.test.mjs b/src/terrainHeightsBounds.test.mjs new file mode 100644 index 000000000..1a0c80290 --- /dev/null +++ b/src/terrainHeightsBounds.test.mjs @@ -0,0 +1,232 @@ +// TERRAIN HEIGHTS — the bounds on a cache keyed by caller-chosen coordinates. +// +// Every distinct 5dp lon/lat is its own cache key, its own permanent line in +// `.gev-cache/terrain-heights.json`, and its own upstream call. Four things +// bound that: WGS-84 range at parse, an entry ceiling on the map, a size +// ceiling on the file that is read back, and a per-client request limiter. +// +// These cases drive the real exported route handler. `fetch` is stubbed per +// test and restored after, so a case that reaches upstream fails loudly. +// +// Run with: npm test +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { parseTerrainPoints } from './data/terrainHeightsProxy.js'; +import { terrainHeightsProxy } from '../server/providers/terrain.js'; + +function install(plugin) { + const routes = new Map(); + plugin.configureServer({ + middlewares: { + use(path, handler) { + routes.set(path, handler); + }, + }, + }); + return routes; +} + +function request(handler, { url = '/', remoteAddress = '127.0.0.1' } = {}) { + return new Promise((resolve, reject) => { + const headers = new Map(); + const req = { method: 'GET', url, headers: {}, socket: { remoteAddress } }; + const res = { + statusCode: 200, + headersSent: false, + setHeader(name, value) { + headers.set(String(name).toLowerCase(), String(value)); + }, + writeHead(status, hdrs = {}) { + this.statusCode = status; + this.headersSent = true; + for (const [name, value] of Object.entries(hdrs)) + headers.set(String(name).toLowerCase(), String(value)); + }, + end(body = '') { + let parsed = null; + try { + parsed = body ? JSON.parse(String(body)) : null; + } catch { + parsed = String(body); + } + resolve({ + statusCode: this.statusCode, + headers: Object.fromEntries(headers), + body: parsed, + }); + }, + }; + Promise.resolve(handler(req, res)).catch(reject); + }); +} + +function stubFetch(t, impl) { + const original = globalThis.fetch; + globalThis.fetch = impl; + t.after(() => { + globalThis.fetch = original; + }); +} + +/** + * Answer like Re:Earth does: one `{ellipsoid}` per requested point, in order. + * Records which point strings were actually asked for, so a test can assert on + * cache misses rather than on internal state. + */ +function stubTerrainUpstream(t) { + const asked = []; + stubFetch(t, async (url) => { + const points = decodeURIComponent( + new URL(String(url)).searchParams.get('points') || '', + ) + .split(';') + .filter(Boolean); + asked.push(...points); + return Response.json({ results: points.map(() => ({ ellipsoid: 150 })) }); + }); + return asked; +} + +test('a coordinate off the globe is not a cache key', () => { + // Finite but not on Earth — the old guard accepted every one of these. + for (const raw of [ + '0,91', + '0,-91', + '181,0', + '-181,0', + '1e9,1e9', + '0,90.00001', + ]) + assert.equal(parseTerrainPoints(raw), null, raw); + + // The poles and the antimeridian are real places and must still parse. + for (const raw of ['0,90', '0,-90', '180,0', '-180,0', '-97.74,30.26']) + assert.ok(parseTerrainPoints(raw), raw); +}); + +test('an out-of-range point is refused before it reaches upstream or the cache', async (t) => { + let upstreamCalls = 0; + stubFetch(t, async () => { + upstreamCalls += 1; + return new Response('[]', { status: 200 }); + }); + const route = install(terrainHeightsProxy()).get('/api/terrain/heights'); + + const refused = await request(route, { url: '/?points=0,91' }); + assert.equal(refused.statusCode, 400); + assert.equal( + upstreamCalls, + 0, + 'an impossible coordinate must not cost an upstream call', + ); +}); + +test('one out-of-range point rejects the batch it is hidden in', async (t) => { + stubFetch(t, async () => new Response('[]', { status: 200 })); + const route = install(terrainHeightsProxy()).get('/api/terrain/heights'); + const mixed = await request(route, { + url: '/?points=-97.74,30.26;0,999;-97.75,30.27', + }); + assert.equal(mixed.statusCode, 400); +}); + +test('the point cache stops growing, and an evicted point is simply refetched', async (t) => { + const asked = stubTerrainUpstream(t); + // A ceiling of 3 exercises the same code path 50,000 does, in four requests. + const route = install(terrainHeightsProxy({ cacheMaxEntries: 3 })).get( + '/api/terrain/heights', + ); + + const point = (i) => `-97.7${i}000,30.26000`; + for (let i = 1; i <= 4; i += 1) + await request(route, { url: `/?points=${point(i)}` }); + assert.equal(asked.length, 4, 'four distinct points, four upstream lookups'); + + // The newest point is still cached... + await request(route, { url: `/?points=${point(4)}` }); + assert.equal(asked.length, 4, 'a cached point costs nothing'); + + // ...and the oldest was evicted, so it costs one refetch — never a wrong + // answer, because terrain does not move. + await request(route, { url: `/?points=${point(1)}` }); + assert.equal(asked.length, 5, 'the evicted point was asked for again'); + assert.equal(asked[4], point(1)); +}); + +test('an over-large cache file is not read back into memory', async (t) => { + const asked = stubTerrainUpstream(t); + const point = '-97.74000,30.26000'; + const dir = await mkdtemp(path.join(tmpdir(), 'gev-terrain-')); + const cachePath = path.join(dir, 'terrain-heights.json'); + t.after(() => rm(dir, { recursive: true, force: true })); + + // A cache file holding the answer, but larger than the ceiling allows. + const padded = { + version: 2, + points: { [point]: { at: Date.now(), result: { ellipsoid: 150 } } }, + _pad: 'x'.repeat(4096), + }; + await writeFile(cachePath, JSON.stringify(padded), 'utf8'); + + const route = install( + terrainHeightsProxy({ cachePath, cacheMaxDiskBytes: 1024 }), + ).get('/api/terrain/heights'); + await request(route, { url: `/?points=${point}` }); + assert.deepEqual( + asked, + [point], + 'the oversized file was skipped, so the point was a miss', + ); + + // The same file under a ceiling that admits it IS used, which is what proves + // the previous assertion came from the size guard and not a broken path. + const admitted = install( + terrainHeightsProxy({ cachePath, cacheMaxDiskBytes: 1024 * 1024 }), + ).get('/api/terrain/heights'); + await request(admitted, { url: `/?points=${point}` }); + assert.deepEqual(asked, [point], 'a cache within the ceiling is still read'); +}); + +test('the route refuses a flood of requests from one client', async (t) => { + stubTerrainUpstream(t); + const route = install(terrainHeightsProxy()).get('/api/terrain/heights'); + + let refusedAt = 0; + for (let i = 1; i <= 95; i += 1) { + // A different point each time: distinct keys are the growth path. + const lon = (-97.7 - i / 1000).toFixed(5); + const answer = await request(route, { url: `/?points=${lon},30.26` }); + if (answer.statusCode === 429) { + refusedAt = i; + break; + } + } + assert.equal(refusedAt, 91, 'the 91st request in a minute is refused'); +}); + +test('a second client keeps its own quota', async (t) => { + stubFetch(t, async () => + Response.json([{ lng: -97.74, lat: 30.26, height: 150 }]), + ); + const route = install(terrainHeightsProxy()).get('/api/terrain/heights'); + + for (let i = 0; i < 90; i += 1) + await request(route, { + url: `/?points=${(-98 - i / 1000).toFixed(5)},30.26`, + remoteAddress: '198.51.100.7', + }); + const exhausted = await request(route, { + url: '/?points=-99.5,30.26', + remoteAddress: '198.51.100.7', + }); + assert.equal(exhausted.statusCode, 429); + + const neighbour = await request(route, { + url: '/?points=-99.6,30.26', + remoteAddress: '198.51.100.8', + }); + assert.notEqual(neighbour.statusCode, 429); +}); From a4ce7381d88f7da849d80010fe1d8979457b754c Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:32:50 -0500 Subject: [PATCH 02/50] fix(ui): enforce Context ownership before disclosure no-op --- src/ui/panelChrome.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 30266312a..6464f1588 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -474,6 +474,17 @@ export class PanelChrome { syncShare, }); } + // Parent ownership is an invariant even on a no-op disclosure request. + // A stale compact Radio disclosure is absolutely positioned outside the + // measured Context rail; clear it before returning from an already-open + // Context request so it cannot paint over the active Context body. + if ( + !nextCollapsed && + panelId === 'global-context-panel' && + this._contextRadioDock?.classList.contains('disclosure-open') + ) { + this._setRadioDisclosure?.(false); + } if ( panelEl.classList.contains('collapsed') === nextCollapsed && !wasAutoCollapsed @@ -502,13 +513,6 @@ export class PanelChrome { this.cockpitView.setContextCollapsed(true); } } - if ( - !nextCollapsed && - panelId === 'global-context-panel' && - this._contextRadioDock?.classList.contains('disclosure-open') - ) { - this._setRadioDisclosure?.(false); - } if ( !nextCollapsed && panelId === 'radio-panel' && From b79ab84a1a5efaffea6eb2540142b22c34a67ca2 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:33:09 -0500 Subject: [PATCH 03/50] test(ui): pin Context and compact Radio ownership --- src/ui/panelChrome.radioOwnership.test.mjs | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/ui/panelChrome.radioOwnership.test.mjs diff --git a/src/ui/panelChrome.radioOwnership.test.mjs b/src/ui/panelChrome.radioOwnership.test.mjs new file mode 100644 index 000000000..c52a4888e --- /dev/null +++ b/src/ui/panelChrome.radioOwnership.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { PanelChrome } from './panelChrome.js'; + +function classList(initial = []) { + const values = new Set(initial); + return { + contains: (value) => values.has(value), + add: (...items) => items.forEach((item) => values.add(item)), + remove: (...items) => items.forEach((item) => values.delete(item)), + toggle(value, force) { + if (force === true) values.add(value); + else if (force === false) values.delete(value); + else if (values.has(value)) values.delete(value); + else values.add(value); + return values.has(value); + }, + }; +} + +test('opening an already-open Context still closes a stale compact Radio disclosure', (t) => { + const priorDocument = globalThis.document; + const contextPanel = { + id: 'global-context-panel', + classList: classList(), + }; + globalThis.document = { + documentElement: { dataset: { uiTheme: 'tactical' } }, + getElementById(id) { + return id === 'global-context-panel' ? contextPanel : null; + }, + }; + t.after(() => { + globalThis.document = priorDocument; + }); + + const radioDock = { classList: classList(['disclosure-open']) }; + const calls = []; + const owner = { + _contextRadioDock: radioDock, + _setRadioDisclosure(open) { + calls.push(open); + radioDock.classList.toggle('disclosure-open', open); + }, + _leftPanelStack: null, + _rightPanelStack: null, + _panelLayout: { + _leftStackPreferredPanelId: null, + _rightStackPreferredPanelId: null, + }, + _syncPanelCollapseButton() {}, + _scheduleLeftPanelLayout() {}, + _scheduleRightPanelLayout() {}, + shareLinkManager: null, + cockpitView: null, + }; + + PanelChrome.prototype.setPanelCollapsed.call( + owner, + 'global-context-panel', + false, + ); + + assert.deepEqual(calls, [false]); + assert.equal(radioDock.classList.contains('disclosure-open'), false); +}); From b22e6d519a0387b846a71a3069dc8c66797bf52d Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:33:11 -0500 Subject: [PATCH 04/50] chore(format): adopt panel ownership test --- scripts/format-scope.json | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index ef44df03f..507ee3126 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -651,6 +651,7 @@ "src/ui/visualSettings.js", "src/ui/panelChrome.js", "src/testSupport/readShellSource.mjs", + "src/ui/panelChrome.radioOwnership.test.mjs", "src/ui/stateOwners.test.mjs", "src/layers/flights/records.js", "src/layers/flights/recordPolicy.js", From 4fc813e1752b8e38f6051c6ddcaf0e3e3a97947a Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:02:02 -0500 Subject: [PATCH 05/50] feat(agent): extract model-free World Packet boundary --- src/agent/worldPacket.js | 184 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/agent/worldPacket.js diff --git a/src/agent/worldPacket.js b/src/agent/worldPacket.js new file mode 100644 index 000000000..c492f3d6b --- /dev/null +++ b/src/agent/worldPacket.js @@ -0,0 +1,184 @@ +import * as Cesium from 'cesium'; +import { + feedProvenanceEnvelope, + layerSnapshots, + viewStateLayerRow, +} from '../data/layerSnapshot.js'; +import { contextModeWord } from '../contextModePolicy.js'; + +const TRACKABLE_FAMILIES = Object.freeze([ + { layerId: 'flights', kind: 'aircraft' }, + { layerId: 'military', kind: 'aircraft' }, + { layerId: 'ais-live-vessels', kind: 'vessel' }, + { layerId: 'satellites', kind: 'satellite' }, +]); + +const CONTEXT_MODE_RESULT_FIELDS = Object.freeze([ + { field: 'mode', emptyAs: 'off' }, + { field: 'entering', emptyAs: null }, + { field: 'priorMode', emptyAs: null }, +]); + +const NESTED_CONTEXT_RESULT_FIELDS = Object.freeze([ + 'context', + 'contextRollback', +]); + +const NON_SEMANTIC_PACKET_KEYS = new Set([ + 'note', + 'ageLabel', + 'generatedAt', +]); + +function semanticPacketValue(value) { + if (Array.isArray(value)) return value.map(semanticPacketValue); + if (!value || typeof value !== 'object') return value; + const out = {}; + for (const key of Object.keys(value).sort()) { + if (NON_SEMANTIC_PACKET_KEYS.has(key)) continue; + out[key] = semanticPacketValue(value[key]); + } + return out; +} + +/** + * Stable semantic fingerprint for change detection, not authentication. + * + * Presentation-only narration/age labels are excluded. Feed state, source, + * lastUpdate, camera, context, tracked entities, and other semantic state stay + * inside the digest. + */ +export function semanticWorldPacketFingerprint(packet) { + const canonical = JSON.stringify(semanticPacketValue(packet)); + let hash = 0xcbf29ce484222325n; + const prime = 0x100000001b3n; + for (let i = 0; i < canonical.length; i += 1) { + hash ^= BigInt(canonical.charCodeAt(i)); + hash = BigInt.asUintN(64, hash * prime); + } + return `fnv1a64:${hash.toString(16).padStart(16, '0')}`; +} + +/** + * Translate context-mode payloads into the public tool vocabulary while + * retaining the internal id beside each translated field. + */ +export function withContextModeVocabulary(state) { + if (!state || typeof state !== 'object') return state; + let out = state; + const mutable = () => { + if (out === state) out = { ...state }; + return out; + }; + for (const { field, emptyAs } of CONTEXT_MODE_RESULT_FIELDS) { + if (!(field in state)) continue; + const internal = state[field] ?? null; + const target = mutable(); + target[field] = contextModeWord(internal, { emptyAs }); + target[`${field}Internal`] = internal; + } + for (const field of NESTED_CONTEXT_RESULT_FIELDS) { + const nested = state[field]; + if (!nested || typeof nested !== 'object') continue; + const translated = withContextModeVocabulary(nested); + if (translated !== nested) mutable()[field] = translated; + } + return out; +} + +/** Read tracked entities from their canonical layer owners. */ +export function collectTrackedEntities(dataManager) { + const tracked = []; + for (const family of TRACKABLE_FAMILIES) { + const module = dataManager?.layers?.get(family.layerId)?.module; + if (!module) continue; + try { + const info = + family.kind === 'vessel' + ? module.getSelectedInfo?.() + : module.getTrackedInfo?.(); + if (info) + tracked.push({ + kind: family.kind, + layerId: family.layerId, + ...info, + }); + } catch { + // A not-yet-ready layer contributes no tracked observation. + } + } + return tracked; +} + +/** Read the active Contacts window from its owning awareness layer. */ +export function activeContactsWindow(dataManager) { + try { + const layer = dataManager?.layers?.get('military-awareness')?.module; + return ( + layer?.contactsWindowFromSnapshot?.(layer.getContextSnapshot?.()) ?? null + ); + } catch { + return null; + } +} + +/** + * Build the model-free fast World Packet from existing application owners. + * + * This performs no network requests and intentionally mirrors the historical + * get_current_view_state payload so voice can delegate without behavior drift. + */ +export function getWorldPacket({ + viewer, + styleManager, + dataManager, + sceneDirector = null, + now = Date.now(), +} = {}) { + if (!viewer?.camera?.positionWC) + throw new TypeError('World Packet requires a viewer camera'); + if (!dataManager || typeof dataManager.getAll !== 'function') + throw new TypeError('World Packet requires a data manager'); + + const cartographic = Cesium.Cartographic.fromCartesian( + viewer.camera.positionWC, + ); + const rows = dataManager.getAll(); + const contactsWindow = activeContactsWindow(dataManager); + const contextState = + typeof styleManager?.getContextModeState === 'function' + ? withContextModeVocabulary(styleManager.getContextModeState()) + : null; + + return { + ok: true, + action: 'get_current_view_state', + camera: { + latitude: Cesium.Math.toDegrees(cartographic.latitude), + longitude: Cesium.Math.toDegrees(cartographic.longitude), + heightM: cartographic.height, + }, + style: styleManager?.activeStyle || 'normal', + context: contextState + ? { + ...contextState, + ...(contactsWindow ? { contactsWindow } : {}), + } + : null, + cockpit: + typeof styleManager?.getCockpitState === 'function' + ? styleManager.getCockpitState() + : null, + controls: + typeof styleManager?.getControlState === 'function' + ? styleManager.getControlState() + : null, + scenePlayback: sceneDirector?.getPlaybackStatus?.() || null, + tracked: collectTrackedEntities(dataManager), + layers: rows.map((layer) => viewStateLayerRow(layer, { now })), + feedProvenance: feedProvenanceEnvelope( + layerSnapshots(rows, { now }).filter((layer) => layer.enabled), + { now }, + ), + }; +} From 5c044a3d8da61eb707c7b61e16e42d6c72fc2ad3 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:02:05 -0500 Subject: [PATCH 06/50] test(agent): prove World Packet parity and fingerprint semantics --- src/agent/worldPacket.test.mjs | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/agent/worldPacket.test.mjs diff --git a/src/agent/worldPacket.test.mjs b/src/agent/worldPacket.test.mjs new file mode 100644 index 000000000..9d9e0bd7d --- /dev/null +++ b/src/agent/worldPacket.test.mjs @@ -0,0 +1,110 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as Cesium from 'cesium'; + +import { + getWorldPacket, + semanticWorldPacketFingerprint, +} from './worldPacket.js'; +import { createGevActionRunner } from '../voice/gevActions.js'; + +function harness() { + const position = Cesium.Cartesian3.fromDegrees(-97.74, 30.26, 500); + const viewer = { + trackedEntity: undefined, + clock: { onTick: { addEventListener: () => () => {} } }, + scene: { + canvas: { + clientWidth: 1200, + clientHeight: 800, + addEventListener() {}, + removeEventListener() {}, + }, + globe: { getHeight: () => 0 }, + tweens: [], + }, + camera: { + positionWC: position, + positionCartographic: Cesium.Cartographic.fromCartesian(position), + heading: 0, + pitch: Cesium.Math.toRadians(-45), + moveEnd: { addEventListener: () => () => {} }, + cancelFlight() {}, + lookAtTransform() {}, + }, + }; + const rows = [ + { + id: 'traffic', + name: 'Traffic', + enabled: true, + stats: { + count: 3, + source: 'fixture', + lastUpdate: null, + }, + }, + ]; + const dataManager = { + layers: new Map([['traffic', { module: null }]]), + getAll: () => rows, + }; + const styleManager = { + activeStyle: 'normal', + getContextModeState: () => ({ mode: 'flights' }), + getCockpitState: () => null, + getControlState: () => ({ hud: { visible: 'auto' } }), + }; + return { viewer, dataManager, styleManager }; +} + +test('World Packet preserves get_current_view_state parity', async () => { + const { viewer, dataManager, styleManager } = harness(); + const packet = getWorldPacket({ viewer, dataManager, styleManager }); + const runner = createGevActionRunner({ viewer, dataManager, styleManager }); + const voice = await runner('get_current_view_state'); + + assert.deepEqual(voice, packet); + assert.equal(packet.context.mode, 'contacts'); + assert.equal(packet.context.modeInternal, 'flights'); + assert.equal(packet.layers[0].feedState, 'nominal'); + assert.equal(packet.feedProvenance.overall, 'nominal'); +}); + +test('World Packet keeps stale/unavailable feed semantics distinct from empty', () => { + const { viewer, dataManager, styleManager } = harness(); + dataManager.getAll = () => [ + { + id: 'traffic', + name: 'Traffic', + enabled: true, + stats: { + count: 0, + source: 'fixture', + error: 'fixture unavailable', + lastUpdate: null, + }, + }, + ]; + + const packet = getWorldPacket({ viewer, dataManager, styleManager }); + assert.equal(packet.layers[0].count, 0); + assert.notEqual(packet.layers[0].feedState, 'nominal'); + assert.notEqual(packet.feedProvenance.overall, 'nominal'); + assert.match(packet.feedProvenance.note, /fixture unavailable/i); +}); + + +test('semantic fingerprint ignores narration prose but changes with observed state', () => { + const { viewer, dataManager, styleManager } = harness(); + const packet = getWorldPacket({ viewer, dataManager, styleManager }); + const original = semanticWorldPacketFingerprint(packet); + + const proseOnly = structuredClone(packet); + proseOnly.feedProvenance.note = 'different generated narration'; + assert.equal(semanticWorldPacketFingerprint(proseOnly), original); + + const changed = structuredClone(packet); + changed.layers[0].count += 1; + assert.notEqual(semanticWorldPacketFingerprint(changed), original); +}); From 3c163d09a15f2a152cb6ce6de09ff0e82db1a63e Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:02:08 -0500 Subject: [PATCH 07/50] refactor(voice): delegate world state to World Packet builder --- src/voice/gevActions.js | 73 ++++------------------------------------- 1 file changed, 7 insertions(+), 66 deletions(-) diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index cfdb1fb0b..cccae249e 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -39,6 +39,7 @@ import { unavailablePlaceSearch } from '../search/placeSearch.js'; import * as defaultAnnotationResolver from '../annotations/annotationResolver.js'; import { normalizeRadioCountryInput } from '../data/radioCountry.js'; import { TR3B_CLASS } from '../data/tr3bRegistry.js'; +import { getWorldPacket } from '../agent/worldPacket.js'; const ALLOWED_STYLES = new Set([ 'normal', @@ -2424,25 +2425,6 @@ function runManagedVoiceNavigation( } /** Gathers tracked/selected entities across layer families for read-back. */ -function collectTrackedEntities(dataManager) { - const tracked = []; - for (const family of TRACKABLE_FAMILIES) { - const module = dataManager.layers.get(family.layerId)?.module; - if (!module) continue; - try { - const info = - family.kind === 'vessel' - ? module.getSelectedInfo?.() - : module.getTrackedInfo?.(); - if (info) - tracked.push({ kind: family.kind, layerId: family.layerId, ...info }); - } catch { - // layer not ready - } - } - return tracked; -} - export async function getBasemapLabelContext( viewer, service = defaultGeospatial, @@ -3073,53 +3055,12 @@ function getCurrentViewState( dataManager, sceneDirector = null, ) { - const cartographic = Cesium.Cartographic.fromCartesian( - viewer.camera.positionWC, - ); - return { - ok: true, - action: 'get_current_view_state', - camera: { - latitude: Cesium.Math.toDegrees(cartographic.latitude), - longitude: Cesium.Math.toDegrees(cartographic.longitude), - heightM: cartographic.height, - }, - style: styleManager.activeStyle || 'normal', - context: - typeof styleManager.getContextModeState === 'function' - ? { - ...withContextModeVocabulary(styleManager.getContextModeState()), - // The numbers on the operator's Contacts panel, so a window/count - // question can be answered from what they are looking at. - ...(activeContactsWindow(dataManager) - ? { contactsWindow: activeContactsWindow(dataManager) } - : {}), - } - : null, - cockpit: - typeof styleManager.getCockpitState === 'function' - ? styleManager.getCockpitState() - : null, - controls: - typeof styleManager.getControlState === 'function' - ? styleManager.getControlState() - : null, - scenePlayback: sceneDirector?.getPlaybackStatus?.() || null, - tracked: collectTrackedEntities(dataManager), - layers: dataManager.getAll().map((layer) => ({ - id: layer.id, - name: layer.name, - enabled: layer.enabled, - count: layer.stats?.count || 0, - error: layer.stats?.error || null, - feedState: layerSnapshot(layer).feedState, - source: layerSnapshot(layer).source, - lastUpdate: layerSnapshot(layer).lastUpdate, - })), - feedProvenance: feedProvenanceEnvelope( - layerSnapshots(dataManager.getAll()).filter((layer) => layer.enabled), - ), - }; + return getWorldPacket({ + viewer, + styleManager, + dataManager, + sceneDirector, + }); } async function getEntityContext( From 6439575d158238ce92470106d221f5d450c6f29c Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:02:11 -0500 Subject: [PATCH 08/50] feat(agent): export World Packet boundary --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e48881c6d..0a299b51d 100644 --- a/package.json +++ b/package.json @@ -250,6 +250,7 @@ "./layers/transit/standalone": "./src/data/transit.js", "./layers/transit/source": "./src/layers/transit/source.js", "./sources/transit-service": "./src/sources/transitService.js", - "./sources/map-features": "./src/sources/overpassFeatures.js" + "./sources/map-features": "./src/sources/overpassFeatures.js", + "./agent/world-packet": "./src/agent/worldPacket.js" } } From c55d4d7c25877abb1a6eec331d82ad0a124bf61e Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:02:14 -0500 Subject: [PATCH 09/50] build: register World Packet package boundary --- scripts/package-boundaries.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 276b9e0db..42e98d736 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -3072,5 +3072,15 @@ "src/layers/perimeters/source.js" ], "external": [] + }, + "agent-world-packet": { + "exports": ["./agent/world-packet"], + "modules": [ + "src/agent/worldPacket.js", + "src/data/layerSnapshot.js", + "src/data/feedState.js", + "src/contextModePolicy.js" + ], + "external": ["cesium"] } } From 05b13e9a78b091d1426e5a27889ad274d4cb350b Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:51:48 -0500 Subject: [PATCH 10/50] chore: add one-shot branch formatter --- .github/workflows/tmp-format-fix.yml | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/tmp-format-fix.yml diff --git a/.github/workflows/tmp-format-fix.yml b/.github/workflows/tmp-format-fix.yml new file mode 100644 index 000000000..73af025ad --- /dev/null +++ b/.github/workflows/tmp-format-fix.yml @@ -0,0 +1,37 @@ +name: Temporary branch formatter + +on: + push: + branches: + - feat/world-packet-boundary-clean-20260929 + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + env: + PUPPETEER_SKIP_DOWNLOAD: 1 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: feat/world-packet-boundary-clean-20260929 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24.14.0 + cache: npm + - run: npm ci + - run: npm run format + - name: Commit formatting + shell: bash + run: | + if git diff --quiet; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -u + git commit -m "style: apply adopted formatting" + git push origin HEAD:feat/world-packet-boundary-clean-20260929 From 58b7d2d95e7ba3e323a5bd506946f4687df670a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:52:28 +0000 Subject: [PATCH 11/50] style: apply adopted formatting --- src/agent/worldPacket.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/agent/worldPacket.js b/src/agent/worldPacket.js index c492f3d6b..e8c522caa 100644 --- a/src/agent/worldPacket.js +++ b/src/agent/worldPacket.js @@ -24,11 +24,7 @@ const NESTED_CONTEXT_RESULT_FIELDS = Object.freeze([ 'contextRollback', ]); -const NON_SEMANTIC_PACKET_KEYS = new Set([ - 'note', - 'ageLabel', - 'generatedAt', -]); +const NON_SEMANTIC_PACKET_KEYS = new Set(['note', 'ageLabel', 'generatedAt']); function semanticPacketValue(value) { if (Array.isArray(value)) return value.map(semanticPacketValue); From 6a6703e0c6db8bca4ac9ab7aa27c6df931106f5c Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:52:54 -0500 Subject: [PATCH 12/50] chore: remove one-shot branch formatter --- .github/workflows/tmp-format-fix.yml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/tmp-format-fix.yml diff --git a/.github/workflows/tmp-format-fix.yml b/.github/workflows/tmp-format-fix.yml deleted file mode 100644 index 73af025ad..000000000 --- a/.github/workflows/tmp-format-fix.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Temporary branch formatter - -on: - push: - branches: - - feat/world-packet-boundary-clean-20260929 - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - env: - PUPPETEER_SKIP_DOWNLOAD: 1 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - ref: feat/world-packet-boundary-clean-20260929 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: 24.14.0 - cache: npm - - run: npm ci - - run: npm run format - - name: Commit formatting - shell: bash - run: | - if git diff --quiet; then - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -u - git commit -m "style: apply adopted formatting" - git push origin HEAD:feat/world-packet-boundary-clean-20260929 From 3c53d63defb180199a039787412e6aa811d43b2d Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:55:09 -0500 Subject: [PATCH 13/50] fix: declare World Packet in application boundary --- scripts/package-boundaries.json | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 42e98d736..b362d28f4 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -1851,6 +1851,7 @@ "src/voice/commands.js", "src/voice/control.js", "src/voice/gevActions.js", + "src/agent/worldPacket.js", "src/voice/gevRealtime.js", "src/voice/layerSummary.js", "src/voice/realtimeBackend.js", From 8acb526d1fc28dbec508b5303f1865bc44c099f2 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:57:17 -0500 Subject: [PATCH 14/50] fix: include World Packet feed-state dependencies --- scripts/package-boundaries.json | 746 ++++++++++++++++++++++++-------- 1 file changed, 567 insertions(+), 179 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index b362d28f4..260179257 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -12,27 +12,50 @@ "src/data/inputOwnership.js", "src/data/infrastructureOverlayEntry.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "application": { - "exports": ["./application"], - "modules": ["src/app/application.js"], + "exports": [ + "./application" + ], + "modules": [ + "src/app/application.js" + ], "external": [] }, "viewer": { - "exports": ["./application/viewer"], - "modules": ["src/app/viewer.js", "src/app/atmosphereCompat.js"], - "external": ["cesium"] + "exports": [ + "./application/viewer" + ], + "modules": [ + "src/app/viewer.js", + "src/app/atmosphereCompat.js" + ], + "external": [ + "cesium" + ] }, "vite-build": { "runtime": "node", - "exports": ["./build/vite", "./build/html"], - "modules": ["build/vite.js", "build/application-html.js"], - "external": ["vite-plugin-cesium"] + "exports": [ + "./build/vite", + "./build/html" + ], + "modules": [ + "build/vite.js", + "build/application-html.js" + ], + "external": [ + "vite-plugin-cesium" + ] }, "live-providers": { "runtime": "node", - "exports": ["./server/providers/live"], + "exports": [ + "./server/providers/live" + ], "modules": [ "server/providers/live.js", "server/providers/common/http.js", @@ -48,16 +71,24 @@ "src/data/aisWatchdog.js", "src/sources/httpBody.js" ], - "external": ["ws"] + "external": [ + "ws" + ] }, "adsb-lol-source": { - "exports": ["./sources/adsb-lol"], - "modules": ["src/data/adsbLolFallback.js"], + "exports": [ + "./sources/adsb-lol" + ], + "modules": [ + "src/data/adsbLolFallback.js" + ], "external": [] }, "place-providers": { "runtime": "node", - "exports": ["./server/providers/places"], + "exports": [ + "./server/providers/places" + ], "modules": [ "server/providers/places.js", "server/providers/places/google.js", @@ -75,13 +106,19 @@ "external": [] }, "place-payloads": { - "exports": ["./sources/places"], - "modules": ["src/data/placeProviderPayloads.js"], + "exports": [ + "./sources/places" + ], + "modules": [ + "src/data/placeProviderPayloads.js" + ], "external": [] }, "space-providers": { "runtime": "node", - "exports": ["./server/providers/space"], + "exports": [ + "./server/providers/space" + ], "modules": [ "server/providers/space.js", "server/providers/space/celestrak.js", @@ -93,13 +130,19 @@ "external": [] }, "space-requests": { - "exports": ["./sources/space"], - "modules": ["src/data/spaceProviderRequests.js"], + "exports": [ + "./sources/space" + ], + "modules": [ + "src/data/spaceProviderRequests.js" + ], "external": [] }, "terrain-provider": { "runtime": "node", - "exports": ["./server/providers/terrain"], + "exports": [ + "./server/providers/terrain" + ], "modules": [ "server/providers/terrain.js", "src/data/terrainHeightsProxy.js" @@ -107,35 +150,59 @@ "external": [] }, "terrain-source": { - "exports": ["./sources/terrain"], - "modules": ["src/data/terrainHeightsProxy.js"], + "exports": [ + "./sources/terrain" + ], + "modules": [ + "src/data/terrainHeightsProxy.js" + ], "external": [] }, "traffic-provider": { "runtime": "node", - "exports": ["./server/providers/traffic"], - "modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"], + "exports": [ + "./server/providers/traffic" + ], + "modules": [ + "server/providers/traffic.js", + "src/data/tomtomTiles.js" + ], "external": [] }, "traffic-source": { - "exports": ["./sources/traffic"], - "modules": ["src/data/tomtomTiles.js"], + "exports": [ + "./sources/traffic" + ], + "modules": [ + "src/data/tomtomTiles.js" + ], "external": [] }, "firms-csv": { - "exports": ["./sources/firms-csv"], - "modules": ["src/data/firmsCsv.js"], + "exports": [ + "./sources/firms-csv" + ], + "modules": [ + "src/data/firmsCsv.js" + ], "external": [] }, "firms-provider": { "runtime": "node", - "exports": ["./server/providers/firms"], - "modules": ["server/providers/firms.js", "src/data/firmsCsv.js"], + "exports": [ + "./server/providers/firms" + ], + "modules": [ + "server/providers/firms.js", + "src/data/firmsCsv.js" + ], "external": [] }, "gbfs-provider": { "runtime": "node", - "exports": ["./server/providers/gbfs"], + "exports": [ + "./server/providers/gbfs" + ], "modules": [ "server/providers/gbfs.js", "server/providers/common/http.js", @@ -145,13 +212,19 @@ "external": [] }, "gbfs-source": { - "exports": ["./sources/gbfs"], - "modules": ["src/data/gbfsSource.js"], + "exports": [ + "./sources/gbfs" + ], + "modules": [ + "src/data/gbfsSource.js" + ], "external": [] }, "cctv-provider": { "runtime": "node", - "exports": ["./server/providers/cctv"], + "exports": [ + "./server/providers/cctv" + ], "modules": [ "server/providers/cctv.js", "server/providers/cctv/cap.js", @@ -176,7 +249,9 @@ }, "radio-provider": { "runtime": "node", - "exports": ["./server/providers/radio"], + "exports": [ + "./server/providers/radio" + ], "modules": [ "server/providers/radio.js", "server/providers/radio/catalog.js", @@ -192,7 +267,9 @@ }, "overpass-provider": { "runtime": "node", - "exports": ["./server/providers/overpass"], + "exports": [ + "./server/providers/overpass" + ], "modules": [ "server/providers/common/geo.js", "server/providers/common/http.js", @@ -215,7 +292,9 @@ }, "military-installations-provider": { "runtime": "node", - "exports": ["./server/providers/military-installations"], + "exports": [ + "./server/providers/military-installations" + ], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -234,7 +313,9 @@ }, "regional-provider": { "runtime": "node", - "exports": ["./server/providers/regional"], + "exports": [ + "./server/providers/regional" + ], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -261,7 +342,9 @@ }, "openai-provider": { "runtime": "node", - "exports": ["./server/providers/openai"], + "exports": [ + "./server/providers/openai" + ], "modules": [ "src/hudLayoutPolicy.js", "server/providers/common/rate-limit.js", @@ -289,7 +372,9 @@ }, "standalone-key-setup": { "runtime": "node", - "exports": ["./server/standalone/key-setup"], + "exports": [ + "./server/standalone/key-setup" + ], "modules": [ "scripts/pinokio-environment.mjs", "server/providers/common/source-root.js", @@ -300,7 +385,9 @@ "external": [] }, "place-search": { - "exports": ["./search"], + "exports": [ + "./search" + ], "modules": [ "src/search/index.js", "src/search/placeSearch.js", @@ -321,17 +408,27 @@ "external": [] }, "panel-disclosure": { - "exports": ["./ui/panels"], - "modules": ["src/ui/panelDisclosure.js"], + "exports": [ + "./ui/panels" + ], + "modules": [ + "src/ui/panelDisclosure.js" + ], "external": [] }, "surface-keyboard": { - "exports": ["./ui/surfaces"], - "modules": ["src/ui/surfaceKeyboard.js"], + "exports": [ + "./ui/surfaces" + ], + "modules": [ + "src/ui/surfaceKeyboard.js" + ], "external": [] }, "panel-rail-layout": { - "exports": ["./ui/layout"], + "exports": [ + "./ui/layout" + ], "modules": [ "src/ui/panelRails.js", "src/ui/displayPanelScroll.js", @@ -344,7 +441,9 @@ "external": [] }, "visual-input": { - "exports": ["./ui/input"], + "exports": [ + "./ui/input" + ], "modules": [ "src/ui/visualInput.js", "src/ui/applicationShortcuts.js", @@ -353,12 +452,19 @@ "external": [] }, "display-controls": { - "exports": ["./ui/display"], - "modules": ["src/ui/displayControls.js"], + "exports": [ + "./ui/display" + ], + "modules": [ + "src/ui/displayControls.js" + ], "external": [] }, "visual-effects": { - "exports": ["./ui/effects", "./ui/effects/bloom"], + "exports": [ + "./ui/effects", + "./ui/effects/bloom" + ], "modules": [ "src/ui/effects.js", "src/ui/visualEffects.js", @@ -371,10 +477,14 @@ "src/styles/noir.js", "src/styles/snow.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "map-source-controls": { - "exports": ["./ui/maps"], + "exports": [ + "./ui/maps" + ], "modules": [ "src/ui/mapSource.js", "src/ui/mapSourceControls.js", @@ -384,7 +494,10 @@ "external": [] }, "layer-panel": { - "exports": ["./ui/layers", "./ui/layers/feedback"], + "exports": [ + "./ui/layers", + "./ui/layers/feedback" + ], "modules": [ "src/ui/layers.js", "src/ui/layerPanel.js", @@ -404,7 +517,9 @@ "external": [] }, "location-controls": { - "exports": ["./ui/location"], + "exports": [ + "./ui/location" + ], "modules": [ "src/ui/location.js", "src/ui/locationControls.js", @@ -415,7 +530,9 @@ "external": [] }, "radio-controls": { - "exports": ["./ui/radio"], + "exports": [ + "./ui/radio" + ], "modules": [ "src/ui/radio.js", "src/ui/radioControls.js", @@ -426,7 +543,9 @@ "external": [] }, "cctv-controls": { - "exports": ["./ui/cctv"], + "exports": [ + "./ui/cctv" + ], "modules": [ "src/ui/cctv.js", "src/ui/cctvBindings.js", @@ -439,7 +558,10 @@ "external": [] }, "context-controls": { - "exports": ["./ui/context", "./ui/context/policy"], + "exports": [ + "./ui/context", + "./ui/context/policy" + ], "modules": [ "src/ui/context.js", "src/ui/contextActions.js", @@ -482,10 +604,14 @@ "src/cockpitVisionPolicy.js", "src/ui/panelRailGeometry.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "scene-controls": { - "exports": ["./ui/scenes"], + "exports": [ + "./ui/scenes" + ], "modules": [ "src/ui/scenes.js", "src/ui/sceneControls.js", @@ -494,7 +620,9 @@ "external": [] }, "ui-shell": { - "exports": ["./ui/shell"], + "exports": [ + "./ui/shell" + ], "modules": [ "src/hudLayouts.js", "src/hudLayoutPolicy.js", @@ -624,10 +752,14 @@ "src/ui/localSdrPresentation.js", "src/data/cameraArrival.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "ui-styles": { - "exports": ["./ui/styles"], + "exports": [ + "./ui/styles" + ], "modules": [ "src/ui/styles.js", "style.css", @@ -657,7 +789,9 @@ "external": [] }, "director": { - "exports": ["./director"], + "exports": [ + "./director" + ], "modules": [ "src/director/playback.js", "src/director/clock.js", @@ -680,7 +814,9 @@ "external": [] }, "scenes": { - "exports": ["./scenes"], + "exports": [ + "./scenes" + ], "modules": [ "src/scenes/nepalEvidencePack.js", "src/data/bhoteKoshiIncidentPlaces.js", @@ -729,10 +865,14 @@ "src/ui/sceneSharing.js", "src/data/cameraArrival.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "live-observations": { - "exports": ["./sources/live"], + "exports": [ + "./sources/live" + ], "modules": [ "src/sources/live/aircraft.js", "src/sources/live/contract.js", @@ -743,7 +883,9 @@ "external": [] }, "civil-flights": { - "exports": ["./layers/flights"], + "exports": [ + "./layers/flights" + ], "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", @@ -782,10 +924,14 @@ "src/data/inputOwnership.js", "src/data/modelEligibility.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "military-flights": { - "exports": ["./layers/military"], + "exports": [ + "./layers/military" + ], "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", @@ -820,10 +966,14 @@ "src/data/inputOwnership.js", "src/data/modelEligibility.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "aircraft-mechanics": { - "exports": ["./aircraft"], + "exports": [ + "./aircraft" + ], "modules": [ "src/data/aircraftClass.js", "src/data/aircraftIcons.js", @@ -845,10 +995,14 @@ "src/layers/aircraft/index.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "vessel-layer": { - "exports": ["./layers/vessels"], + "exports": [ + "./layers/vessels" + ], "modules": [ "src/layers/vessels/cards.js", "src/layers/vessels/evidence.js", @@ -869,20 +1023,28 @@ "src/layers/vessels/snapshotRenderer.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "earthquakes": { - "exports": ["./layers/earthquakes"], + "exports": [ + "./layers/earthquakes" + ], "modules": [ "src/layers/earthquakes/index.js", "src/layers/earthquakes/model.js", "src/layers/earthquakes/source.js", "src/layers/earthquakes/records.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "recent-imagery": { - "exports": ["./layers/recent-imagery"], + "exports": [ + "./layers/recent-imagery" + ], "modules": [ "src/layers/recentImagery/index.js", "src/layers/recentImagery/model.js", @@ -891,10 +1053,14 @@ "src/layers/recentImagery/thumbnails.js", "src/maps/imageryHost.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "fire-layer": { - "exports": ["./layers/firms"], + "exports": [ + "./layers/firms" + ], "modules": [ "src/data/firmsAdapt.js", "src/data/firmsLabels.js", @@ -914,10 +1080,14 @@ "src/layers/firms/viewport.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "satellites-layer": { - "exports": ["./layers/satellites"], + "exports": [ + "./layers/satellites" + ], "modules": [ "src/data/issPass.js", "src/data/satelliteClass.js", @@ -939,10 +1109,15 @@ "src/layers/satellites/tracking.js", "src/data/inputOwnership.js" ], - "external": ["cesium", "satellite.js"] + "external": [ + "cesium", + "satellite.js" + ] }, "launches-layer": { - "exports": ["./layers/launches"], + "exports": [ + "./layers/launches" + ], "modules": [ "src/cyberSonar.js", "src/layers/launches/camera.js", @@ -966,10 +1141,14 @@ "src/layers/launches/testing.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "installations-layer": { - "exports": ["./layers/installations"], + "exports": [ + "./layers/installations" + ], "modules": [ "src/data/cameraSensitivity.js", "src/data/installationFeedback.js", @@ -1001,10 +1180,16 @@ "src/layers/installations/namedMarkers.js", "src/data/infrastructureOverlayEntry.js" ], - "external": ["cesium", "@mapbox/vector-tile", "pbf"] + "external": [ + "cesium", + "@mapbox/vector-tile", + "pbf" + ] }, "awareness-layer": { - "exports": ["./layers/awareness"], + "exports": [ + "./layers/awareness" + ], "modules": [ "src/cyberSonar.js", "src/cockpitMath.js", @@ -1028,10 +1213,14 @@ "src/layers/awareness/testing.js", "src/ui/panelRailGeometry.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "traffic-layer": { - "exports": ["./layers/traffic"], + "exports": [ + "./layers/traffic" + ], "modules": [ "src/data/cameraSensitivity.js", "src/data/flowMatch.js", @@ -1069,10 +1258,16 @@ "src/sources/phaseTiming.js", "src/layers/traffic/roadModes.js" ], - "external": ["cesium", "pbf", "@mapbox/vector-tile"] + "external": [ + "cesium", + "pbf", + "@mapbox/vector-tile" + ] }, "bikeshare-layer": { - "exports": ["./layers/bikeshare"], + "exports": [ + "./layers/bikeshare" + ], "modules": [ "src/layers/bikeshare/controls.js", "src/layers/bikeshare/index.js", @@ -1091,10 +1286,14 @@ "src/data/inputOwnership.js", "src/data/cameraSensitivity.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "cctv-layer": { - "exports": ["./layers/cctv"], + "exports": [ + "./layers/cctv" + ], "modules": [ "src/data/cctvCards.js", "src/data/cctvGizmo.js", @@ -1132,10 +1331,15 @@ "src/data/inputOwnership.js", "src/layers/cctv/sourcePolicy.js" ], - "external": ["hls.js", "cesium"] + "external": [ + "hls.js", + "cesium" + ] }, "alpr-cameras": { - "exports": ["./layers/alpr"], + "exports": [ + "./layers/alpr" + ], "modules": [ "src/layers/alpr/index.js", "src/layers/alpr/model.js", @@ -1154,10 +1358,16 @@ "src/sources/phaseTiming.js", "src/layers/alpr/floors.js" ], - "external": ["cesium", "@mapbox/vector-tile", "pbf"] + "external": [ + "cesium", + "@mapbox/vector-tile", + "pbf" + ] }, "radio-layer": { - "exports": ["./layers/radio"], + "exports": [ + "./layers/radio" + ], "modules": [ "src/data/iconOrientation.js", "src/data/layerState.js", @@ -1188,10 +1398,14 @@ "src/ui/radioTunerModel.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "submarine-cables": { - "exports": ["./layers/submarine-cables"], + "exports": [ + "./layers/submarine-cables" + ], "modules": [ "src/layers/submarineCables/geometry.js", "src/layers/submarineCables/index.js", @@ -1205,10 +1419,14 @@ "src/layers/submarineCables/surface.js", "src/data/inputOwnership.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "bundled-cables": { - "exports": ["./sources/bundled-cables"], + "exports": [ + "./sources/bundled-cables" + ], "modules": [ "src/layers/submarineCables/bundledSource.js", "src/data/local_data/telegeography_submarine_cables/cable-geo.json", @@ -1217,7 +1435,10 @@ "external": [] }, "geography": { - "exports": ["./geography/natural-earth", "./geography/neighborhoods"], + "exports": [ + "./geography/natural-earth", + "./geography/neighborhoods" + ], "modules": [ "src/data/naturalEarthRegions.js", "src/data/neighborhoodPolygons.js", @@ -1253,7 +1474,9 @@ "src/keySetupCore.mjs", "src/maps/imageryHost.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "voice-connection": { "exports": [ @@ -2042,12 +2265,18 @@ ] }, "application-catalog": { - "exports": ["./application/catalog"], - "modules": ["src/app/catalog.js"], + "exports": [ + "./application/catalog" + ], + "modules": [ + "src/app/catalog.js" + ], "external": [] }, "application-layer-construction": { - "exports": ["./application/layers"], + "exports": [ + "./application/layers" + ], "modules": [ "src/cyberSonar.js", "src/data/bhoteKoshiEvent.js", @@ -2526,7 +2755,9 @@ ] }, "application-operations": { - "exports": ["./application/operations"], + "exports": [ + "./application/operations" + ], "modules": [ "src/annotations/annotationResolver.js", "src/annotations/resolver.js", @@ -2587,22 +2818,33 @@ ] }, "layer-lifecycle": { - "exports": ["./data/lifecycle"], - "modules": ["src/data/lifecycle.js", "src/data/cameraArrival.js"], + "exports": [ + "./data/lifecycle" + ], + "modules": [ + "src/data/lifecycle.js", + "src/data/cameraArrival.js" + ], "external": [] }, "ui-navigation": { - "exports": ["./ui/navigation"], + "exports": [ + "./ui/navigation" + ], "modules": [ "src/navigationPolicy.js", "src/ui/navigationController.js", "src/app/stateChannel.js", "src/data/cameraArrival.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "ui-share-restoration": { - "exports": ["./ui/share-restoration"], + "exports": [ + "./ui/share-restoration" + ], "modules": [ "src/data/installationFeedback.js", "src/data/layerState.js", @@ -2615,7 +2857,9 @@ "external": [] }, "ui-visual-settings": { - "exports": ["./ui/visual-settings"], + "exports": [ + "./ui/visual-settings" + ], "modules": [ "src/cyberSonar.js", "src/hudLayouts.js", @@ -2640,10 +2884,14 @@ "src/ui/visualPresets.js", "src/ui/visualSettings.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "ui-panel-chrome": { - "exports": ["./ui/panel-chrome"], + "exports": [ + "./ui/panel-chrome" + ], "modules": [ "src/ui/displayPanelScroll.js", "src/ui/radioPanelPlacement.js", @@ -2662,7 +2910,9 @@ "external": [] }, "civil-flight-records": { - "exports": ["./layers/flights/records"], + "exports": [ + "./layers/flights/records" + ], "modules": [ "src/layers/flights/records.js", "src/layers/flights/recordPolicy.js", @@ -2673,7 +2923,9 @@ "external": [] }, "civil-flight-ingestion": { - "exports": ["./layers/flights/ingestion"], + "exports": [ + "./layers/flights/ingestion" + ], "modules": [ "src/layers/flights/ingestion.js", "src/layers/flights/recordPolicy.js" @@ -2681,7 +2933,9 @@ "external": [] }, "military-flight-records": { - "exports": ["./layers/military/records"], + "exports": [ + "./layers/military/records" + ], "modules": [ "src/layers/military/records.js", "src/layers/military/recordPolicy.js", @@ -2692,7 +2946,9 @@ "external": [] }, "military-flight-ingestion": { - "exports": ["./layers/military/ingestion"], + "exports": [ + "./layers/military/ingestion" + ], "modules": [ "src/layers/military/ingestion.js", "src/layers/military/recordPolicy.js" @@ -2700,7 +2956,9 @@ "external": [] }, "vessel-records": { - "exports": ["./layers/vessels/records"], + "exports": [ + "./layers/vessels/records" + ], "modules": [ "src/layers/vessels/records.js", "src/layers/vessels/recordPolicy.js" @@ -2708,7 +2966,9 @@ "external": [] }, "vessel-ingestion": { - "exports": ["./layers/vessels/ingestion"], + "exports": [ + "./layers/vessels/ingestion" + ], "modules": [ "src/layers/vessels/ingestion.js", "src/layers/vessels/recordPolicy.js" @@ -2716,37 +2976,68 @@ "external": [] }, "directions-layer": { - "exports": ["./layers/directions"], - "modules": ["src/layers/directions/index.js", "src/data/routeSteps.js"], - "external": ["cesium"] + "exports": [ + "./layers/directions" + ], + "modules": [ + "src/layers/directions/index.js", + "src/data/routeSteps.js" + ], + "external": [ + "cesium" + ] }, "radio-browser-source": { - "exports": ["./sources/radio-browser"], - "modules": ["src/sources/radioBrowser.js", "src/data/radioCountry.js"], + "exports": [ + "./sources/radio-browser" + ], + "modules": [ + "src/sources/radioBrowser.js", + "src/data/radioCountry.js" + ], "external": [] }, "cctv-types-source": { - "exports": ["./sources/cctv-types"], - "modules": ["src/sources/cctvTypes.js"], + "exports": [ + "./sources/cctv-types" + ], + "modules": [ + "src/sources/cctvTypes.js" + ], "external": [] }, "regional-source": { - "exports": ["./sources/regional"], - "modules": ["src/data/regionalModel.js"], + "exports": [ + "./sources/regional" + ], + "modules": [ + "src/data/regionalModel.js" + ], "external": [] }, "voice-action-schemas": { - "exports": ["./voice/action-schemas"], - "modules": ["src/voice/actionSchemas.js", "src/hudLayoutPolicy.js"], + "exports": [ + "./voice/action-schemas" + ], + "modules": [ + "src/voice/actionSchemas.js", + "src/hudLayoutPolicy.js" + ], "external": [] }, "voice-session": { - "exports": ["./voice/session"], - "modules": ["src/voice/session.js"], + "exports": [ + "./voice/session" + ], + "modules": [ + "src/voice/session.js" + ], "external": [] }, "voice-session-controls": { - "exports": ["./voice/session-controls"], + "exports": [ + "./voice/session-controls" + ], "modules": [ "src/voice/sessionCommands.js", "src/voice/session.js", @@ -2755,7 +3046,9 @@ "external": [] }, "alpr-source": { - "exports": ["./layers/alpr/source"], + "exports": [ + "./layers/alpr/source" + ], "modules": [ "src/layers/alpr/policy.js", "src/layers/alpr/records.js", @@ -2766,20 +3059,34 @@ "src/sources/vectorTiles.js", "src/sources/phaseTiming.js" ], - "external": ["@mapbox/vector-tile", "pbf"] + "external": [ + "@mapbox/vector-tile", + "pbf" + ] }, "bikeshare-source": { - "exports": ["./layers/bikeshare/source"], - "modules": ["src/layers/bikeshare/source.js"], + "exports": [ + "./layers/bikeshare/source" + ], + "modules": [ + "src/layers/bikeshare/source.js" + ], "external": [] }, "cctv-source": { - "exports": ["./layers/cctv/source"], - "modules": ["src/layers/cctv/source.js", "src/layers/cctv/sourcePolicy.js"], + "exports": [ + "./layers/cctv/source" + ], + "modules": [ + "src/layers/cctv/source.js", + "src/layers/cctv/sourcePolicy.js" + ], "external": [] }, "earthquakes-source": { - "exports": ["./layers/earthquakes/source"], + "exports": [ + "./layers/earthquakes/source" + ], "modules": [ "src/layers/earthquakes/records.js", "src/layers/earthquakes/source.js" @@ -2787,12 +3094,18 @@ "external": [] }, "firms-source": { - "exports": ["./layers/firms/source"], - "modules": ["src/layers/firms/source.js"], + "exports": [ + "./layers/firms/source" + ], + "modules": [ + "src/layers/firms/source.js" + ], "external": [] }, "installations-source": { - "exports": ["./layers/installations/source"], + "exports": [ + "./layers/installations/source" + ], "modules": [ "src/layers/installations/source.js", "src/data/militaryInstallationData.js", @@ -2809,25 +3122,43 @@ "src/data/bundledJson.js", "src/data/retryableLoad.js" ], - "external": ["@mapbox/vector-tile", "pbf"] + "external": [ + "@mapbox/vector-tile", + "pbf" + ] }, "launches-source": { - "exports": ["./layers/launches/source"], - "modules": ["src/layers/launches/source.js"], + "exports": [ + "./layers/launches/source" + ], + "modules": [ + "src/layers/launches/source.js" + ], "external": [] }, "radio-source": { - "exports": ["./layers/radio/source"], - "modules": ["src/layers/radio/policy.js", "src/layers/radio/source.js"], + "exports": [ + "./layers/radio/source" + ], + "modules": [ + "src/layers/radio/policy.js", + "src/layers/radio/source.js" + ], "external": [] }, "satellites-source": { - "exports": ["./layers/satellites/source"], - "modules": ["src/layers/satellites/source.js"], + "exports": [ + "./layers/satellites/source" + ], + "modules": [ + "src/layers/satellites/source.js" + ], "external": [] }, "traffic-source-factory": { - "exports": ["./layers/traffic/source"], + "exports": [ + "./layers/traffic/source" + ], "modules": [ "src/data/tomtomTiles.js", "src/layers/traffic/flowDecode.js", @@ -2843,10 +3174,16 @@ "src/layers/traffic/roadModes.js", "src/data/flowMatch.js" ], - "external": ["@mapbox/vector-tile", "pbf"] + "external": [ + "@mapbox/vector-tile", + "pbf" + ] }, "reference-sources": { - "exports": ["./sources/reference", "./layers/submarine-cables/source"], + "exports": [ + "./sources/reference", + "./layers/submarine-cables/source" + ], "modules": [ "src/sources/reference.js", "src/layers/earthquakes/source.js", @@ -2859,7 +3196,9 @@ "external": [] }, "nominatim-provider": { - "exports": ["./search/nominatim"], + "exports": [ + "./search/nominatim" + ], "modules": [ "src/search/nominatim.js", "src/nominatimGeocode.js", @@ -2872,17 +3211,27 @@ "external": [] }, "source-http-body": { - "exports": ["./sources/http-body"], - "modules": ["src/sources/httpBody.js"], + "exports": [ + "./sources/http-body" + ], + "modules": [ + "src/sources/httpBody.js" + ], "external": [] }, "overpass-lexer": { - "exports": ["./sources/overpass"], - "modules": ["src/sources/overpass.js"], + "exports": [ + "./sources/overpass" + ], + "modules": [ + "src/sources/overpass.js" + ], "external": [] }, "nominatim-source": { - "exports": ["./sources/nominatim"], + "exports": [ + "./sources/nominatim" + ], "modules": [ "src/sources/nominatim.js", "src/nominatimGeocode.js", @@ -2894,7 +3243,9 @@ }, "transit-provider": { "runtime": "node", - "exports": ["./server/providers/transit"], + "exports": [ + "./server/providers/transit" + ], "modules": [ "server/providers/common/http.js", "server/providers/common/rate-limit.js", @@ -2909,20 +3260,28 @@ "src/sources/transitHistoryStore.js", "src/sources/rateLimit.js" ], - "external": ["pbf"] + "external": [ + "pbf" + ] }, "transit-source": { - "exports": ["./sources/transit"], + "exports": [ + "./sources/transit" + ], "modules": [ "src/data/gtfsRealtime.js", "src/data/transitFeeds.js", "src/data/transitProxy.js", "src/sources/transitHistory.js" ], - "external": ["pbf"] + "external": [ + "pbf" + ] }, "transit-standalone": { - "exports": ["./layers/transit/standalone"], + "exports": [ + "./layers/transit/standalone" + ], "modules": [ "src/cyberSonar.js", "src/app/sourceSlot.js", @@ -2981,10 +3340,16 @@ "src/sources/featureSource.js", "src/sources/capability.js" ], - "external": ["cesium", "egm96-universal", "pbf"] + "external": [ + "cesium", + "egm96-universal", + "pbf" + ] }, "transit-layer": { - "exports": ["./layers/transit"], + "exports": [ + "./layers/transit" + ], "modules": [ "src/data/iconOrientation.js", "src/data/inputOwnership.js", @@ -3018,10 +3383,15 @@ "src/layers/transit/source.js", "src/sources/transitHistory.js" ], - "external": ["cesium", "pbf"] + "external": [ + "cesium", + "pbf" + ] }, "transit-source-factory": { - "exports": ["./layers/transit/source"], + "exports": [ + "./layers/transit/source" + ], "modules": [ "src/layers/transit/source.js", "src/sources/transitHistory.js" @@ -3029,7 +3399,9 @@ "external": [] }, "transit-service": { - "exports": ["./sources/transit-service"], + "exports": [ + "./sources/transit-service" + ], "modules": [ "src/data/transitFeeds.js", "src/data/transitProxy.js", @@ -3040,10 +3412,14 @@ "src/sources/transitHistoryStore.js", "src/sources/rateLimit.js" ], - "external": ["pbf"] + "external": [ + "pbf" + ] }, "map-feature-source": { - "exports": ["./sources/map-features"], + "exports": [ + "./sources/map-features" + ], "modules": [ "src/sources/featureGeometry.js", "src/sources/overpassFeaturesRecords.js", @@ -3054,7 +3430,9 @@ "external": [] }, "perimeters": { - "exports": ["./layers/perimeters"], + "exports": [ + "./layers/perimeters" + ], "modules": [ "src/layers/perimeters/index.js", "src/sources/httpBody.js", @@ -3063,10 +3441,14 @@ "src/layers/perimeters/cards.js", "src/layers/perimeters/inciweb.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] }, "perimeters-source": { - "exports": ["./layers/perimeters/source"], + "exports": [ + "./layers/perimeters/source" + ], "modules": [ "src/layers/perimeters/records.js", "src/sources/httpBody.js", @@ -3075,13 +3457,19 @@ "external": [] }, "agent-world-packet": { - "exports": ["./agent/world-packet"], + "exports": [ + "./agent/world-packet" + ], "modules": [ "src/agent/worldPacket.js", "src/data/layerSnapshot.js", "src/data/feedState.js", - "src/contextModePolicy.js" + "src/contextModePolicy.js", + "src/loadingFeedback.js", + "src/data/installationFeedback.js" ], - "external": ["cesium"] + "external": [ + "cesium" + ] } } From cbd3b13afb022cf1904dd08e809d58167f6e7340 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:58:20 -0500 Subject: [PATCH 15/50] chore: add one-shot branch formatter --- .github/workflows/tmp-format-fix.yml | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/tmp-format-fix.yml diff --git a/.github/workflows/tmp-format-fix.yml b/.github/workflows/tmp-format-fix.yml new file mode 100644 index 000000000..73af025ad --- /dev/null +++ b/.github/workflows/tmp-format-fix.yml @@ -0,0 +1,37 @@ +name: Temporary branch formatter + +on: + push: + branches: + - feat/world-packet-boundary-clean-20260929 + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + env: + PUPPETEER_SKIP_DOWNLOAD: 1 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + ref: feat/world-packet-boundary-clean-20260929 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24.14.0 + cache: npm + - run: npm ci + - run: npm run format + - name: Commit formatting + shell: bash + run: | + if git diff --quiet; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -u + git commit -m "style: apply adopted formatting" + git push origin HEAD:feat/world-packet-boundary-clean-20260929 From 914df05346d50adf13abd3337e0ab1d65daf19b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:59:00 +0000 Subject: [PATCH 16/50] style: apply adopted formatting --- scripts/package-boundaries.json | 742 ++++++++------------------------ 1 file changed, 178 insertions(+), 564 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 260179257..fa4f872a4 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -12,50 +12,27 @@ "src/data/inputOwnership.js", "src/data/infrastructureOverlayEntry.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "application": { - "exports": [ - "./application" - ], - "modules": [ - "src/app/application.js" - ], + "exports": ["./application"], + "modules": ["src/app/application.js"], "external": [] }, "viewer": { - "exports": [ - "./application/viewer" - ], - "modules": [ - "src/app/viewer.js", - "src/app/atmosphereCompat.js" - ], - "external": [ - "cesium" - ] + "exports": ["./application/viewer"], + "modules": ["src/app/viewer.js", "src/app/atmosphereCompat.js"], + "external": ["cesium"] }, "vite-build": { "runtime": "node", - "exports": [ - "./build/vite", - "./build/html" - ], - "modules": [ - "build/vite.js", - "build/application-html.js" - ], - "external": [ - "vite-plugin-cesium" - ] + "exports": ["./build/vite", "./build/html"], + "modules": ["build/vite.js", "build/application-html.js"], + "external": ["vite-plugin-cesium"] }, "live-providers": { "runtime": "node", - "exports": [ - "./server/providers/live" - ], + "exports": ["./server/providers/live"], "modules": [ "server/providers/live.js", "server/providers/common/http.js", @@ -71,24 +48,16 @@ "src/data/aisWatchdog.js", "src/sources/httpBody.js" ], - "external": [ - "ws" - ] + "external": ["ws"] }, "adsb-lol-source": { - "exports": [ - "./sources/adsb-lol" - ], - "modules": [ - "src/data/adsbLolFallback.js" - ], + "exports": ["./sources/adsb-lol"], + "modules": ["src/data/adsbLolFallback.js"], "external": [] }, "place-providers": { "runtime": "node", - "exports": [ - "./server/providers/places" - ], + "exports": ["./server/providers/places"], "modules": [ "server/providers/places.js", "server/providers/places/google.js", @@ -106,19 +75,13 @@ "external": [] }, "place-payloads": { - "exports": [ - "./sources/places" - ], - "modules": [ - "src/data/placeProviderPayloads.js" - ], + "exports": ["./sources/places"], + "modules": ["src/data/placeProviderPayloads.js"], "external": [] }, "space-providers": { "runtime": "node", - "exports": [ - "./server/providers/space" - ], + "exports": ["./server/providers/space"], "modules": [ "server/providers/space.js", "server/providers/space/celestrak.js", @@ -130,19 +93,13 @@ "external": [] }, "space-requests": { - "exports": [ - "./sources/space" - ], - "modules": [ - "src/data/spaceProviderRequests.js" - ], + "exports": ["./sources/space"], + "modules": ["src/data/spaceProviderRequests.js"], "external": [] }, "terrain-provider": { "runtime": "node", - "exports": [ - "./server/providers/terrain" - ], + "exports": ["./server/providers/terrain"], "modules": [ "server/providers/terrain.js", "src/data/terrainHeightsProxy.js" @@ -150,59 +107,35 @@ "external": [] }, "terrain-source": { - "exports": [ - "./sources/terrain" - ], - "modules": [ - "src/data/terrainHeightsProxy.js" - ], + "exports": ["./sources/terrain"], + "modules": ["src/data/terrainHeightsProxy.js"], "external": [] }, "traffic-provider": { "runtime": "node", - "exports": [ - "./server/providers/traffic" - ], - "modules": [ - "server/providers/traffic.js", - "src/data/tomtomTiles.js" - ], + "exports": ["./server/providers/traffic"], + "modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"], "external": [] }, "traffic-source": { - "exports": [ - "./sources/traffic" - ], - "modules": [ - "src/data/tomtomTiles.js" - ], + "exports": ["./sources/traffic"], + "modules": ["src/data/tomtomTiles.js"], "external": [] }, "firms-csv": { - "exports": [ - "./sources/firms-csv" - ], - "modules": [ - "src/data/firmsCsv.js" - ], + "exports": ["./sources/firms-csv"], + "modules": ["src/data/firmsCsv.js"], "external": [] }, "firms-provider": { "runtime": "node", - "exports": [ - "./server/providers/firms" - ], - "modules": [ - "server/providers/firms.js", - "src/data/firmsCsv.js" - ], + "exports": ["./server/providers/firms"], + "modules": ["server/providers/firms.js", "src/data/firmsCsv.js"], "external": [] }, "gbfs-provider": { "runtime": "node", - "exports": [ - "./server/providers/gbfs" - ], + "exports": ["./server/providers/gbfs"], "modules": [ "server/providers/gbfs.js", "server/providers/common/http.js", @@ -212,19 +145,13 @@ "external": [] }, "gbfs-source": { - "exports": [ - "./sources/gbfs" - ], - "modules": [ - "src/data/gbfsSource.js" - ], + "exports": ["./sources/gbfs"], + "modules": ["src/data/gbfsSource.js"], "external": [] }, "cctv-provider": { "runtime": "node", - "exports": [ - "./server/providers/cctv" - ], + "exports": ["./server/providers/cctv"], "modules": [ "server/providers/cctv.js", "server/providers/cctv/cap.js", @@ -249,9 +176,7 @@ }, "radio-provider": { "runtime": "node", - "exports": [ - "./server/providers/radio" - ], + "exports": ["./server/providers/radio"], "modules": [ "server/providers/radio.js", "server/providers/radio/catalog.js", @@ -267,9 +192,7 @@ }, "overpass-provider": { "runtime": "node", - "exports": [ - "./server/providers/overpass" - ], + "exports": ["./server/providers/overpass"], "modules": [ "server/providers/common/geo.js", "server/providers/common/http.js", @@ -292,9 +215,7 @@ }, "military-installations-provider": { "runtime": "node", - "exports": [ - "./server/providers/military-installations" - ], + "exports": ["./server/providers/military-installations"], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -313,9 +234,7 @@ }, "regional-provider": { "runtime": "node", - "exports": [ - "./server/providers/regional" - ], + "exports": ["./server/providers/regional"], "modules": [ "server/providers/common/http.js", "server/providers/common/query.js", @@ -342,9 +261,7 @@ }, "openai-provider": { "runtime": "node", - "exports": [ - "./server/providers/openai" - ], + "exports": ["./server/providers/openai"], "modules": [ "src/hudLayoutPolicy.js", "server/providers/common/rate-limit.js", @@ -372,9 +289,7 @@ }, "standalone-key-setup": { "runtime": "node", - "exports": [ - "./server/standalone/key-setup" - ], + "exports": ["./server/standalone/key-setup"], "modules": [ "scripts/pinokio-environment.mjs", "server/providers/common/source-root.js", @@ -385,9 +300,7 @@ "external": [] }, "place-search": { - "exports": [ - "./search" - ], + "exports": ["./search"], "modules": [ "src/search/index.js", "src/search/placeSearch.js", @@ -408,27 +321,17 @@ "external": [] }, "panel-disclosure": { - "exports": [ - "./ui/panels" - ], - "modules": [ - "src/ui/panelDisclosure.js" - ], + "exports": ["./ui/panels"], + "modules": ["src/ui/panelDisclosure.js"], "external": [] }, "surface-keyboard": { - "exports": [ - "./ui/surfaces" - ], - "modules": [ - "src/ui/surfaceKeyboard.js" - ], + "exports": ["./ui/surfaces"], + "modules": ["src/ui/surfaceKeyboard.js"], "external": [] }, "panel-rail-layout": { - "exports": [ - "./ui/layout" - ], + "exports": ["./ui/layout"], "modules": [ "src/ui/panelRails.js", "src/ui/displayPanelScroll.js", @@ -441,9 +344,7 @@ "external": [] }, "visual-input": { - "exports": [ - "./ui/input" - ], + "exports": ["./ui/input"], "modules": [ "src/ui/visualInput.js", "src/ui/applicationShortcuts.js", @@ -452,19 +353,12 @@ "external": [] }, "display-controls": { - "exports": [ - "./ui/display" - ], - "modules": [ - "src/ui/displayControls.js" - ], + "exports": ["./ui/display"], + "modules": ["src/ui/displayControls.js"], "external": [] }, "visual-effects": { - "exports": [ - "./ui/effects", - "./ui/effects/bloom" - ], + "exports": ["./ui/effects", "./ui/effects/bloom"], "modules": [ "src/ui/effects.js", "src/ui/visualEffects.js", @@ -477,14 +371,10 @@ "src/styles/noir.js", "src/styles/snow.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "map-source-controls": { - "exports": [ - "./ui/maps" - ], + "exports": ["./ui/maps"], "modules": [ "src/ui/mapSource.js", "src/ui/mapSourceControls.js", @@ -494,10 +384,7 @@ "external": [] }, "layer-panel": { - "exports": [ - "./ui/layers", - "./ui/layers/feedback" - ], + "exports": ["./ui/layers", "./ui/layers/feedback"], "modules": [ "src/ui/layers.js", "src/ui/layerPanel.js", @@ -517,9 +404,7 @@ "external": [] }, "location-controls": { - "exports": [ - "./ui/location" - ], + "exports": ["./ui/location"], "modules": [ "src/ui/location.js", "src/ui/locationControls.js", @@ -530,9 +415,7 @@ "external": [] }, "radio-controls": { - "exports": [ - "./ui/radio" - ], + "exports": ["./ui/radio"], "modules": [ "src/ui/radio.js", "src/ui/radioControls.js", @@ -543,9 +426,7 @@ "external": [] }, "cctv-controls": { - "exports": [ - "./ui/cctv" - ], + "exports": ["./ui/cctv"], "modules": [ "src/ui/cctv.js", "src/ui/cctvBindings.js", @@ -558,10 +439,7 @@ "external": [] }, "context-controls": { - "exports": [ - "./ui/context", - "./ui/context/policy" - ], + "exports": ["./ui/context", "./ui/context/policy"], "modules": [ "src/ui/context.js", "src/ui/contextActions.js", @@ -604,14 +482,10 @@ "src/cockpitVisionPolicy.js", "src/ui/panelRailGeometry.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "scene-controls": { - "exports": [ - "./ui/scenes" - ], + "exports": ["./ui/scenes"], "modules": [ "src/ui/scenes.js", "src/ui/sceneControls.js", @@ -620,9 +494,7 @@ "external": [] }, "ui-shell": { - "exports": [ - "./ui/shell" - ], + "exports": ["./ui/shell"], "modules": [ "src/hudLayouts.js", "src/hudLayoutPolicy.js", @@ -752,14 +624,10 @@ "src/ui/localSdrPresentation.js", "src/data/cameraArrival.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "ui-styles": { - "exports": [ - "./ui/styles" - ], + "exports": ["./ui/styles"], "modules": [ "src/ui/styles.js", "style.css", @@ -789,9 +657,7 @@ "external": [] }, "director": { - "exports": [ - "./director" - ], + "exports": ["./director"], "modules": [ "src/director/playback.js", "src/director/clock.js", @@ -814,9 +680,7 @@ "external": [] }, "scenes": { - "exports": [ - "./scenes" - ], + "exports": ["./scenes"], "modules": [ "src/scenes/nepalEvidencePack.js", "src/data/bhoteKoshiIncidentPlaces.js", @@ -865,14 +729,10 @@ "src/ui/sceneSharing.js", "src/data/cameraArrival.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "live-observations": { - "exports": [ - "./sources/live" - ], + "exports": ["./sources/live"], "modules": [ "src/sources/live/aircraft.js", "src/sources/live/contract.js", @@ -883,9 +743,7 @@ "external": [] }, "civil-flights": { - "exports": [ - "./layers/flights" - ], + "exports": ["./layers/flights"], "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", @@ -924,14 +782,10 @@ "src/data/inputOwnership.js", "src/data/modelEligibility.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "military-flights": { - "exports": [ - "./layers/military" - ], + "exports": ["./layers/military"], "modules": [ "src/cyberSonar.js", "src/data/aircraftClass.js", @@ -966,14 +820,10 @@ "src/data/inputOwnership.js", "src/data/modelEligibility.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "aircraft-mechanics": { - "exports": [ - "./aircraft" - ], + "exports": ["./aircraft"], "modules": [ "src/data/aircraftClass.js", "src/data/aircraftIcons.js", @@ -995,14 +845,10 @@ "src/layers/aircraft/index.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "vessel-layer": { - "exports": [ - "./layers/vessels" - ], + "exports": ["./layers/vessels"], "modules": [ "src/layers/vessels/cards.js", "src/layers/vessels/evidence.js", @@ -1023,28 +869,20 @@ "src/layers/vessels/snapshotRenderer.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "earthquakes": { - "exports": [ - "./layers/earthquakes" - ], + "exports": ["./layers/earthquakes"], "modules": [ "src/layers/earthquakes/index.js", "src/layers/earthquakes/model.js", "src/layers/earthquakes/source.js", "src/layers/earthquakes/records.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "recent-imagery": { - "exports": [ - "./layers/recent-imagery" - ], + "exports": ["./layers/recent-imagery"], "modules": [ "src/layers/recentImagery/index.js", "src/layers/recentImagery/model.js", @@ -1053,14 +891,10 @@ "src/layers/recentImagery/thumbnails.js", "src/maps/imageryHost.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "fire-layer": { - "exports": [ - "./layers/firms" - ], + "exports": ["./layers/firms"], "modules": [ "src/data/firmsAdapt.js", "src/data/firmsLabels.js", @@ -1080,14 +914,10 @@ "src/layers/firms/viewport.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "satellites-layer": { - "exports": [ - "./layers/satellites" - ], + "exports": ["./layers/satellites"], "modules": [ "src/data/issPass.js", "src/data/satelliteClass.js", @@ -1109,15 +939,10 @@ "src/layers/satellites/tracking.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium", - "satellite.js" - ] + "external": ["cesium", "satellite.js"] }, "launches-layer": { - "exports": [ - "./layers/launches" - ], + "exports": ["./layers/launches"], "modules": [ "src/cyberSonar.js", "src/layers/launches/camera.js", @@ -1141,14 +966,10 @@ "src/layers/launches/testing.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "installations-layer": { - "exports": [ - "./layers/installations" - ], + "exports": ["./layers/installations"], "modules": [ "src/data/cameraSensitivity.js", "src/data/installationFeedback.js", @@ -1180,16 +1001,10 @@ "src/layers/installations/namedMarkers.js", "src/data/infrastructureOverlayEntry.js" ], - "external": [ - "cesium", - "@mapbox/vector-tile", - "pbf" - ] + "external": ["cesium", "@mapbox/vector-tile", "pbf"] }, "awareness-layer": { - "exports": [ - "./layers/awareness" - ], + "exports": ["./layers/awareness"], "modules": [ "src/cyberSonar.js", "src/cockpitMath.js", @@ -1213,14 +1028,10 @@ "src/layers/awareness/testing.js", "src/ui/panelRailGeometry.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "traffic-layer": { - "exports": [ - "./layers/traffic" - ], + "exports": ["./layers/traffic"], "modules": [ "src/data/cameraSensitivity.js", "src/data/flowMatch.js", @@ -1258,16 +1069,10 @@ "src/sources/phaseTiming.js", "src/layers/traffic/roadModes.js" ], - "external": [ - "cesium", - "pbf", - "@mapbox/vector-tile" - ] + "external": ["cesium", "pbf", "@mapbox/vector-tile"] }, "bikeshare-layer": { - "exports": [ - "./layers/bikeshare" - ], + "exports": ["./layers/bikeshare"], "modules": [ "src/layers/bikeshare/controls.js", "src/layers/bikeshare/index.js", @@ -1286,14 +1091,10 @@ "src/data/inputOwnership.js", "src/data/cameraSensitivity.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "cctv-layer": { - "exports": [ - "./layers/cctv" - ], + "exports": ["./layers/cctv"], "modules": [ "src/data/cctvCards.js", "src/data/cctvGizmo.js", @@ -1331,15 +1132,10 @@ "src/data/inputOwnership.js", "src/layers/cctv/sourcePolicy.js" ], - "external": [ - "hls.js", - "cesium" - ] + "external": ["hls.js", "cesium"] }, "alpr-cameras": { - "exports": [ - "./layers/alpr" - ], + "exports": ["./layers/alpr"], "modules": [ "src/layers/alpr/index.js", "src/layers/alpr/model.js", @@ -1358,16 +1154,10 @@ "src/sources/phaseTiming.js", "src/layers/alpr/floors.js" ], - "external": [ - "cesium", - "@mapbox/vector-tile", - "pbf" - ] + "external": ["cesium", "@mapbox/vector-tile", "pbf"] }, "radio-layer": { - "exports": [ - "./layers/radio" - ], + "exports": ["./layers/radio"], "modules": [ "src/data/iconOrientation.js", "src/data/layerState.js", @@ -1398,14 +1188,10 @@ "src/ui/radioTunerModel.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "submarine-cables": { - "exports": [ - "./layers/submarine-cables" - ], + "exports": ["./layers/submarine-cables"], "modules": [ "src/layers/submarineCables/geometry.js", "src/layers/submarineCables/index.js", @@ -1419,14 +1205,10 @@ "src/layers/submarineCables/surface.js", "src/data/inputOwnership.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "bundled-cables": { - "exports": [ - "./sources/bundled-cables" - ], + "exports": ["./sources/bundled-cables"], "modules": [ "src/layers/submarineCables/bundledSource.js", "src/data/local_data/telegeography_submarine_cables/cable-geo.json", @@ -1435,10 +1217,7 @@ "external": [] }, "geography": { - "exports": [ - "./geography/natural-earth", - "./geography/neighborhoods" - ], + "exports": ["./geography/natural-earth", "./geography/neighborhoods"], "modules": [ "src/data/naturalEarthRegions.js", "src/data/neighborhoodPolygons.js", @@ -1474,9 +1253,7 @@ "src/keySetupCore.mjs", "src/maps/imageryHost.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "voice-connection": { "exports": [ @@ -2265,18 +2042,12 @@ ] }, "application-catalog": { - "exports": [ - "./application/catalog" - ], - "modules": [ - "src/app/catalog.js" - ], + "exports": ["./application/catalog"], + "modules": ["src/app/catalog.js"], "external": [] }, "application-layer-construction": { - "exports": [ - "./application/layers" - ], + "exports": ["./application/layers"], "modules": [ "src/cyberSonar.js", "src/data/bhoteKoshiEvent.js", @@ -2755,9 +2526,7 @@ ] }, "application-operations": { - "exports": [ - "./application/operations" - ], + "exports": ["./application/operations"], "modules": [ "src/annotations/annotationResolver.js", "src/annotations/resolver.js", @@ -2818,33 +2587,22 @@ ] }, "layer-lifecycle": { - "exports": [ - "./data/lifecycle" - ], - "modules": [ - "src/data/lifecycle.js", - "src/data/cameraArrival.js" - ], + "exports": ["./data/lifecycle"], + "modules": ["src/data/lifecycle.js", "src/data/cameraArrival.js"], "external": [] }, "ui-navigation": { - "exports": [ - "./ui/navigation" - ], + "exports": ["./ui/navigation"], "modules": [ "src/navigationPolicy.js", "src/ui/navigationController.js", "src/app/stateChannel.js", "src/data/cameraArrival.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "ui-share-restoration": { - "exports": [ - "./ui/share-restoration" - ], + "exports": ["./ui/share-restoration"], "modules": [ "src/data/installationFeedback.js", "src/data/layerState.js", @@ -2857,9 +2615,7 @@ "external": [] }, "ui-visual-settings": { - "exports": [ - "./ui/visual-settings" - ], + "exports": ["./ui/visual-settings"], "modules": [ "src/cyberSonar.js", "src/hudLayouts.js", @@ -2884,14 +2640,10 @@ "src/ui/visualPresets.js", "src/ui/visualSettings.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "ui-panel-chrome": { - "exports": [ - "./ui/panel-chrome" - ], + "exports": ["./ui/panel-chrome"], "modules": [ "src/ui/displayPanelScroll.js", "src/ui/radioPanelPlacement.js", @@ -2910,9 +2662,7 @@ "external": [] }, "civil-flight-records": { - "exports": [ - "./layers/flights/records" - ], + "exports": ["./layers/flights/records"], "modules": [ "src/layers/flights/records.js", "src/layers/flights/recordPolicy.js", @@ -2923,9 +2673,7 @@ "external": [] }, "civil-flight-ingestion": { - "exports": [ - "./layers/flights/ingestion" - ], + "exports": ["./layers/flights/ingestion"], "modules": [ "src/layers/flights/ingestion.js", "src/layers/flights/recordPolicy.js" @@ -2933,9 +2681,7 @@ "external": [] }, "military-flight-records": { - "exports": [ - "./layers/military/records" - ], + "exports": ["./layers/military/records"], "modules": [ "src/layers/military/records.js", "src/layers/military/recordPolicy.js", @@ -2946,9 +2692,7 @@ "external": [] }, "military-flight-ingestion": { - "exports": [ - "./layers/military/ingestion" - ], + "exports": ["./layers/military/ingestion"], "modules": [ "src/layers/military/ingestion.js", "src/layers/military/recordPolicy.js" @@ -2956,9 +2700,7 @@ "external": [] }, "vessel-records": { - "exports": [ - "./layers/vessels/records" - ], + "exports": ["./layers/vessels/records"], "modules": [ "src/layers/vessels/records.js", "src/layers/vessels/recordPolicy.js" @@ -2966,9 +2708,7 @@ "external": [] }, "vessel-ingestion": { - "exports": [ - "./layers/vessels/ingestion" - ], + "exports": ["./layers/vessels/ingestion"], "modules": [ "src/layers/vessels/ingestion.js", "src/layers/vessels/recordPolicy.js" @@ -2976,68 +2716,37 @@ "external": [] }, "directions-layer": { - "exports": [ - "./layers/directions" - ], - "modules": [ - "src/layers/directions/index.js", - "src/data/routeSteps.js" - ], - "external": [ - "cesium" - ] + "exports": ["./layers/directions"], + "modules": ["src/layers/directions/index.js", "src/data/routeSteps.js"], + "external": ["cesium"] }, "radio-browser-source": { - "exports": [ - "./sources/radio-browser" - ], - "modules": [ - "src/sources/radioBrowser.js", - "src/data/radioCountry.js" - ], + "exports": ["./sources/radio-browser"], + "modules": ["src/sources/radioBrowser.js", "src/data/radioCountry.js"], "external": [] }, "cctv-types-source": { - "exports": [ - "./sources/cctv-types" - ], - "modules": [ - "src/sources/cctvTypes.js" - ], + "exports": ["./sources/cctv-types"], + "modules": ["src/sources/cctvTypes.js"], "external": [] }, "regional-source": { - "exports": [ - "./sources/regional" - ], - "modules": [ - "src/data/regionalModel.js" - ], + "exports": ["./sources/regional"], + "modules": ["src/data/regionalModel.js"], "external": [] }, "voice-action-schemas": { - "exports": [ - "./voice/action-schemas" - ], - "modules": [ - "src/voice/actionSchemas.js", - "src/hudLayoutPolicy.js" - ], + "exports": ["./voice/action-schemas"], + "modules": ["src/voice/actionSchemas.js", "src/hudLayoutPolicy.js"], "external": [] }, "voice-session": { - "exports": [ - "./voice/session" - ], - "modules": [ - "src/voice/session.js" - ], + "exports": ["./voice/session"], + "modules": ["src/voice/session.js"], "external": [] }, "voice-session-controls": { - "exports": [ - "./voice/session-controls" - ], + "exports": ["./voice/session-controls"], "modules": [ "src/voice/sessionCommands.js", "src/voice/session.js", @@ -3046,9 +2755,7 @@ "external": [] }, "alpr-source": { - "exports": [ - "./layers/alpr/source" - ], + "exports": ["./layers/alpr/source"], "modules": [ "src/layers/alpr/policy.js", "src/layers/alpr/records.js", @@ -3059,34 +2766,20 @@ "src/sources/vectorTiles.js", "src/sources/phaseTiming.js" ], - "external": [ - "@mapbox/vector-tile", - "pbf" - ] + "external": ["@mapbox/vector-tile", "pbf"] }, "bikeshare-source": { - "exports": [ - "./layers/bikeshare/source" - ], - "modules": [ - "src/layers/bikeshare/source.js" - ], + "exports": ["./layers/bikeshare/source"], + "modules": ["src/layers/bikeshare/source.js"], "external": [] }, "cctv-source": { - "exports": [ - "./layers/cctv/source" - ], - "modules": [ - "src/layers/cctv/source.js", - "src/layers/cctv/sourcePolicy.js" - ], + "exports": ["./layers/cctv/source"], + "modules": ["src/layers/cctv/source.js", "src/layers/cctv/sourcePolicy.js"], "external": [] }, "earthquakes-source": { - "exports": [ - "./layers/earthquakes/source" - ], + "exports": ["./layers/earthquakes/source"], "modules": [ "src/layers/earthquakes/records.js", "src/layers/earthquakes/source.js" @@ -3094,18 +2787,12 @@ "external": [] }, "firms-source": { - "exports": [ - "./layers/firms/source" - ], - "modules": [ - "src/layers/firms/source.js" - ], + "exports": ["./layers/firms/source"], + "modules": ["src/layers/firms/source.js"], "external": [] }, "installations-source": { - "exports": [ - "./layers/installations/source" - ], + "exports": ["./layers/installations/source"], "modules": [ "src/layers/installations/source.js", "src/data/militaryInstallationData.js", @@ -3122,43 +2809,25 @@ "src/data/bundledJson.js", "src/data/retryableLoad.js" ], - "external": [ - "@mapbox/vector-tile", - "pbf" - ] + "external": ["@mapbox/vector-tile", "pbf"] }, "launches-source": { - "exports": [ - "./layers/launches/source" - ], - "modules": [ - "src/layers/launches/source.js" - ], + "exports": ["./layers/launches/source"], + "modules": ["src/layers/launches/source.js"], "external": [] }, "radio-source": { - "exports": [ - "./layers/radio/source" - ], - "modules": [ - "src/layers/radio/policy.js", - "src/layers/radio/source.js" - ], + "exports": ["./layers/radio/source"], + "modules": ["src/layers/radio/policy.js", "src/layers/radio/source.js"], "external": [] }, "satellites-source": { - "exports": [ - "./layers/satellites/source" - ], - "modules": [ - "src/layers/satellites/source.js" - ], + "exports": ["./layers/satellites/source"], + "modules": ["src/layers/satellites/source.js"], "external": [] }, "traffic-source-factory": { - "exports": [ - "./layers/traffic/source" - ], + "exports": ["./layers/traffic/source"], "modules": [ "src/data/tomtomTiles.js", "src/layers/traffic/flowDecode.js", @@ -3174,16 +2843,10 @@ "src/layers/traffic/roadModes.js", "src/data/flowMatch.js" ], - "external": [ - "@mapbox/vector-tile", - "pbf" - ] + "external": ["@mapbox/vector-tile", "pbf"] }, "reference-sources": { - "exports": [ - "./sources/reference", - "./layers/submarine-cables/source" - ], + "exports": ["./sources/reference", "./layers/submarine-cables/source"], "modules": [ "src/sources/reference.js", "src/layers/earthquakes/source.js", @@ -3196,9 +2859,7 @@ "external": [] }, "nominatim-provider": { - "exports": [ - "./search/nominatim" - ], + "exports": ["./search/nominatim"], "modules": [ "src/search/nominatim.js", "src/nominatimGeocode.js", @@ -3211,27 +2872,17 @@ "external": [] }, "source-http-body": { - "exports": [ - "./sources/http-body" - ], - "modules": [ - "src/sources/httpBody.js" - ], + "exports": ["./sources/http-body"], + "modules": ["src/sources/httpBody.js"], "external": [] }, "overpass-lexer": { - "exports": [ - "./sources/overpass" - ], - "modules": [ - "src/sources/overpass.js" - ], + "exports": ["./sources/overpass"], + "modules": ["src/sources/overpass.js"], "external": [] }, "nominatim-source": { - "exports": [ - "./sources/nominatim" - ], + "exports": ["./sources/nominatim"], "modules": [ "src/sources/nominatim.js", "src/nominatimGeocode.js", @@ -3243,9 +2894,7 @@ }, "transit-provider": { "runtime": "node", - "exports": [ - "./server/providers/transit" - ], + "exports": ["./server/providers/transit"], "modules": [ "server/providers/common/http.js", "server/providers/common/rate-limit.js", @@ -3260,28 +2909,20 @@ "src/sources/transitHistoryStore.js", "src/sources/rateLimit.js" ], - "external": [ - "pbf" - ] + "external": ["pbf"] }, "transit-source": { - "exports": [ - "./sources/transit" - ], + "exports": ["./sources/transit"], "modules": [ "src/data/gtfsRealtime.js", "src/data/transitFeeds.js", "src/data/transitProxy.js", "src/sources/transitHistory.js" ], - "external": [ - "pbf" - ] + "external": ["pbf"] }, "transit-standalone": { - "exports": [ - "./layers/transit/standalone" - ], + "exports": ["./layers/transit/standalone"], "modules": [ "src/cyberSonar.js", "src/app/sourceSlot.js", @@ -3340,16 +2981,10 @@ "src/sources/featureSource.js", "src/sources/capability.js" ], - "external": [ - "cesium", - "egm96-universal", - "pbf" - ] + "external": ["cesium", "egm96-universal", "pbf"] }, "transit-layer": { - "exports": [ - "./layers/transit" - ], + "exports": ["./layers/transit"], "modules": [ "src/data/iconOrientation.js", "src/data/inputOwnership.js", @@ -3383,15 +3018,10 @@ "src/layers/transit/source.js", "src/sources/transitHistory.js" ], - "external": [ - "cesium", - "pbf" - ] + "external": ["cesium", "pbf"] }, "transit-source-factory": { - "exports": [ - "./layers/transit/source" - ], + "exports": ["./layers/transit/source"], "modules": [ "src/layers/transit/source.js", "src/sources/transitHistory.js" @@ -3399,9 +3029,7 @@ "external": [] }, "transit-service": { - "exports": [ - "./sources/transit-service" - ], + "exports": ["./sources/transit-service"], "modules": [ "src/data/transitFeeds.js", "src/data/transitProxy.js", @@ -3412,14 +3040,10 @@ "src/sources/transitHistoryStore.js", "src/sources/rateLimit.js" ], - "external": [ - "pbf" - ] + "external": ["pbf"] }, "map-feature-source": { - "exports": [ - "./sources/map-features" - ], + "exports": ["./sources/map-features"], "modules": [ "src/sources/featureGeometry.js", "src/sources/overpassFeaturesRecords.js", @@ -3430,9 +3054,7 @@ "external": [] }, "perimeters": { - "exports": [ - "./layers/perimeters" - ], + "exports": ["./layers/perimeters"], "modules": [ "src/layers/perimeters/index.js", "src/sources/httpBody.js", @@ -3441,14 +3063,10 @@ "src/layers/perimeters/cards.js", "src/layers/perimeters/inciweb.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] }, "perimeters-source": { - "exports": [ - "./layers/perimeters/source" - ], + "exports": ["./layers/perimeters/source"], "modules": [ "src/layers/perimeters/records.js", "src/sources/httpBody.js", @@ -3457,9 +3075,7 @@ "external": [] }, "agent-world-packet": { - "exports": [ - "./agent/world-packet" - ], + "exports": ["./agent/world-packet"], "modules": [ "src/agent/worldPacket.js", "src/data/layerSnapshot.js", @@ -3468,8 +3084,6 @@ "src/loadingFeedback.js", "src/data/installationFeedback.js" ], - "external": [ - "cesium" - ] + "external": ["cesium"] } } From abea3f2feb99c14a64e5c1884292411b044a0bb2 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:59:48 -0500 Subject: [PATCH 17/50] chore: remove one-shot branch formatter --- .github/workflows/tmp-format-fix.yml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/tmp-format-fix.yml diff --git a/.github/workflows/tmp-format-fix.yml b/.github/workflows/tmp-format-fix.yml deleted file mode 100644 index 73af025ad..000000000 --- a/.github/workflows/tmp-format-fix.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Temporary branch formatter - -on: - push: - branches: - - feat/world-packet-boundary-clean-20260929 - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - env: - PUPPETEER_SKIP_DOWNLOAD: 1 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - ref: feat/world-packet-boundary-clean-20260929 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: 24.14.0 - cache: npm - - run: npm ci - - run: npm run format - - name: Commit formatting - shell: bash - run: | - if git diff --quiet; then - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -u - git commit -m "style: apply adopted formatting" - git push origin HEAD:feat/world-packet-boundary-clean-20260929 From a194da21dd6f1fdb4d41170e560e759f5244cf16 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:52:08 -0500 Subject: [PATCH 18/50] chore: add one-shot downstream format fixer --- .github/workflows/downstream-format-fix.yml | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/downstream-format-fix.yml diff --git a/.github/workflows/downstream-format-fix.yml b/.github/workflows/downstream-format-fix.yml new file mode 100644 index 000000000..40b244d2a --- /dev/null +++ b/.github/workflows/downstream-format-fix.yml @@ -0,0 +1,37 @@ +name: Downstream format fix + +on: + push: + branches: + - feat/world-packet-boundary-clean-20260929 + +permissions: + contents: write + +jobs: + format: + if: ${{ github.actor != 'github-actions[bot]' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: feat/world-packet-boundary-clean-20260929 + - uses: actions/setup-node@v5 + with: + node-version: 24.14.0 + cache: npm + - run: npm ci + env: + PUPPETEER_SKIP_DOWNLOAD: 1 + - run: npm run format + - name: Commit formatting if needed + shell: bash + run: | + if git diff --quiet; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore: apply adopted formatting" + git push From 2b7f832f456c933811c8dc0faa75caf111816718 Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:52:50 -0500 Subject: [PATCH 19/50] chore: remove one-shot downstream format fixer --- .github/workflows/downstream-format-fix.yml | 37 --------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/downstream-format-fix.yml diff --git a/.github/workflows/downstream-format-fix.yml b/.github/workflows/downstream-format-fix.yml deleted file mode 100644 index 40b244d2a..000000000 --- a/.github/workflows/downstream-format-fix.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Downstream format fix - -on: - push: - branches: - - feat/world-packet-boundary-clean-20260929 - -permissions: - contents: write - -jobs: - format: - if: ${{ github.actor != 'github-actions[bot]' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - ref: feat/world-packet-boundary-clean-20260929 - - uses: actions/setup-node@v5 - with: - node-version: 24.14.0 - cache: npm - - run: npm ci - env: - PUPPETEER_SKIP_DOWNLOAD: 1 - - run: npm run format - - name: Commit formatting if needed - shell: bash - run: | - if git diff --quiet; then - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "chore: apply adopted formatting" - git push From ff799d1520087c01c6b9f3bd88bf2e3a4d6064cc Mon Sep 17 00:00:00 2001 From: Sunil Date: Thu, 8 Oct 2026 21:13:16 +0530 Subject: [PATCH 20/50] fix(server): refuse cross-site requests on the terrain-heights route Follow-up to #686 requested on #923. The route's per-client limiter keys on the socket address, which for a cross-site is the visitor's own browser, so any web page could fill the bounded cache with junk keys and spend the visitor's 90/min quota, leaving the app's own terrain lookups on 429. Gate the route with admitSameSite before the limiter, like the Places routes. Co-authored-by: daikaginza --- scripts/package-boundaries.json | 4 ++- server/providers/terrain.js | 9 +++++- src/proxyErrorResponses.test.mjs | 3 ++ src/terrainHeightsBounds.test.mjs | 46 +++++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index e1af1ed50..610986341 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -113,7 +113,9 @@ "server/providers/terrain.js", "src/data/terrainHeightsProxy.js", "server/providers/common/rate-limit.js", - "src/sources/rateLimit.js" + "src/sources/rateLimit.js", + "server/providers/common/same-site.js", + "src/localRequestGate.mjs" ], "external": [] }, diff --git a/server/providers/terrain.js b/server/providers/terrain.js index ca9442db1..b2615094e 100644 --- a/server/providers/terrain.js +++ b/server/providers/terrain.js @@ -9,6 +9,7 @@ import { validTerrainResult, } from '../../src/data/terrainHeightsProxy.js'; import { makeRateLimiter, clientKey } from './common/rate-limit.js'; +import { admitSameSite } from './common/same-site.js'; /** * Re:Earth terrain point-height proxy: batched lon/lat → ellipsoidal height @@ -25,7 +26,9 @@ import { makeRateLimiter, clientKey } from './common/rate-limit.js'; * Bounds, because every distinct 5dp point is its own key: coordinates must be * in WGS-84 range (rejected at parse), the point cache has an entry ceiling * with oldest-first eviction, an over-large cache file is not read back, and - * the route carries an always-on per-client limiter. + * the route carries an always-on per-client limiter. The route also refuses + * cross-site browser requests, so another web page cannot use the visitor's + * browser to fill the cache or spend the visitor's own rate-limit quota. */ export function terrainHeightsProxy(options = {}) { // Destructured in the body, not the signature: `proxyErrorResponses.test.mjs` @@ -231,6 +234,10 @@ export function terrainHeightsProxy(options = {}) { const installMiddleware = (server) => { server.middlewares.use('/api/terrain/heights', async (req, res) => { + // Refuse cross-site browser requests (an or fetch from another + // site) before the limiter, cache or upstream see them. Same-origin app + // requests and non-browser loopback callers (MCP panel_request) pass. + if (admitSameSite(req, res)) return; const send = (status, bodyObj) => { if (res.headersSent) return; res.writeHead(status, { 'Content-Type': 'application/json' }); diff --git a/src/proxyErrorResponses.test.mjs b/src/proxyErrorResponses.test.mjs index 9cc1ff9c7..35845ee88 100644 --- a/src/proxyErrorResponses.test.mjs +++ b/src/proxyErrorResponses.test.mjs @@ -26,6 +26,9 @@ function fixture(name, overrides = {}, preview = false) { // The rate-limited proxies reach for these; give them the real ones so a // fixture request is throttled exactly as production would be. makeRateLimiter, clientKey, + // terrain.js gates cross-site requests first; fixture requests carry no + // browser headers, so admit them like a loopback tool. + admitSameSite: () => false, path, process: { cwd: () => '/fixture', env: {} }, fsp: { readFile: async () => { throw new Error('cache absent'); }, diff --git a/src/terrainHeightsBounds.test.mjs b/src/terrainHeightsBounds.test.mjs index 1a0c80290..83edc262d 100644 --- a/src/terrainHeightsBounds.test.mjs +++ b/src/terrainHeightsBounds.test.mjs @@ -29,10 +29,18 @@ function install(plugin) { return routes; } -function request(handler, { url = '/', remoteAddress = '127.0.0.1' } = {}) { +function request( + handler, + { url = '/', remoteAddress = '127.0.0.1', reqHeaders = {} } = {}, +) { return new Promise((resolve, reject) => { const headers = new Map(); - const req = { method: 'GET', url, headers: {}, socket: { remoteAddress } }; + const req = { + method: 'GET', + url, + headers: reqHeaders, + socket: { remoteAddress }, + }; const res = { statusCode: 200, headersSent: false, @@ -207,6 +215,40 @@ test('the route refuses a flood of requests from one client', async (t) => { assert.equal(refusedAt, 91, 'the 91st request in a minute is refused'); }); +test('a cross-site page cannot fill the cache or spend the visitor quota', async (t) => { + const asked = stubTerrainUpstream(t); + const route = install(terrainHeightsProxy()).get('/api/terrain/heights'); + const host = 'localhost:5173'; + const refused = [ + { host, 'sec-fetch-site': 'cross-site' }, // from any web page + { host, 'sec-fetch-site': 'same-site' }, + { host, origin: 'https://evil.example' }, // fetch from another page + { host, origin: 'null' }, // sandboxed frame + { host, 'x-forwarded-for': '203.0.113.9' }, // through a proxy + ]; + // More refused requests than the 90/min quota, each a distinct new key. + for (let i = 0; i < 100; i += 1) { + const reqHeaders = refused[i % refused.length]; + const answer = await request(route, { + url: `/?points=${(-97 - i / 1000).toFixed(5)},30.26`, + reqHeaders, + }); + assert.equal(answer.statusCode, 403, JSON.stringify(reqHeaders)); + } + assert.deepEqual(asked, [], 'nothing reached upstream or the cache'); + + // The visitor's own app still has its full quota afterwards, and a + // loopback tool that sends no browser headers is admitted too. + const app = await request(route, { + url: '/?points=-97.74,30.26', + reqHeaders: { host, 'sec-fetch-site': 'same-origin' }, + }); + assert.equal(app.statusCode, 200); + const tool = await request(route, { url: '/?points=-97.75,30.26' }); + assert.equal(tool.statusCode, 200); + assert.equal(asked.length, 2); +}); + test('a second client keeps its own quota', async (t) => { stubFetch(t, async () => Response.json([{ lng: -97.74, lat: 30.26, height: 150 }]), From e38672b45d7660381afbb36e37af68ddad595157 Mon Sep 17 00:00:00 2001 From: Sunil Date: Fri, 9 Oct 2026 14:38:41 +0530 Subject: [PATCH 21/50] docs: document terrain same-site gate --- CHANGELOG.md | 4 ++++ docs/CURRENT-STATE.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a155d92..c5b572bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,10 @@ answer, since terrain does not move. Ordinary use is unaffected: a full `test:track` run drives 10 requests, peaking at 8/min. +- `/api/terrain/heights` refuses cross-site browser requests before rate + limiting, cache access or upstream work. Same-origin app requests and + non-browser local callers remain supported. + - Add an experimental ChatGPT/Codex OAuth option for cloud voice in Provider Settings. API-key voice remains the default. Local sign-in can start from the auth button and reports completion, failure, timeout, or expired diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c7afdd24c..7c260baff 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1103,7 +1103,10 @@ plugin order, server-key selection, validation, disk caches, budgets, retries and stale/error responses remain unchanged. Each has a Node-only package entry under `gods-eye-view/server/providers/`. Portable terrain mechanics, traffic tile math and GBFS source rules are available under `gods-eye-view/sources/`. -The browser layers and their rendering remain in their existing modules. +The `/api/terrain/heights` route refuses cross-site browser requests before +rate limiting, cache access or upstream work. Same-origin app requests and +non-browser local callers remain supported in development and preview. The +browser layers and their rendering remain in their existing modules. ## Terrain height cache bound From 2110d06e36eb7b7ed0a6cf595257c414ecde2587 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:32:45 -0700 Subject: [PATCH 22/50] feat(tools): count and summarize every match in the area tools aircraft_in_area and vessels_in_area return at most 200 rows, so a client asked which operators dominate an area, or the average altitude there, could only estimate from a truncated sample. Both tools now take an optional group_by (aircraft: operator, origin_country, type_code; ships: type, destination) and stats (airborne altitude and speed for aircraft, speed for ships). Each is computed over every match before rows are capped. Breakdowns list the ten largest groups plus other and unknown counts, and the summary names the three largest. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ++ docs/CURRENT-STATE.md | 6 ++- docs/TOOLS.md | 4 +- scripts/format-scope.json | 1 + src/tools/queries/aviation.js | 37 +++++++++++++- src/tools/queries/aviation.test.mjs | 57 +++++++++++++++++++++ src/tools/queries/maritime.js | 22 +++++++- src/tools/queries/maritime.test.mjs | 25 +++++++++ src/tools/results.js | 69 +++++++++++++++++++++++++ src/tools/results.test.mjs | 78 +++++++++++++++++++++++++++++ 10 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 src/tools/results.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ab1c52d..0a5f41549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so a + client can ask which operators or ship types dominate an area, or the average + altitude or speed, over every match instead of estimating from the returned + rows. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 274294b5d..520bb9207 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -93,7 +93,11 @@ perimeters, terrain height, military installations and map features, plus a combined situation brief, military awareness around a point, the app's heads-up display caption, and a link that opens the app over an area. Tools reuse the layers' portable source factories, take a shared `area` argument (place name, bounding box, or point and radius) and cap -lists at 25 rows by default. Voice offers the same queries next to its app +lists at 25 rows by default. `aircraft_in_area` and `vessels_in_area` can also +count every match by a field (`group_by`: operator, country or type for +aircraft; type or destination for ships; the ten largest groups, then other and +unknown counts) and summarize airborne altitude and speed (`stats`: count, min, +median, mean, max). Both cover all matches, not just the rows returned. Voice offers the same queries next to its app actions: the session lists them, and the browser runs them through the same catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 401058d91..f3b86f7a3 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -263,11 +263,11 @@ and the development server's internal routes. See SECURITY.md. | `get_earthquakes` | `earthquakes` | USGS M2.5+ events in the last 24 hours, strongest first | | `get_active_fires` | `fires` | NASA FIRMS detections in an area, highest radiative power first | | `get_recent_launches` | `launches` | Launch Library 2 launches in the last 30 days, newest first | -| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed | +| `aircraft_in_area` | `aircraft` | Aircraft in an area, nearest first; `military: true` reads the `military` feed; `group_by` and `stats` count and summarize every match | | `find_aircraft` | `aircraft` | Aircraft anywhere by callsign, ICAO address or registration | | `get_aircraft_track` | `aircraft` | Recent positions of one aircraft, thinned to 200 points | | `get_aircraft_info` | `aircraft` | Aircraft type and registration, and flight route, from adsbdb | -| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type | +| `vessels_in_area` | `vessels` | Ships reported by AIS in an area, nearest first, optionally by type; `group_by` and `stats` count and summarize every match | | `find_vessel` | `vessels` | Ships anywhere by MMSI, IMO number or name | | `get_vessel_track` | `vessels` | Recent positions of one ship, thinned to 200 points | | `next_satellite_pass` | `satellites` | Next pass over a place or point (default the ISS), with naked-eye visibility | diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 6b5c9b544..f8c056177 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -941,6 +941,7 @@ "scripts/qa-context-arrivals.mjs", "src/tools/catalog.test.mjs", "src/tools/area.test.mjs", + "src/tools/results.test.mjs", "src/tools/queries/queries.test.mjs", "src/tools/mcp/mcp.test.mjs", "src/tools/localServer.test.mjs", diff --git a/src/tools/queries/aviation.js b/src/tools/queries/aviation.js index 0b8b54b79..e3aca82c4 100644 --- a/src/tools/queries/aviation.js +++ b/src/tools/queries/aviation.js @@ -11,9 +11,12 @@ import { } from '../area.js'; import { LIMIT_SCHEMA, + breakdown, + breakdownPhrase, capRows, countNoun, isoTime, + numericSummary, thinEvenly, } from '../results.js'; @@ -32,6 +35,11 @@ const MILITARY_SCHEMA = Object.freeze({ description: 'Use the military aircraft feed instead of the general feed.', }); const MAX_TRACK_POINTS = 200; +const GROUP_LABELS = Object.freeze({ + operator: 'operator', + origin_country: 'country', + type_code: 'type', +}); const round = (value, digits) => Number.isFinite(value) ? Number(value.toFixed(digits)) : null; @@ -82,7 +90,10 @@ export const aircraftInArea = defineTool({ title: 'Aircraft in an area', description: 'Aircraft currently reported inside an area, nearest the center first. ' + - 'Optionally military aircraft only, airborne only, or within an altitude band.', + 'Optionally military aircraft only, airborne only, or within an altitude band. ' + + 'group_by counts every matched aircraft by operator, country or type, and ' + + 'stats summarizes airborne altitude and speed; both cover all matches, not ' + + 'just the rows returned.', inputSchema: { type: 'object', properties: { @@ -91,6 +102,16 @@ export const aircraftInArea = defineTool({ airborne_only: { type: 'boolean' }, min_altitude_m: { type: 'number', minimum: -500, maximum: 30000 }, max_altitude_m: { type: 'number', minimum: -500, maximum: 30000 }, + group_by: { + type: 'string', + enum: Object.keys(GROUP_LABELS), + description: 'Count matched aircraft by this field, largest first.', + }, + stats: { + type: 'boolean', + description: + 'Summarize airborne altitude and speed over matched aircraft.', + }, limit: LIMIT_SCHEMA, }, required: ['area'], @@ -126,9 +147,11 @@ export const aircraftInArea = defineTool({ ...(regional ? [`regional feed: ${snapshot.coverage}`] : []), ...(snapshot.freshness === 'stale' ? ['data may be stale'] : []), ]; + const groups = args.group_by ? breakdown(rows, args.group_by) : null; return { summary: `${rows.length} ${kind} in ${area.label}` + + (groups ? breakdownPhrase(groups, GROUP_LABELS[args.group_by]) : '') + (notes.length ? ` (${notes.join('; ')}).` : '.'), data: { view: suggestView(services, { @@ -136,6 +159,18 @@ export const aircraftInArea = defineTool({ layers: [args.military ? 'military' : 'flights'], }), ...capRows(rows, args.limit), + ...(groups ? { breakdown: groups } : {}), + ...(args.stats + ? { + stats: { + altitude_m: numericSummary( + rows.filter((row) => !row.on_ground), + 'altitude_m', + ), + speed_mps: numericSummary(rows, 'speed_mps'), + }, + } + : {}), ...snapshotInfo(snapshot), }, }; diff --git a/src/tools/queries/aviation.test.mjs b/src/tools/queries/aviation.test.mjs index 20ec833aa..618085b00 100644 --- a/src/tools/queries/aviation.test.mjs +++ b/src/tools/queries/aviation.test.mjs @@ -388,3 +388,60 @@ test('the real OpenSky source marks an hour-old snapshot stale for searches', as assert.deepEqual(result.data.stale_feeds, ['civil']); assert.match(result.summary, /may be stale\.$/); }); + +test('aircraft in an area can be counted by operator over every match', async () => { + const fleet = [ + record('a1', 37.6, -122.3, { operator: 'United' }), + record('a2', 37.61, -122.3, { operator: 'Delta' }), + record('a3', 37.62, -122.3, { operator: 'United', baroAltitudeM: 3000 }), + record('a4', 37.63, -122.3, { onGround: true, baroAltitudeM: 0 }), + record('a5', 51.5, -0.1, { operator: 'British Airways' }), + ]; + const catalog = composeCatalog({ + tools: coreTools, + services: { aircraft: feed(fleet) }, + }); + const result = await catalog.call('aircraft_in_area', { + area: bay, + group_by: 'operator', + stats: true, + limit: 1, + }); + assert.equal( + result.summary, + '4 aircraft in 60 km around 37.600, -122.300; most by operator: United (2), Delta (1).', + ); + assert.equal(result.data.returned, 1); + assert.deepEqual(result.data.breakdown, { + field: 'operator', + groups: [ + { value: 'United', count: 2 }, + { value: 'Delta', count: 1 }, + ], + other_count: 0, + unknown_count: 1, + }); + // Altitude covers airborne aircraft only; speed covers every match. + assert.deepEqual(result.data.stats.altitude_m, { + count: 3, + min: 3000, + median: 10000, + mean: 7666.7, + max: 10000, + }); + assert.equal(result.data.stats.speed_mps.count, 4); +}); + +test('aircraft answers carry no breakdown or stats unless asked', async () => { + const catalog = composeCatalog({ + tools: coreTools, + services: { aircraft: feed(civil) }, + }); + const result = await catalog.call('aircraft_in_area', { area: bay }); + assert.equal('breakdown' in result.data, false); + assert.equal('stats' in result.data, false); + await assert.rejects( + catalog.call('aircraft_in_area', { area: bay, group_by: 'callsign' }), + /must be one of/, + ); +}); diff --git a/src/tools/queries/maritime.js b/src/tools/queries/maritime.js index 5b64f2826..a918c1f52 100644 --- a/src/tools/queries/maritime.js +++ b/src/tools/queries/maritime.js @@ -12,9 +12,12 @@ import { } from '../area.js'; import { LIMIT_SCHEMA, + breakdown, + breakdownPhrase, capRows, countNoun, isoTime, + numericSummary, thinEvenly, } from '../results.js'; @@ -88,7 +91,9 @@ export const vesselsInArea = defineTool({ title: 'Vessels in an area', description: 'Ships currently reported by AIS inside an area, nearest the center ' + - 'first, optionally filtered by vessel type.', + 'first, optionally filtered by vessel type. group_by counts every matched ' + + 'ship by type or destination, and stats summarizes their speed; both ' + + 'cover all matches, not just the rows returned.', inputSchema: { type: 'object', properties: { @@ -99,6 +104,15 @@ export const vesselsInArea = defineTool({ maxLength: 40, description: 'Vessel type to match, such as "tanker" or "cargo".', }, + group_by: { + type: 'string', + enum: ['type', 'destination'], + description: 'Count matched vessels by this field, largest first.', + }, + stats: { + type: 'boolean', + description: 'Summarize speed over matched vessels.', + }, limit: LIMIT_SCHEMA, }, required: ['area'], @@ -119,13 +133,19 @@ export const vesselsInArea = defineTool({ .map((record) => vesselRow(record, center)) .sort((a, b) => a.distance_km - b.distance_km); const kind = args.type ? `${args.type} vessel` : 'vessel'; + const groups = args.group_by ? breakdown(rows, args.group_by) : null; return { summary: `${countNoun(rows.length, kind)} in ${area.label}` + + (groups ? breakdownPhrase(groups, args.group_by) : '') + (snapshot.freshness === 'stale' ? ' (data may be stale).' : '.'), data: { view: suggestView(services, { area, layers: ['ais-live-vessels'] }), ...capRows(rows, args.limit), + ...(groups ? { breakdown: groups } : {}), + ...(args.stats + ? { stats: { speed_mps: numericSummary(rows, 'speed_mps') } } + : {}), ...snapshotInfo(snapshot), }, }; diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index 785e38223..d1c43e60a 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -224,3 +224,28 @@ test('an area search names its area to the server; a vessel lookup asks for ever assert.equal(lookup.has('lat'), false); assert.equal(lookup.has('radius_km'), false); }); + +test('vessels in an area can be counted by destination with a speed summary', async () => { + const catalog = composeCatalog({ + tools: coreTools, + services: { vessels: vessels() }, + }); + const result = await catalog.call('vessels_in_area', { + area: bay, + group_by: 'destination', + stats: true, + }); + assert.equal( + result.summary, + '2 vessels in 20 km around 37.780, -122.400; most by destination: OAKLAND (1).', + ); + assert.deepEqual(result.data.breakdown, { + field: 'destination', + groups: [{ value: 'OAKLAND', count: 1 }], + other_count: 0, + unknown_count: 1, + }); + assert.deepEqual(result.data.stats, { + speed_mps: { count: 2, min: 6.2, median: 6.2, mean: 6.2, max: 6.2 }, + }); +}); diff --git a/src/tools/results.js b/src/tools/results.js index 51ed82bbb..b4a24c72a 100644 --- a/src/tools/results.js +++ b/src/tools/results.js @@ -22,6 +22,75 @@ export function capRows(rows, limit = DEFAULT_LIMIT) { }; } +/** Most groups a breakdown lists before counting the rest as other. */ +export const MAX_GROUPS = 10; + +/** + * Count rows by one text field over every row, not just those returned. + * Values group case-insensitively and keep their first spelling; rows + * without a value are counted as unknown. + */ +export function breakdown(rows, field) { + const groups = new Map(); + let unknown = 0; + for (const row of rows) { + const value = typeof row[field] === 'string' ? row[field].trim() : ''; + if (!value) { + unknown += 1; + continue; + } + const key = value.toLowerCase(); + const group = groups.get(key); + if (group) group.count += 1; + else groups.set(key, { value, count: 1 }); + } + const sorted = [...groups.values()].sort( + (a, b) => b.count - a.count || a.value.localeCompare(b.value), + ); + const listed = sorted.slice(0, MAX_GROUPS); + return { + field, + groups: listed, + other_count: rows.length - unknown - sum(listed.map((g) => g.count)), + unknown_count: unknown, + }; +} + +/** Minimum, median, mean and maximum of one numeric field, or null. */ +export function numericSummary(rows, field) { + const values = rows + .map((row) => row[field]) + .filter(Number.isFinite) + .sort((a, b) => a - b); + if (!values.length) return null; + const middle = values.length >> 1; + const median = + values.length % 2 + ? values[middle] + : (values[middle - 1] + values[middle]) / 2; + const round1 = (value) => Number(value.toFixed(1)); + return { + count: values.length, + min: values[0], + median: round1(median), + mean: round1(sum(values) / values.length), + max: values.at(-1), + }; +} + +/** "; most by operator: Delta (12), United (9), American (7)" or "". */ +export function breakdownPhrase(result, label) { + if (!result.groups.length) return ''; + const top = result.groups + .slice(0, 3) + .map((group) => `${group.value} (${group.count})`); + return `; most by ${label}: ${top.join(', ')}`; +} + +function sum(values) { + return values.reduce((total, value) => total + value, 0); +} + /** Format an epoch millisecond time as ISO 8601, or null. */ export function isoTime(ms) { return Number.isFinite(ms) ? new Date(ms).toISOString() : null; diff --git a/src/tools/results.test.mjs b/src/tools/results.test.mjs new file mode 100644 index 000000000..ba67b9f64 --- /dev/null +++ b/src/tools/results.test.mjs @@ -0,0 +1,78 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + MAX_GROUPS, + breakdown, + breakdownPhrase, + numericSummary, +} from './results.js'; + +test('a breakdown counts every row, case-insensitively, largest first', () => { + const rows = [ + { operator: 'Delta' }, + { operator: 'United' }, + { operator: ' delta ' }, + { operator: null }, + { operator: '' }, + { operator: 'American' }, + { operator: 'United' }, + { operator: 'DELTA' }, + ]; + assert.deepEqual(breakdown(rows, 'operator'), { + field: 'operator', + groups: [ + { value: 'Delta', count: 3 }, + { value: 'United', count: 2 }, + { value: 'American', count: 1 }, + ], + other_count: 0, + unknown_count: 2, + }); +}); + +test('groups past the limit are counted as other', () => { + const rows = Array.from({ length: MAX_GROUPS + 3 }, (_, index) => ({ + type: `T${String(index).padStart(2, '0')}`, + })); + rows.push({ type: 'T00' }); + const result = breakdown(rows, 'type'); + assert.equal(result.groups.length, MAX_GROUPS); + assert.deepEqual(result.groups[0], { value: 'T00', count: 2 }); + assert.equal(result.other_count, 3); + assert.equal( + result.groups.reduce((total, group) => total + group.count, 0) + + result.other_count + + result.unknown_count, + rows.length, + ); +}); + +test('a numeric summary skips missing values', () => { + const rows = [{ v: 4 }, { v: null }, { v: 1 }, { v: 10 }, { v: 3 }, {}]; + assert.deepEqual(numericSummary(rows, 'v'), { + count: 4, + min: 1, + median: 3.5, + mean: 4.5, + max: 10, + }); + assert.equal(numericSummary([{ v: 2 }, { v: 7 }, { v: 5 }], 'v').median, 5); + assert.equal(numericSummary([{ v: null }], 'v'), null); + assert.equal(numericSummary([], 'v'), null); +}); + +test('the breakdown phrase names the three largest groups', () => { + const result = { + groups: [ + { value: 'A', count: 4 }, + { value: 'B', count: 3 }, + { value: 'C', count: 2 }, + { value: 'D', count: 1 }, + ], + }; + assert.equal( + breakdownPhrase(result, 'type'), + '; most by type: A (4), B (3), C (2)', + ); + assert.equal(breakdownPhrase({ groups: [] }, 'type'), ''); +}); From 603f428397f61ddd06893b3178081398f5f9c50c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:35:24 -0700 Subject: [PATCH 23/50] fix(tools): say when a breakdown's field is missing from the feed OpenSky, the default civil feed, reports no operator or type code, so a breakdown by either put every aircraft under unknown while the summary said nothing. The summary now says "no operator reported" in that case, and otherwise appends the unknown count after the largest groups. Co-Authored-By: Claude Opus 5.5 --- docs/CURRENT-STATE.md | 5 +++-- src/tools/queries/aviation.js | 4 +++- src/tools/queries/aviation.test.mjs | 2 +- src/tools/queries/maritime.test.mjs | 2 +- src/tools/results.js | 13 ++++++++++--- src/tools/results.test.mjs | 12 +++++++++++- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 520bb9207..f9e9c02a7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -96,8 +96,9 @@ shared `area` argument (place name, bounding box, or point and radius) and cap lists at 25 rows by default. `aircraft_in_area` and `vessels_in_area` can also count every match by a field (`group_by`: operator, country or type for aircraft; type or destination for ships; the ten largest groups, then other and -unknown counts) and summarize airborne altitude and speed (`stats`: count, min, -median, mean, max). Both cover all matches, not just the rows returned. Voice offers the same queries next to its app +unknown counts; the summary says when a feed reports none of the field) and +summarize airborne altitude and speed (`stats`: count, min, median, mean, max). +Both cover all matches, not just the rows returned. Voice offers the same queries next to its app actions: the session lists them, and the browser runs them through the same catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). diff --git a/src/tools/queries/aviation.js b/src/tools/queries/aviation.js index e3aca82c4..23652f211 100644 --- a/src/tools/queries/aviation.js +++ b/src/tools/queries/aviation.js @@ -105,7 +105,9 @@ export const aircraftInArea = defineTool({ group_by: { type: 'string', enum: Object.keys(GROUP_LABELS), - description: 'Count matched aircraft by this field, largest first.', + description: + 'Count matched aircraft by this field, largest first. Feeds that do not ' + + 'report the field count those aircraft as unknown.', }, stats: { type: 'boolean', diff --git a/src/tools/queries/aviation.test.mjs b/src/tools/queries/aviation.test.mjs index 618085b00..b6b6e3497 100644 --- a/src/tools/queries/aviation.test.mjs +++ b/src/tools/queries/aviation.test.mjs @@ -409,7 +409,7 @@ test('aircraft in an area can be counted by operator over every match', async () }); assert.equal( result.summary, - '4 aircraft in 60 km around 37.600, -122.300; most by operator: United (2), Delta (1).', + '4 aircraft in 60 km around 37.600, -122.300; most by operator: United (2), Delta (1); 1 unknown.', ); assert.equal(result.data.returned, 1); assert.deepEqual(result.data.breakdown, { diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index d1c43e60a..662e5bdf2 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -237,7 +237,7 @@ test('vessels in an area can be counted by destination with a speed summary', as }); assert.equal( result.summary, - '2 vessels in 20 km around 37.780, -122.400; most by destination: OAKLAND (1).', + '2 vessels in 20 km around 37.780, -122.400; most by destination: OAKLAND (1); 1 unknown.', ); assert.deepEqual(result.data.breakdown, { field: 'destination', diff --git a/src/tools/results.js b/src/tools/results.js index b4a24c72a..528f59ea5 100644 --- a/src/tools/results.js +++ b/src/tools/results.js @@ -78,13 +78,20 @@ export function numericSummary(rows, field) { }; } -/** "; most by operator: Delta (12), United (9), American (7)" or "". */ +/** + * "; most by operator: Delta (12), United (9), American (7); 4 unknown", or + * "; no operator reported" when no row carries the field. + */ export function breakdownPhrase(result, label) { - if (!result.groups.length) return ''; + if (!result.groups.length) + return result.unknown_count ? `; no ${label} reported` : ''; const top = result.groups .slice(0, 3) .map((group) => `${group.value} (${group.count})`); - return `; most by ${label}: ${top.join(', ')}`; + const unknown = result.unknown_count + ? `; ${result.unknown_count} unknown` + : ''; + return `; most by ${label}: ${top.join(', ')}${unknown}`; } function sum(values) { diff --git a/src/tools/results.test.mjs b/src/tools/results.test.mjs index ba67b9f64..2e480afcd 100644 --- a/src/tools/results.test.mjs +++ b/src/tools/results.test.mjs @@ -63,6 +63,7 @@ test('a numeric summary skips missing values', () => { test('the breakdown phrase names the three largest groups', () => { const result = { + unknown_count: 0, groups: [ { value: 'A', count: 4 }, { value: 'B', count: 3 }, @@ -74,5 +75,14 @@ test('the breakdown phrase names the three largest groups', () => { breakdownPhrase(result, 'type'), '; most by type: A (4), B (3), C (2)', ); - assert.equal(breakdownPhrase({ groups: [] }, 'type'), ''); + assert.equal( + breakdownPhrase({ ...result, unknown_count: 5 }, 'type'), + '; most by type: A (4), B (3), C (2); 5 unknown', + ); + // A feed that never reports the field says so instead of staying silent. + assert.equal( + breakdownPhrase({ groups: [], unknown_count: 236 }, 'operator'), + '; no operator reported', + ); + assert.equal(breakdownPhrase({ groups: [], unknown_count: 0 }, 'type'), ''); }); From 1371a867137aebcdcd488213a0b2ce7b3e42bfa9 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:11:47 -0700 Subject: [PATCH 24/50] docs: say the area breakdowns reach MCP clients only Voice leaves aircraft_in_area and vessels_in_area out, so the changelog no longer suggests spoken questions gain breakdowns. It also stops leading with operators, which the default civil feed never reports. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++---- docs/CURRENT-STATE.md | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5f41549..b918e89cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog -- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so a - client can ask which operators or ship types dominate an area, or the average - altitude or speed, over every match instead of estimating from the returned - rows. +- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an + MCP client can ask which countries, aircraft types or ship types dominate an + area, or the average altitude or speed, over every match instead of + estimating from the returned rows. Operator and type are counted only where + the feed reports them. - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f9e9c02a7..bdc9573b4 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -98,7 +98,8 @@ count every match by a field (`group_by`: operator, country or type for aircraft; type or destination for ships; the ten largest groups, then other and unknown counts; the summary says when a feed reports none of the field) and summarize airborne altitude and speed (`stats`: count, min, median, mean, max). -Both cover all matches, not just the rows returned. Voice offers the same queries next to its app +Both cover all matches, not just the rows returned; voice leaves these two +tools out, so they reach MCP clients only. Voice offers the same queries next to its app actions: the session lists them, and the browser runs them through the same catalog, loaded on first use. See [tools and the MCP server](TOOLS.md). From d9e2c8a7e744c3fff55f8e28b097a4c922fd4fac Mon Sep 17 00:00:00 2001 From: Kevin Rajan <7121943+kvnloo@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:38:19 -0500 Subject: [PATCH 25/50] fix(ui): enforce compact Radio ownership on right-rail restores Close the overlay when adaptive layout restores an auto-collapsed Context directly. Regression covers the reachable rail reflow path; changelog credits the original report by raysung-2026 (#804). --- CHANGELOG.md | 5 ++ src/ui/panelChrome.js | 16 ++++- src/ui/panelChrome.radioOwnership.test.mjs | 69 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0c5f754..802407a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +- Prevent a compact Radio disclosure from covering Space Missions when the + adaptive right rail restores Context without calling the panel owner. + Repeated Context-open calls and rail-driven expansion both close the mini, + with a regression for auto-collapse restoration (raysung-2026, #804). + - CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD diff --git a/src/ui/panelChrome.js b/src/ui/panelChrome.js index 6464f1588..0717a1313 100644 --- a/src/ui/panelChrome.js +++ b/src/ui/panelChrome.js @@ -68,7 +68,8 @@ export class PanelChrome { variant: this.hud.getVariant(), }), scheduleCockpitLayout: () => this.cockpitView?.scheduleContextLayout(), - syncPanelCollapseButton: (panel) => this._syncPanelCollapseButton(panel), + syncPanelCollapseButton: (panel) => + this._syncRailPanelCollapseButton(panel), readDisplayScrollTop: () => this._displayPortalScrollRestoreOwner === 'standard' ? this._standardDisplayScrollTop @@ -282,6 +283,19 @@ export class PanelChrome { return this._panelLayout._scheduleLeftPanelLayout(options0); } + _syncRailPanelCollapseButton(panelEl) { + // Adaptive rail layout may directly restore an auto-collapsed Context + // without invoking setPanelCollapsed(). Enforce Radio ownership there too. + if ( + panelEl?.id === 'global-context-panel' && + !panelEl.classList.contains('collapsed') && + this._contextRadioDock?.classList.contains('disclosure-open') + ) { + this._setRadioDisclosure?.(false); + } + this._syncPanelCollapseButton(panelEl); + } + _syncPanelCollapseButton(panelEl) { const isRightRail = [ 'pp-toggles', diff --git a/src/ui/panelChrome.radioOwnership.test.mjs b/src/ui/panelChrome.radioOwnership.test.mjs index c52a4888e..1ecc1c7b9 100644 --- a/src/ui/panelChrome.radioOwnership.test.mjs +++ b/src/ui/panelChrome.radioOwnership.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { PanelChrome } from './panelChrome.js'; +import { PanelLayoutController } from './panelLayoutController.js'; function classList(initial = []) { const values = new Set(initial); @@ -65,3 +66,71 @@ test('opening an already-open Context still closes a stale compact Radio disclos assert.deepEqual(calls, [false]); assert.equal(radioDock.classList.contains('disclosure-open'), false); }); + +test('adaptive rail restore hides compact Radio before revealing Context', (t) => { + const priorRaf = globalThis.requestAnimationFrame; + let nextFrame; + globalThis.requestAnimationFrame = (callback) => { + nextFrame = callback; + return 1; + }; + t.after(() => { + globalThis.requestAnimationFrame = priorRaf; + }); + + // Valid starting state: the tactical rail automatically collapsed Context, + // and the user opened the compact Radio controls from its header. + const contextPanel = { + id: 'global-context-panel', + classList: classList(['collapsed', 'layout-auto-collapsed']), + }; + const radioDock = { classList: classList(['disclosure-open']) }; + const radioMini = { hidden: false }; + const calls = []; + const chrome = { + _contextRadioDock: radioDock, + _setRadioDisclosure(open) { + calls.push(`disclosure:${open}`); + radioDock.classList.toggle('disclosure-open', open); + radioMini.hidden = !open; + }, + _syncPanelCollapseButton(panel) { + calls.push(`button:${panel.id}`); + }, + }; + const layout = { + destroyed: false, + _rightStackLayoutFrame: null, + _rightStackReconsiderAutoCollapse: false, + _rightPanelStack: { + querySelectorAll: (selector) => + selector === '.layout-auto-collapsed' ? [contextPanel] : [], + }, + _syncPanelCollapseButton(panel) { + PanelChrome.prototype._syncRailPanelCollapseButton.call(chrome, panel); + }, + _syncRightPanelAdaptiveLayout() { + calls.push('layout'); + }, + }; + + PanelLayoutController.prototype._scheduleRightPanelLayout.call(layout, { + reconsiderAutoCollapse: true, + }); + assert.equal(contextPanel.classList.contains('collapsed'), true); + assert.equal(radioMini.hidden, false); + nextFrame(); + + assert.equal(contextPanel.classList.contains('collapsed'), false); + assert.equal( + contextPanel.classList.contains('layout-auto-collapsed'), + false, + ); + assert.equal(radioDock.classList.contains('disclosure-open'), false); + assert.equal(radioMini.hidden, true); + assert.deepEqual(calls, [ + 'disclosure:false', + 'button:global-context-panel', + 'layout', + ]); +}); From 52863a9f422c4120b6533339b770b8ca7995121c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:06:10 -0700 Subject: [PATCH 26/50] fix(tools): count AIS ship type 0 as unknown in vessel breakdowns AIS type 0 means "not available" (ITU-R M.1371), but the source keeps it as '0', so group_by: 'type' listed "0" as a type and understated unknown_count. vessels_in_area rows now report it as null, and a regression fixture mixes type 0 with Cargo and Tanker. Co-Authored-By: Claude Opus 5.5 --- src/tools/queries/maritime.js | 8 ++++++- src/tools/queries/maritime.test.mjs | 35 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/tools/queries/maritime.js b/src/tools/queries/maritime.js index a918c1f52..b5bdc4afe 100644 --- a/src/tools/queries/maritime.js +++ b/src/tools/queries/maritime.js @@ -32,6 +32,12 @@ const ALL_VESSELS = 1_000_000; const round = (value, digits) => Number.isFinite(value) ? Number(value.toFixed(digits)) : null; const text = (value) => (typeof value === 'string' && value ? value : null); +// AIS ship type 0 is "not available" (ITU-R M.1371); the source keeps it as +// '0', so it is reported as unknown rather than counted as a type. +const vesselType = (value) => { + const type = text(value); + return type === '0' ? null : type; +}; function vesselRow(record, from) { const point = { lat: record.latitude, lon: record.longitude }; @@ -39,7 +45,7 @@ function vesselRow(record, from) { mmsi: record.id, name: text(record.name), imo: text(record.imo), - type: text(record.type), + type: vesselType(record.type), destination: text(record.destination), lat: round(record.latitude, 5), lon: round(record.longitude, 5), diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index 662e5bdf2..c5a5ab21f 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -225,6 +225,41 @@ test('an area search names its area to the server; a vessel lookup asks for ever assert.equal(lookup.has('radius_km'), false); }); +test('AIS type 0 (not available) is counted as unknown, not as a type', async () => { + const catalog = composeCatalog({ + tools: coreTools, + services: { + vessels: vessels({ + records: [ + vessel('366000001', 37.78, -122.4, { type: '0' }), + vessel('366000002', 37.79, -122.41, { type: '0' }), + vessel('366000003', 37.77, -122.39, { type: 'Cargo' }), + vessel('366000004', 37.76, -122.38, { type: 'Tanker' }), + ], + }), + }, + }); + const result = await catalog.call('vessels_in_area', { + area: bay, + group_by: 'type', + }); + assert.deepEqual(result.data.breakdown, { + field: 'type', + groups: [ + { value: 'Cargo', count: 1 }, + { value: 'Tanker', count: 1 }, + ], + other_count: 0, + unknown_count: 2, + }); + assert.match(result.summary, /most by type: Cargo \(1\), Tanker \(1\); 2 unknown\./); + assert.doesNotMatch(result.summary, /0 \(/); + assert.equal( + result.data.rows.find((row) => row.mmsi === '366000001').type, + null, + ); +}); + test('vessels in an area can be counted by destination with a speed summary', async () => { const catalog = composeCatalog({ tools: coreTools, From 60444cca53af98c21713d2fe4825efc3d20b69c2 Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:06:24 -0700 Subject: [PATCH 27/50] style: format the AIS type 0 regression test Co-Authored-By: Claude Opus 5.5 --- src/tools/queries/maritime.test.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tools/queries/maritime.test.mjs b/src/tools/queries/maritime.test.mjs index c5a5ab21f..f1d09bccb 100644 --- a/src/tools/queries/maritime.test.mjs +++ b/src/tools/queries/maritime.test.mjs @@ -252,7 +252,10 @@ test('AIS type 0 (not available) is counted as unknown, not as a type', async () other_count: 0, unknown_count: 2, }); - assert.match(result.summary, /most by type: Cargo \(1\), Tanker \(1\); 2 unknown\./); + assert.match( + result.summary, + /most by type: Cargo \(1\), Tanker \(1\); 2 unknown\./, + ); assert.doesNotMatch(result.summary, /0 \(/); assert.equal( result.data.rows.find((row) => row.mmsi === '366000001').type, From 8f6671b6c271abc9464ac990259a03db6943695c Mon Sep 17 00:00:00 2001 From: quanticsoul4772 <71096528+quanticsoul4772@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:22:39 -0700 Subject: [PATCH 28/50] Merge upstream main into feat/tools-area-breakdowns Move the changelog entry into changelog.d/1056-area-tool-breakdowns.md (upstream's one-file-per-PR rule, #787) and leave CHANGELOG.md as on main. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ----- changelog.d/1056-area-tool-breakdowns.md | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/1056-area-tool-breakdowns.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 22dce6a93..8ee00d839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,5 @@ # Changelog -- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an - MCP client can ask which countries, aircraft types or ship types dominate an - area, or the average altitude or speed, over every match instead of - estimating from the returned rows. Operator and type are counted only where - the feed reports them. - Local FM seek finds stations after wrapping around 87.5–108 MHz, covers both band edges, skips the starting station and rejects spectra from an earlier tune (Aditya, @Rohilalala, #1174). diff --git a/changelog.d/1056-area-tool-breakdowns.md b/changelog.d/1056-area-tool-breakdowns.md new file mode 100644 index 000000000..75f713907 --- /dev/null +++ b/changelog.d/1056-area-tool-breakdowns.md @@ -0,0 +1,6 @@ +- `aircraft_in_area` and `vessels_in_area` take `group_by` and `stats`, so an + MCP client can ask which countries, aircraft types or ship types dominate an + area, or the average altitude or speed, over every match instead of + estimating from the returned rows. Operator and type are counted only where + the feed reports them; AIS ship type 0 ("not available") counts as unknown + (quanticsoul4772, #1056). From c7ff33ac2e0e7f4ab16562e652a24d64e2bd2a3a Mon Sep 17 00:00:00 2001 From: Sunil Date: Sat, 17 Oct 2026 14:36:24 +0530 Subject: [PATCH 29/50] fix(server): admit reverse-proxied same-origin terrain requests Review follow-up on #975. The shared same-site gate refuses any forwarding header, which is right for routes that spend a key, but terrain is keyless and needed for rendering: behind the reverse proxy the README documents, every /api/terrain/heights chunk got 403 and fell back to the geoid. Terrain now uses admitSameSiteKeyless, which keeps the Sec-Fetch-Site and exact-Origin checks (comparing Origin against a single X-Forwarded-Proto scheme) but does not refuse forwarding headers. The shared admitSameSite gate is unchanged for every other route. The changelog entry moves to changelog.d/975-terrain-cross-site-gate.md. Co-authored-by: daikaginza --- SECURITY.md | 2 +- changelog.d/975-terrain-cross-site-gate.md | 8 ++ server/providers/common/same-site.js | 45 ++++++++++ server/providers/terrain.js | 9 +- src/proxyErrorResponses.test.mjs | 2 +- src/terrainHeightsBounds.test.mjs | 96 +++++++++++++++++++++- 6 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 changelog.d/975-terrain-cross-site-gate.md diff --git a/SECURITY.md b/SECURITY.md index e53883548..b694d8dcd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -73,7 +73,7 @@ The data proxies under `server/providers/` are written so the browser cannot tur - **Sanitized errors** — internal error details are not echoed back to clients. - **Coalesced OAuth refresh** and cached successful responses only (OpenSky). - **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing. -- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles, and `/api/tomtom/*`, backed by its persistent daily tile budget) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an ``/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. +- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`), quota-bearing (`/api/mapillary/*`, with a per-IP limit on tiles, and `/api/tomtom/*`, backed by its persistent daily tile budget) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an ``/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. The keyless terrain route (`/api/terrain/heights`) uses the same cross-site checks, minus the forwarding-header refusal: it guards only the per-client quota and point cache, not a key or a bill, and the map needs it to render behind a reverse proxy. - **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads and the Maps JavaScript API that Street View runs on, with a matching `frame-src` for their players and posts: no inline script and no other script origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `