Merge branch 'main' into feat/voice-geometry

Picks up #1308 (Overpass admission), #114 (Sea Marks and Ski Pistes),
#1206 (radio country coverage) and #1435. Resolutions:
- SECURITY.md: main's rewritten cross-site paragraph, with voice place
  search listed beside /api/overpass among the keyless map routes.
- scripts/format-scope.json: both sides. A merge on main dropped 97 entries
  (being restored there); keeping them, this PR's 12 and main's 2 new ones
  keeps every file in format:check. All 1,222 paths exist.
- Translation catalogs: main's two new keys and this PR's 47, no overlap.
- Layer count 36 (33 + OSM Places + Sea Marks + Ski Pistes); both schema
  pins re-derived, the first-run length is exactly 30,919 + 292 + 107.
This commit is contained in:
Manjunath committed 2026-11-03 22:16:52 +05:30
commit 1e12d5ba26
56 files changed
+1834 -209

No files matched your search

+2
View File
@@ -30,6 +30,7 @@ import { createApplicationFirePerimeters } from './layers/perimeters.js';
import { createApplicationGnssIntegrity } from './layers/gnss.js';
import { createApplicationGhostMaps } from './layers/ghostMaps.js';
import { createApplicationCables } from './layers/submarineCables.js';
import { createOsmOverlayLayers } from '../data/osmOverlays.js';
import { createApplicationStreetLevel } from './layers/streetLevel.js';
import { createInfrastructureLayers } from '../data/infrastructure.js';
import { localGeoJsonServices } from './localGeojsonServices.js';
@@ -173,6 +174,7 @@ export function createApplicationCatalog({
clock: weatherClock,
}),
createCyclonesLayer({ feed: sources.cyclones }),
...createOsmOverlayLayers(),
...createInfrastructureLayers(localGeoJsonServices),
createApplicationCables({ source: sources.cables }),
createApplicationFirms({
+2 -2
View File
@@ -40,7 +40,7 @@ test('catalogs construct distinct layers and classification from their supplied
signal: b.signal,
surface: fixtureSurface(b.signal),
});
assert.equal(first.layers.length, 34);
assert.equal(first.layers.length, 36);
assert.ok(first.get('local-adsb'), 'Local ADS-B is registered');
assert.deepEqual(
first.metadata.find(({ id }) => id === 'osm-places'),
@@ -160,7 +160,7 @@ test('Mapillary can be omitted or replaced without changing catalog membership',
surface: fixtureSurface(lifetime.signal),
};
const catalog = createApplicationCatalog(options);
assert.equal(catalog.layers.length, 34);
assert.equal(catalog.layers.length, 36);
assert.deepEqual(catalog.get('street-level').providerIds, []);
assert.equal(
catalogControlServices(catalog).streetLevelLayer,
+1 -1
View File
@@ -37,7 +37,7 @@ test('every acquisition source can be omitted without losing catalog membership'
test('all acquisition sources may be absent, and enable fails before initializing or fetching', async (t) => {
const actual = catalog({}, t);
assert.equal(actual.layers.length, 34);
assert.equal(actual.layers.length, 36);
const manager = new LayerLifecycle(
{},
{ getSourceAvailability: actual.getSourceAvailability },
+14
View File
@@ -150,6 +150,20 @@ export const DATA_CREDITS = [
key: 'usgs',
html: 'Earthquakes: Data courtesy of the U.S. Geological Survey',
},
{
key: 'openseamap',
html:
'Sea marks overlay: ' +
'<a href="https://www.openseamap.org" target="_blank" rel="noopener">© OpenSeaMap contributors</a> (CC BY-SA 2.0) · ' +
'Data © OpenStreetMap contributors (ODbL)',
},
{
key: 'opensnowmap',
html:
'Ski pistes overlay: ' +
'<a href="https://www.opensnowmap.org" target="_blank" rel="noopener">© www.opensnowmap.org</a> (CC BY-SA) · ' +
'Data © OpenStreetMap contributors (ODbL)',
},
{
key: 'nasa-gibs',
html:
+2
View File
@@ -666,6 +666,8 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
disposition: 'enabled+options',
optionOwner: 'satellites',
}),
Object.freeze({ id: 'sea-marks', token: '6', disposition: 'enabled-only' }),
Object.freeze({ id: 'ski-pistes', token: '7', disposition: 'enabled-only' }),
Object.freeze({
id: 'street-level',
token: '0',
+13 -15
View File
@@ -202,8 +202,8 @@ function encode(state) {
test('production registry is exact, canonical, and rejects incomplete contracts', async () => {
assert.equal(validateLayerStateRegistry(), true);
assert.equal(REGISTERED_LAYER_IDS.length, 32);
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 32);
assert.equal(REGISTERED_LAYER_IDS.length, 34);
assert.equal(new Set(REGISTERED_LAYER_IDS).size, 34);
assert.ok(REGISTERED_LAYER_IDS.includes('transit'));
assert.deepEqual(REGISTERED_LAYER_IDS, [...REGISTERED_LAYER_IDS].sort());
assert.deepEqual(LEGACY_LAYER_STATE_TOKENS, {
@@ -242,20 +242,18 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
assert.equal(LAYER_STATE_TOKEN_RESERVATIONS[id], token);
}
assert.equal(nextLayerStateToken(), '6');
assert.equal(nextLayerStateToken(), '8');
assert.equal(
nextLayerStateToken({
...LAYER_STATE_TOKEN_RESERVATIONS,
alpha: '6',
bravo: '7',
alpha: '8',
bravo: '9',
}),
'8',
'00',
);
const digitsExhausted = {
...LAYER_STATE_TOKEN_RESERVATIONS,
...Object.fromEntries(
[...'6789'].map((digit) => [`prior-${digit}`, digit]),
),
...Object.fromEntries([...'89'].map((digit) => [`prior-${digit}`, digit])),
};
assert.equal(nextLayerStateToken(digitsExhausted), '00');
assert.equal(
@@ -307,8 +305,8 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
assert.equal(
validateLayerStateAllocations(LAYER_STATE_TOKEN_RESERVATIONS, {
...LAYER_STATE_TOKEN_RESERVATIONS,
future: '6',
next: '7',
future: '8',
next: '9',
}),
true,
);
@@ -318,7 +316,7 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
...LAYER_STATE_TOKEN_RESERVATIONS,
future: '00',
}),
/next free token 6/,
/next free token 8/,
);
const beforeLastDigit = { ...digitsExhausted };
delete beforeLastDigit['prior-9'];
@@ -341,10 +339,10 @@ test('production registry is exact, canonical, and rejects incomplete contracts'
assert.throws(
() =>
validateLayerStateAllocations(
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '6' },
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '6', competing: '6' },
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '8' },
{ ...LAYER_STATE_TOKEN_RESERVATIONS, merged: '8', competing: '8' },
),
/next free token 7/,
/next free token 9/,
);
assert.throws(
() => validateLayerStateAllocations({ future: '00' }, { future: '01' }),
+30 -28
View File
@@ -128,7 +128,7 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
{ ...parseLayerStateTokenReservations(reservationRows) },
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
);
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 32);
assert.equal(Object.keys(LAYER_STATE_TOKEN_RESERVATIONS).length, 34);
assert.deepEqual(
{ ...LAYER_STATE_TOKEN_RESERVATIONS },
{
@@ -137,6 +137,8 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
'gnss-integrity': '3',
'air-quality': '4',
's2-ghostmaps': '5',
'sea-marks': '6',
'ski-pistes': '7',
},
);
assert.throws(
@@ -162,9 +164,9 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
/Duplicate layer-state token reservation/,
);
assert.equal(
parseLayerStateTokenReservations([...reservationRows, ['future', '6']])
parseLayerStateTokenReservations([...reservationRows, ['future', '8']])
.future,
'6',
'8',
);
assert.throws(
() =>
@@ -182,14 +184,14 @@ test('reservation ledger is complete, pinned, and rejects duplicate or malformed
test('published retired digits and competing PRs advance the merge-time allocation', () => {
withPublishedBase(
{ rows: [...reservationRows, ['retired-layer', '6']] },
{ rows: [...reservationRows, ['retired-layer', '8']] },
(cwd) => {
const published = readPublishedLayerStateReservations('HEAD', cwd);
assert.equal(nextLayerStateToken(published), '7');
assert.equal(nextLayerStateToken(published), '9');
assert.equal(
validateLayerStateAllocations(published, {
...published,
newcomer: '7',
newcomer: '9',
}),
true,
);
@@ -197,15 +199,15 @@ test('published retired digits and competing PRs advance the merge-time allocati
() =>
validateLayerStateAllocations(published, {
...published,
competing: '6',
competing: '8',
}),
/next free token 7/,
/next free token 9/,
);
assert.throws(
() =>
validateLayerStateAllocations(published, {
...LAYER_STATE_TOKEN_RESERVATIONS,
newcomer: '6',
newcomer: '8',
}),
/changed or removed: retired-layer/,
);
@@ -213,8 +215,8 @@ test('published retired digits and competing PRs advance the merge-time allocati
);
});
test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () => {
withPublishedBase({ rows: [...reservationRows, ['pr-a', '6']] }, (cwd) => {
test('PR B manually replaces provisional 8 with 9 after PR A publishes 8', () => {
withPublishedBase({ rows: [...reservationRows, ['pr-a', '8']] }, (cwd) => {
mkdirSync(path.join(cwd, 'scripts'));
const fixtureChecker = path.join(
cwd,
@@ -235,8 +237,8 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
writeCodecFixture(
cwd,
[...reservationRows, ['pr-b', '6']],
[{ id: 'pr-b', token: '6' }],
[...reservationRows, ['pr-b', '8']],
[{ id: 'pr-b', token: '8' }],
);
const beforeRebase = runCheck();
assert.equal(beforeRebase.status, 1);
@@ -244,10 +246,10 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
writeCodecFixture(
cwd,
[...reservationRows, ['pr-a', '6'], ['pr-b', '6']],
[...reservationRows, ['pr-a', '8'], ['pr-b', '8']],
[
{ id: 'pr-a', token: '6' },
{ id: 'pr-b', token: '6' },
{ id: 'pr-a', token: '8' },
{ id: 'pr-b', token: '8' },
],
);
const staleRebase = runCheck();
@@ -255,16 +257,16 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
assert.match(staleRebase.stderr, /Duplicate layer-state token reservation/);
const published = readPublishedLayerStateReservations('HEAD', cwd);
assert.equal(nextLayerStateToken(published), '7');
const validBaselineRows = [...reservationRows, ['pr-a', '6']];
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '6' }]);
assert.equal(nextLayerStateToken(published), '9');
const validBaselineRows = [...reservationRows, ['pr-a', '8']];
writeCodecFixture(cwd, validBaselineRows, [{ id: 'pr-a', token: '8' }]);
const helper = spawnSync(
process.execPath,
[realpathSync(fixtureNext), 'pr-b'],
{ cwd, encoding: 'utf8' },
);
assert.equal(helper.status, 0, helper.stderr);
assert.match(helper.stdout, /pr-b: 7/);
assert.match(helper.stdout, /pr-b: 9/);
assert.deepEqual(
JSON.parse(readFileSync(path.join(cwd, LEDGER_PATH), 'utf8')),
validBaselineRows,
@@ -272,20 +274,20 @@ test('PR B manually replaces provisional 6 with 7 after PR A publishes 6', () =>
);
writeCodecFixture(
cwd,
[...reservationRows, ['pr-a', '6'], ['pr-b', '7']],
[...reservationRows, ['pr-a', '8'], ['pr-b', '9']],
[
{ id: 'pr-a', token: '6' },
{ id: 'pr-b', token: '7' },
{ id: 'pr-a', token: '8' },
{ id: 'pr-b', token: '9' },
],
);
const corrected = runCheck();
assert.equal(corrected.status, 0, corrected.stderr);
assert.match(corrected.stdout, /33 published, 1 new/);
assert.match(corrected.stdout, /35 published, 1 new/);
});
});
test('allocation batches cross the last digit and base-36 pair boundaries in order', () => {
const digitRows = [...'6789'].map((digit) => [`retired-${digit}`, digit]);
const digitRows = [...'89'].map((digit) => [`retired-${digit}`, digit]);
const allDigits = parseLayerStateTokenReservations([
...reservationRows,
...digitRows,
@@ -337,7 +339,7 @@ test('allocation batches cross the last digit and base-36 pair boundaries in ord
test('an isolated valid two-character fixture round-trips an l field beyond the old 64-character cap', async () => {
const cwd = mkdtempSync(path.join(tmpdir(), 'gev-layer-token-width-'));
try {
const priorDigits = [...'6789'].map((digit) => [
const priorDigits = [...'89'].map((digit) => [
`qa-prior-digit-${digit}`,
digit,
]);
@@ -374,7 +376,7 @@ test('an isolated valid two-character fixture round-trips an l field beyond the
const params = new URLSearchParams([['v', '2']]);
codec.encodeLayerStateParams(params, state);
assert.ok(params.get('l').length > 64, 'fixture must cross the old cap');
assert.equal(params.get('l').length, 75);
assert.equal(params.get('l').length, 79);
const restored = codec.decodeLayerStateParams(params);
assert.deepEqual(restored?.enabledLayerIds, expectedLayerIds);
assert.deepEqual(restored?.options, state.options);
@@ -392,7 +394,7 @@ test('checker reads a complete future base ledger and rejects retired-token reus
},
(cwd) => {
const published = readPublishedLayerStateReservations('HEAD', cwd);
assert.equal(Object.keys(published).length, 33);
assert.equal(Object.keys(published).length, 35);
assert.equal(published['retired-layer'], '00');
assert.throws(
() =>
@@ -22,6 +22,8 @@
["rocket-launches", "x"],
["s2-ghostmaps", "5"],
["satellites", "s"],
["sea-marks", "6"],
["ski-pistes", "7"],
["street-level", "0"],
["telegeography-submarine-cables", "u"],
["traffic", "t"],
+190
View File
@@ -0,0 +1,190 @@
import * as Cesium from 'cesium';
import { NO_IMAGERY_HOST, resolveImageryHost } from '../maps/imageryHost.js';
/**
* @file Open-data raster overlays: OpenSeaMap sea marks and OpenSnowMap ski
* pistes (#10). Each overlay is its own layer with its own row under
* Infrastructure; both are built from the descriptors below.
*
* OpenRailwayMap and OpenInfraMap, also named in #10, are deliberately not
* included:
* - OpenRailwayMap's public tile server (tiles.openrailwaymap.org) only
* serves requests whose Referer is openrailwaymap.org; every other Referer,
* including none, gets 403. A tab on this app sends its own origin, so the
* tiles would fail, and embedding them would run against their tile-usage
* policy either way.
* - OpenInfraMap serves MapLibre vector tiles, not raster XYZ tiles. That
* needs a vector-tile rendering pipeline, not an imagery-layer drop-in.
*
* The overlays drape on whatever surface the active map stack offers (see
* `maps/imageryHost.js`): the globe on the OSM and Bing stacks, the Google
* Photorealistic tileset's own `imageryLayers` on Google 3D. Only a stack with
* neither leaves them hidden, and the row says so.
* @module data/osmOverlays
*/
/** @type {ReadonlyArray<{id:string,name:string,icon:string,source:string,urlTemplate:string,credit:string,maximumLevel:number}>} */
export const OSM_OVERLAYS = Object.freeze([
Object.freeze({
id: 'sea-marks',
name: 'Sea Marks',
icon: '⚓',
source: 'OpenSeaMap',
urlTemplate: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
// Chart tiles CC BY-SA 2.0; the underlying data is OpenStreetMap (ODbL).
credit:
'Sea marks © OpenSeaMap contributors (CC BY-SA 2.0) · Data © OpenStreetMap contributors (ODbL)',
maximumLevel: 18,
}),
Object.freeze({
id: 'ski-pistes',
name: 'Ski Pistes',
icon: '⛷',
source: 'OpenSnowMap',
urlTemplate: 'https://www.opensnowmap.org/pistes/{z}/{x}/{y}.png',
// OpenSnowMap asks for "(c)www.opensnowmap.org" wherever its tiles appear.
credit:
'Ski pistes © www.opensnowmap.org (CC BY-SA) · Data © OpenStreetMap contributors (ODbL)',
maximumLevel: 18,
}),
]);
/** Why an enabled overlay is showing nothing, or null. */
export function osmOverlayHiddenReason(enabled, hostKind) {
return enabled && hostKind === 'none' ? NO_IMAGERY_HOST : null;
}
/**
* Create the layer for one overlay descriptor. Each application catalog gets
* its own instances; the Cesium factories are injectable so tests run without
* WebGL.
*/
export function createOsmOverlayLayer(
overlay,
{
host = resolveImageryHost,
createProvider = (descriptor) =>
new Cesium.UrlTemplateImageryProvider({
url: descriptor.urlTemplate,
credit: descriptor.credit,
maximumLevel: descriptor.maximumLevel,
}),
createImageryLayer = (provider) => new Cesium.ImageryLayer(provider),
} = {},
) {
if (!OSM_OVERLAYS.includes(overlay))
throw new TypeError(`Unknown open map overlay: ${overlay?.id}`);
let viewer = null;
let mapStack = null;
let unsubscribeMap = null;
let enabled = false;
let hostInfo = { collection: null, kind: 'none' };
/** The imagery layer this instance owns while enabled, and where it sits. */
let owned = null;
const usable = (collection) => collection && !collection.isDestroyed?.();
function detach(destroy) {
if (!owned) return;
const { collection, layer } = owned;
if (usable(collection) && collection.contains(layer))
collection.remove(layer, destroy);
else if (destroy && !layer.isDestroyed?.()) layer.destroy?.();
owned.collection = null;
if (destroy) owned = null;
}
function resolveHost() {
return (
host({ viewer, tileset: mapStack?.getImageryHostTileset?.() }) || {
collection: null,
kind: 'none',
}
);
}
/** Create, move or drop the owned imagery layer to match the state. */
function sync() {
if (!viewer) return;
hostInfo = enabled ? resolveHost() : { collection: null, kind: 'none' };
if (!enabled) {
detach(true);
} else {
owned ||= {
layer: createImageryLayer(createProvider(overlay)),
collection: null,
};
if (owned.collection !== hostInfo.collection) {
detach(false);
if (usable(hostInfo.collection)) {
// Appended, so it draws above the map stack's base imagery.
hostInfo.collection.add(owned.layer);
owned.collection = hostInfo.collection;
}
}
}
viewer.scene?.requestRender?.();
}
return {
id: overlay.id,
name: overlay.name,
icon: overlay.icon,
source: overlay.source,
updateInterval: 0,
init(nextViewer) {
viewer = nextViewer;
},
/** Follow map-stack switches so the drape moves with the surface. */
attachMapStackController(controller) {
unsubscribeMap?.();
unsubscribeMap = null;
mapStack = controller || null;
const off = controller?.subscribe?.(() => {
if (enabled) sync();
});
unsubscribeMap = typeof off === 'function' ? off : null;
},
enable() {
enabled = true;
sync();
},
disable() {
enabled = false;
sync();
},
update() {
return true;
},
getStats() {
// Without a controller subscription the stats poll doubles as the
// map-stack watch.
if (enabled && !unsubscribeMap) {
const next = resolveHost();
if (next.collection !== hostInfo.collection) sync();
}
return {
count: enabled ? 1 : 0,
error: osmOverlayHiddenReason(enabled, hostInfo.kind),
};
},
/** Owned layer count and host kind, for tests and live gates. */
diagnostics() {
return { host: hostInfo.kind, ownedCount: owned ? 1 : 0 };
},
destroy() {
enabled = false;
detach(true);
unsubscribeMap?.();
unsubscribeMap = null;
mapStack = null;
viewer = null;
hostInfo = { collection: null, kind: 'none' };
},
};
}
/** One layer per overlay, in panel order. */
export function createOsmOverlayLayers(options) {
return OSM_OVERLAYS.map((overlay) => createOsmOverlayLayer(overlay, options));
}
+167
View File
@@ -0,0 +1,167 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
OSM_OVERLAYS,
createOsmOverlayLayer,
createOsmOverlayLayers,
osmOverlayHiddenReason,
} from './osmOverlays.js';
import { NO_IMAGERY_HOST } from '../maps/imageryHost.js';
function fakeCollection() {
const layers = [];
const removed = [];
return {
layers,
removed,
add(layer) {
layers.push(layer);
},
contains(layer) {
return layers.includes(layer);
},
remove(layer, destroy) {
const index = layers.indexOf(layer);
if (index < 0) return false;
layers.splice(index, 1);
removed.push({ layer, destroy });
if (destroy) layer.destroyed = true;
return true;
},
};
}
function fakeMapStack() {
const subscribers = new Set();
return {
tileset: null,
getImageryHostTileset() {
return this.tileset;
},
subscribe(fn) {
subscribers.add(fn);
return () => subscribers.delete(fn);
},
emit() {
for (const fn of subscribers) fn();
},
subscribers,
};
}
function setup(overlay = OSM_OVERLAYS[0]) {
const globe = fakeCollection();
const viewer = {
imageryLayers: globe,
scene: { globe: { show: true }, requestRender() {} },
};
const created = [];
const layer = createOsmOverlayLayer(overlay, {
createProvider: (descriptor) => ({ descriptor }),
createImageryLayer: (provider) => {
const imagery = { provider, destroyed: false };
created.push(imagery);
return imagery;
},
});
const mapStack = fakeMapStack();
layer.init(viewer);
layer.attachMapStackController(mapStack);
return { layer, viewer, globe, created, mapStack };
}
test('OSM_OVERLAYS: the two shipped layers, each with a tile template and credit', () => {
assert.deepEqual(
OSM_OVERLAYS.map((o) => o.id),
['sea-marks', 'ski-pistes'],
);
for (const overlay of OSM_OVERLAYS) {
assert.match(overlay.urlTemplate, /^https:\/\/.*\{z\}.*\{x\}.*\{y\}/);
assert.match(overlay.credit, /OpenStreetMap contributors \(ODbL\)/);
}
assert.match(OSM_OVERLAYS[0].credit, /CC BY-SA 2\.0/);
assert.match(OSM_OVERLAYS[1].credit, /www\.opensnowmap\.org/);
});
test('createOsmOverlayLayers builds one independent layer per overlay', () => {
const layers = createOsmOverlayLayers();
assert.deepEqual(
layers.map((layer) => [layer.id, layer.name]),
[
['sea-marks', 'Sea Marks'],
['ski-pistes', 'Ski Pistes'],
],
);
assert.notEqual(createOsmOverlayLayers()[0], layers[0]);
assert.throws(() => createOsmOverlayLayer({ id: 'other' }), /Unknown/);
});
test('osmOverlayHiddenReason: only when enabled with no imagery host', () => {
assert.equal(osmOverlayHiddenReason(false, 'none'), null);
assert.equal(osmOverlayHiddenReason(true, 'globe'), null);
assert.equal(osmOverlayHiddenReason(true, 'tileset'), null);
assert.equal(osmOverlayHiddenReason(true, 'none'), NO_IMAGERY_HOST);
});
test('a layer owns no imagery until enabled and destroys it on disable', () => {
const { layer, globe, created } = setup(OSM_OVERLAYS[1]);
assert.equal(created.length, 0);
layer.enable();
assert.equal(globe.layers.length, 1);
assert.equal(globe.layers[0].provider.descriptor.id, 'ski-pistes');
assert.deepEqual(layer.getStats(), { count: 1, error: null });
layer.disable();
assert.equal(globe.layers.length, 0);
assert.equal(created[0].destroyed, true);
assert.deepEqual(layer.diagnostics(), { host: 'none', ownedCount: 0 });
assert.deepEqual(layer.getStats(), { count: 0, error: null });
layer.enable();
assert.equal(created.length, 2, 're-enabling creates a fresh imagery layer');
});
test('the overlay follows a map-stack switch onto the Google 3D tileset and back', () => {
const { layer, viewer, globe, mapStack } = setup();
layer.enable();
const [imagery] = globe.layers;
const tileset = { imageryLayers: fakeCollection() };
viewer.scene.globe.show = false;
mapStack.tileset = tileset;
mapStack.emit();
assert.equal(globe.layers.length, 0);
assert.deepEqual(tileset.imageryLayers.layers, [imagery]);
assert.equal(globe.removed[0].destroy, false);
assert.equal(layer.diagnostics().host, 'tileset');
assert.equal(layer.getStats().error, null);
viewer.scene.globe.show = true;
mapStack.tileset = null;
mapStack.emit();
assert.deepEqual(globe.layers, [imagery]);
assert.equal(tileset.imageryLayers.layers.length, 0);
});
test('with no surface to drape on the row explains itself, and recovers when one appears', () => {
const { layer, viewer, globe, mapStack } = setup();
viewer.scene.globe.show = false;
layer.enable();
assert.equal(globe.layers.length, 0);
assert.equal(layer.getStats().error, NO_IMAGERY_HOST);
viewer.scene.globe.show = true;
mapStack.emit();
assert.equal(globe.layers.length, 1);
assert.equal(layer.getStats().error, null);
});
test('destroy removes owned imagery and unsubscribes from the map stack', () => {
const { layer, globe, created, mapStack } = setup();
layer.enable();
layer.destroy();
assert.equal(globe.layers.length, 0);
assert.equal(created[0].destroyed, true);
assert.equal(mapStack.subscribers.size, 0);
});
+211
View File
@@ -541,6 +541,191 @@ test('a mirror answering an invalid reason phrase rejects instead of crashing',
await assert.rejects(Promise.race([fetchMirror(), crashed]), TypeError);
});
test('priority-country configuration is bounded, normalized and validated before requests', async () => {
const { parseRadioPriorityCountries } = await import('../../server/providers/radio/catalog.js');
assert.deepEqual(parseRadioPriorityCountries(' in, PL,in, '), ['IN', 'PL']);
assert.deepEqual(parseRadioPriorityCountries(''), []);
assert.deepEqual(parseRadioPriorityCountries('uk,GB,UK'), ['GB']);
assert.deepEqual(parseRadioPriorityCountries('US,CA,IN,PL,DE,FR,UK,GB,JP'), ['US', 'CA', 'IN', 'PL', 'DE', 'FR', 'GB', 'JP']);
for (const value of ['ZZ', 'India', 'IN&limit=9999', 'US,CA,IN,PL,DE,FR,GB,JP,AU']) {
assert.throws(() => createProxy({ priorityCountries: value }), /RADIO_PRIORITY_COUNTRIES/);
}
});
test('UK aliases query and report GB once and retain British stations', async () => {
const countryQueries = [];
const middleware = createProxy({
priorityCountries: 'UK,GB',
fetchImpl: async (url) => {
const parsed = new URL(url);
if (parsed.pathname === '/json/servers')
return responseJson([{ name: 'de1.api.radio-browser.info' }]);
const country = parsed.searchParams.get('countrycode');
if (country) {
countryQueries.push(country);
return responseJson([station({ country: 'United Kingdom', countrycode: 'GB' })]);
}
return responseJson(healthyRowsForQuery(url));
},
});
const response = await invoke(middleware, '/stations');
assert.equal(response.status, 200);
const body = JSON.parse(response.body);
assert.deepEqual(countryQueries, ['GB']);
assert.deepEqual(body.coverage.priorityCountries, [
{ countryCode: 'GB', succeeded: true, stationCount: 1 },
]);
assert.equal(body.stations.find((row) => row.id === UUID)?.countryCode, 'GB');
});
test('low-click country stations survive a full global catalog, with bounded cached queries and deduplication', async () => {
const calls = [];
let active = 0;
let peak = 0;
const middleware = createProxy({
priorityCountries: 'IN,PL',
fetchImpl: async (url) => {
const parsed = new URL(url);
calls.push(parsed);
if (parsed.pathname === '/json/servers') return responseJson([{ name: 'de1.api.radio-browser.info' }]);
if (parsed.pathname.startsWith('/json/url/')) return responseJson({ ok: true });
peak = Math.max(peak, ++active);
await new Promise((resolve) => setTimeout(resolve, 1));
active--;
const country = parsed.searchParams.get('countrycode');
if (country) {
assert.equal(parsed.searchParams.get('limit'), '200');
assert.equal(parsed.searchParams.get('is_https'), 'true');
const rows = catalogRows(country === 'IN' ? '10000000' : '20000000', 'community', 60)
.map((row) => ({ ...row, countrycode: country, clickcount: 0 }));
return responseJson([rows[0], ...rows]);
}
return responseJson(catalogRows('30000000', queryTag(url) || 'news', 900));
},
});
const responses = await Promise.all([invoke(middleware, '/stations'), invoke(middleware, '/stations')]);
const body = JSON.parse(responses[0].body);
assert.equal(body.degraded, false);
assert.equal(body.stations.length, 750);
assert.equal(new Set(body.stations.map((row) => row.id)).size, 750);
for (const country of ['IN', 'PL'])
assert.ok(body.stations.filter((row) => row.countryCode === country).length >= 40);
assert.ok(body.stations.some((row) => row.countryCode === 'US'));
assert.equal(calls.filter((url) => url.pathname.includes('/stations/')).length, 11);
assert.ok(peak <= 3);
const count = calls.length;
await invoke(middleware, '/stations');
assert.equal(calls.length, count, 'country queries share the 45-minute cache');
const id = body.stations.find((row) => row.countryCode === 'IN').id;
assert.equal((await invoke(middleware, `/click/${id}`, 'POST')).status, 204);
});
test('country failures never block cold or warm global catalog admission, even after seven days', async () => {
for (const coldFailure of [false, true]) {
let clock = 4_000_000;
let fail = coldFailure;
let calls = 0;
const middleware = createProxy({
priorityCountries: ['IN'], now: () => clock,
fetchImpl: async (url) => {
calls++;
if (String(url).includes('/json/servers')) return responseJson([{ name: 'de1.api.radio-browser.info' }]);
if (new URL(url).searchParams.has('countrycode')) {
if (fail) throw new Error('offline');
return responseJson([station({ countrycode: 'IN', country: 'India', tags: 'community' })]);
}
return responseJson(healthyRowsForQuery(url));
},
});
let generation = 0;
for (const elapsed of [0, 46 * 60 * 1000, 8 * 24 * 60 * 60 * 1000]) {
clock += elapsed;
if (elapsed) fail = true;
const response = await invoke(middleware, '/stations');
const body = JSON.parse(response.body);
assert.equal(response.status, 200);
assert.equal(body.stale, false);
assert.equal(body.degraded, false);
assert.ok(body.acceptedGeneration > generation);
generation = body.acceptedGeneration;
assert.equal(body.coverage.priorityCountries[0].succeeded, !fail);
assert.equal(body.coverage.priorityCountries[0].stationCount, fail ? 0 : 1);
const count = calls;
clock += 60_001;
assert.deepEqual(JSON.parse((await invoke(middleware, '/stations')).body), body);
assert.equal(calls, count, 'country failure does not shorten the healthy cache');
}
}
});
test('valid countries with no eligible stations are accepted and cached; malformed country responses stay isolated', async () => {
for (const mode of ['wrong', 'oversized', 'empty', 'ineligible', 'malformed']) {
let clock = 4_000_000;
let calls = 0;
const middleware = createProxy({
priorityCountries: ['IN'], now: () => clock,
fetchImpl: async (url) => {
calls++;
if (String(url).includes('/json/servers')) return responseJson([{ name: 'de1.api.radio-browser.info' }]);
if (new URL(url).searchParams.has('countrycode')) {
const rows = {
empty: [], wrong: [station()], malformed: [{ name: 'missing fields' }],
oversized: catalogRows('10000000', 'community', 201).map((row) => ({ ...row, countrycode: 'IN' })),
ineligible: [{ hls: 1 }, { codec: 'OGG' }, { codec: 'OPUS' }, { url_resolved: 'https://127.0.0.1/live.mp3' }]
.map((overrides) => station({ countrycode: 'IN', ...overrides })),
};
return responseJson(rows[mode]);
}
return responseJson(healthyRowsForQuery(url));
},
});
const response = await invoke(middleware, '/stations');
const body = JSON.parse(response.body);
assert.equal(response.status, 200, mode);
assert.equal(body.degraded, false, mode);
assert.ok(body.acceptedGeneration > 0, mode);
assert.equal(body.coverage.priorityCountries[0].stationCount, 0, mode);
assert.equal(body.coverage.priorityCountries[0].succeeded, !['oversized', 'malformed'].includes(mode), mode);
const count = calls;
clock += 44 * 60 * 1000;
assert.deepEqual(JSON.parse((await invoke(middleware, '/stations')).body), body);
assert.equal(calls, count, `${mode} country response preserves the 45-minute cache`);
}
});
for (const priorityCountries of ['', 'IN']) {
test(`cold failures coalesce, bound mirrors and back off with priority countries ${priorityCountries || 'unset'}`, async () => {
let clock = Date.UTC(2026, 0, 1);
let calls = 0;
let offline = true;
const middleware = createProxy({
priorityCountries, now: () => clock,
fetchImpl: async (url) => {
calls++;
if (String(url).includes('/json/servers')) return responseJson([{ name: 'de1.api.radio-browser.info' }]);
if (offline) throw new Error('offline');
if (new URL(url).searchParams.has('countrycode')) return responseJson([]);
return responseJson(healthyRowsForQuery(url));
},
});
const responses = await Promise.all([invoke(middleware, '/stations'), invoke(middleware, '/stations')]);
assert.ok(responses.every((response) => response.status === 503));
const initial = calls;
assert.equal(initial, 1 + (priorityCountries ? 10 : 9) * 3, 'one discovery and at most three mirrors per query');
offline = false;
clock += 59_999;
assert.equal((await invoke(middleware, '/stations')).status, 503);
assert.equal(calls, initial);
clock += 2;
const recovered = await invoke(middleware, '/stations');
assert.equal(recovered.status, 200);
assert.equal(JSON.parse(recovered.body).degraded, false);
assert.ok(calls > initial);
const afterRecovery = calls;
await invoke(middleware, '/stations');
assert.equal(calls, afterRecovery);
});
}
test('case expansion is capped after tag normalization in the portable and server station path', () => {
const row = normalizeRadioBrowserStation(station({tags: 'İ'.repeat(80) + ', NEWS__RADIO ,news-radio'}));
@@ -599,3 +784,29 @@ test('server structural rejections reach the client diagnostic without discardin
radioLayer.destroy();
}
});
test('country coverage retains valid rows and upstream rejection diagnostics in a cached healthy catalog', async () => {
let calls = 0;
const middleware = createProxy({
priorityCountries: 'IN',
fetchImpl: async (url) => {
calls++;
if (String(url).includes('/json/servers')) return responseJson([{ name: 'de1.api.radio-browser.info' }]);
if (new URL(url).searchParams.has('countrycode')) return responseJson([
station({ stationuuid: '60000000-0000-4000-8000-000000000001', countrycode: 'IN' }),
{ name: 'missing required fields' },
]);
return responseJson(healthyRowsForQuery(url));
},
});
const response = await invoke(middleware, '/stations');
const body = JSON.parse(response.body);
assert.equal(response.status, 200);
assert.equal(body.degraded, false);
assert.equal(body.rejectedStationCount, 1);
assert.deepEqual(body.coverage.priorityCountries, [{ countryCode: 'IN', succeeded: true, stationCount: 1 }]);
const count = calls;
assert.deepEqual(JSON.parse((await invoke(middleware, '/stations')).body), body);
assert.equal(calls, count);
});
+5 -4
View File
@@ -1036,12 +1036,13 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping
// preserves the mission mapping and globe-rotation arguments.
// S2 GhostMaps extends the generated layer enums; both feature contracts remain.
// The layer alias hint leaves out aliases the layer id already spells.
// The Utilities panel adds one set_panel_open target.
// Voice geometry adds the analyst area scopes and the osm-places layer.
assert.equal(block.length, 31211, 'serialized tool schema length drifted');
// The Utilities panel adds one set_panel_open target; Sea Marks and Ski
// Pistes extend the generated layer enums and alias hint; voice geometry
// adds the analyst area scopes and the osm-places layer.
assert.equal(block.length, 31318, 'serialized tool schema length drifted');
assert.equal(
crypto.createHash('sha256').update(block).digest('hex'),
'f37523ad0022f8320dc0c9550d70ed192f80a436cccde743b9dcb1efa2110ea1',
'f5b42f41c2f575838f979e88a1dbff615a77ccd85fdf8fcd0a0a47d02f7d3ddc',
'the first-run missions must ride EXISTING tools: no schema edit, no cache bust',
);
const instructions = fs.readFileSync(
+3 -1
View File
@@ -3304,5 +3304,7 @@
},
"listed the first {limit}": "عُرض أول {limit}",
"counted over the bounding box of {area}, not its outline": "عُدّ ضمن المستطيل المحيط ({area})، لا ضمن الحدود",
"Search cancelled": "أُلغي البحث"
"Search cancelled": "أُلغي البحث",
"Sea Marks": "علامات بحرية",
"Ski Pistes": "مسارات التزلج"
}
+3 -1
View File
@@ -3004,5 +3004,7 @@
},
"listed the first {limit}": "die ersten {limit} aufgelistet",
"counted over the bounding box of {area}, not its outline": "über das umgebende Rechteck gezählt ({area}), nicht über den Umriss",
"Search cancelled": "Suche abgebrochen"
"Search cancelled": "Suche abgebrochen",
"Sea Marks": "Seezeichen",
"Ski Pistes": "Skipisten"
}
+3 -1
View File
@@ -3079,5 +3079,7 @@
},
"listed the first {limit}": "se listaron los primeros {limit}",
"counted over the bounding box of {area}, not its outline": "contado en el rectángulo que lo rodea ({area}), no en su contorno",
"Search cancelled": "Búsqueda cancelada"
"Search cancelled": "Búsqueda cancelada",
"Sea Marks": "Señales marítimas",
"Ski Pistes": "Pistas de esquí"
}
+3 -1
View File
@@ -3079,5 +3079,7 @@
},
"listed the first {limit}": "les {limit} premiers sont listés",
"counted over the bounding box of {area}, not its outline": "compté sur le rectangle englobant ({area}), pas sur le contour",
"Search cancelled": "Recherche annulée"
"Search cancelled": "Recherche annulée",
"Sea Marks": "Balisage maritime",
"Ski Pistes": "Pistes de ski"
}
+3 -1
View File
@@ -3079,5 +3079,7 @@
},
"listed the first {limit}": "elencati i primi {limit}",
"counted over the bounding box of {area}, not its outline": "contato sul riquadro di delimitazione ({area}), non sul contorno",
"Search cancelled": "Ricerca annullata"
"Search cancelled": "Ricerca annullata",
"Sea Marks": "Segnalamenti marittimi",
"Ski Pistes": "Piste da sci"
}
+3 -1
View File
@@ -2929,5 +2929,7 @@
},
"listed the first {limit}": "最初の {limit} 件を表示",
"counted over the bounding box of {area}, not its outline": "外接矩形 ({area}) で数えた結果で、輪郭ではありません",
"Search cancelled": "検索をキャンセルしました"
"Search cancelled": "検索をキャンセルしました",
"Sea Marks": "航路標識",
"Ski Pistes": "スキーゲレンデ"
}
+3 -1
View File
@@ -3079,5 +3079,7 @@
},
"listed the first {limit}": "listados os primeiros {limit}",
"counted over the bounding box of {area}, not its outline": "contado na caixa delimitadora ({area}), não no contorno",
"Search cancelled": "Busca cancelada"
"Search cancelled": "Busca cancelada",
"Sea Marks": "Sinais marítimos",
"Ski Pistes": "Pistas de esqui"
}
+3 -1
View File
@@ -3154,5 +3154,7 @@
},
"listed the first {limit}": "показаны первые {limit}",
"counted over the bounding box of {area}, not its outline": "подсчитано по ограничивающему прямоугольнику ({area}), а не по контуру",
"Search cancelled": "Поиск отменён"
"Search cancelled": "Поиск отменён",
"Sea Marks": "Морские знаки",
"Ski Pistes": "Горнолыжные трассы"
}
+3 -1
View File
@@ -3154,5 +3154,7 @@
},
"listed the first {limit}": "показано перші {limit}",
"counted over the bounding box of {area}, not its outline": "підраховано за обмежувальним прямокутником ({area}), а не за контуром",
"Search cancelled": "Пошук скасовано"
"Search cancelled": "Пошук скасовано",
"Sea Marks": "Морські знаки",
"Ski Pistes": "Гірськолижні траси"
}
+3 -1
View File
@@ -2929,5 +2929,7 @@
},
"listed the first {limit}": "已列出前 {limit} 个",
"counted over the bounding box of {area}, not its outline": "按外接矩形({area})计数,而非其轮廓",
"Search cancelled": "搜索已取消"
"Search cancelled": "搜索已取消",
"Sea Marks": "航标",
"Ski Pistes": "滑雪道"
}
+3 -1
View File
@@ -2929,5 +2929,7 @@
},
"listed the first {limit}": "已列出前 {limit} 個",
"counted over the bounding box of {area}, not its outline": "按外接矩形({area})計數,而非其輪廓",
"Search cancelled": "搜索已取消"
"Search cancelled": "搜索已取消",
"Sea Marks": "航標",
"Ski Pistes": "滑雪道"
}
+73 -20
View File
@@ -28,6 +28,29 @@ function services() {
};
}
// The application's own scene services, as src/app/layers/militaryFlights.js
// composes them; only the surface samplers and registry are stubbed.
async function sceneServices() {
return {
picking: await import('../../data/pickRegistry.js'),
sprites: await import('../../data/spriteOrder.js'),
trails: await import('../../data/trailRenderer.js'),
aircraftPresentation: await import('../../data/tr3bRegistry.js'),
camera: await import('../../data/trackedCamera.js'),
labels: await import('../../data/detectionDraw.js'),
geoid: await import('../../data/geoid.js'),
focus: await import('../../data/focusDeemphasis.js'),
readout: await import('../../data/trackedReadout.js'),
context: await import('../../data/contextStore.js'),
render: await import('../../renderGovernor.js'),
recession: await import('../../data/aircraftRecession.js'),
groundFloor: await import('../../data/groundFloor.js'),
groundSnap: await import('../../data/groundSnap.js'),
meshFloor: { sampleMeshFloorCells: () => {} },
militaryRegistry: { registerMilitaryIcaos: () => {} },
};
}
test('military layer instances isolate policy and restoration state without requesting a source', async () => {
let requests = 0;
const source = {
@@ -122,26 +145,7 @@ test('a newly seen military contact appears at its delayed position, not ahead o
// the next fleet tick jumped it back.
const Cesium = await import('cesium');
const { RENDER_DELAY_SEC } = await import('./policy.js');
// The application's own scene services, as src/app/layers/militaryFlights.js
// composes them; only the surface samplers and registry are stubbed.
const supplied = {
picking: await import('../../data/pickRegistry.js'),
sprites: await import('../../data/spriteOrder.js'),
trails: await import('../../data/trailRenderer.js'),
aircraftPresentation: await import('../../data/tr3bRegistry.js'),
camera: await import('../../data/trackedCamera.js'),
labels: await import('../../data/detectionDraw.js'),
geoid: await import('../../data/geoid.js'),
focus: await import('../../data/focusDeemphasis.js'),
readout: await import('../../data/trackedReadout.js'),
context: await import('../../data/contextStore.js'),
render: await import('../../renderGovernor.js'),
recession: await import('../../data/aircraftRecession.js'),
groundFloor: await import('../../data/groundFloor.js'),
groundSnap: await import('../../data/groundSnap.js'),
meshFloor: { sampleMeshFloorCells: () => {} },
militaryRegistry: { registerMilitaryIcaos: () => {} },
};
const supplied = await sceneServices();
const fixLon = -97.6;
const fixLat = 30.3;
const speedMps = 200;
@@ -213,6 +217,55 @@ test('a newly seen military contact appears at its delayed position, not ahead o
);
});
test('a vanished military contact fades while position-less rows keep theirs', async () => {
// About a third of a live adsb.lol /v2/mil fetch is contacts without a
// position. Those rows marked every snapshot partial, so an aircraft that
// left the feed stayed at full opacity for five minutes.
const Cesium = await import('cesium');
const { readsbSnapshot } = await import('../../sources/live/aircraft.js');
const nowMs = Date.now();
const row = (hex, lon) => ({
hex,
lat: 30.3,
lon,
alt_baro: 20_000,
gs: 300,
track: 90,
seen_pos: 1,
});
const polls = [
[row('ae0001', -97.6), row('ae0002', -96.6), row('ae0003', -95.6)],
[row('ae0001', -97.5), { hex: 'ae0003' }],
];
// Every poll also lists a contact that never has a position.
for (const poll of polls) poll.push({ hex: 'ae0009', type: 'mode_s' });
const layer = createMilitaryFlightLayer({
services: await sceneServices(),
source: {
label: 'Fixture aircraft',
getSnapshot: async () =>
readsbSnapshot(
{ ac: polls.shift() },
{ observedAtMs: nowMs, now: nowMs },
),
},
});
const viewer = { camera: { positionCartographic: null }, scene: {} };
layer.testing._setTrackedMilitaryRefreshStateForTest({
icao24: 'seed00',
entity: null,
meta: { rawLat: 0, rawLon: 0, onGround: false },
billboard: { show: false, position: Cesium.Cartesian3.fromDegrees(0, 0) },
billboardCollection: { show: true, add: (options) => ({ ...options }) },
viewer,
tracked: false,
});
await layer.update(viewer);
await layer.update(viewer);
assert.equal(layer.findByQuery('ae0002')?.stale, true, 'vanished: fading');
assert.equal(layer.findByQuery('ae0003')?.stale, false, 'still listed');
});
test('voice context reads a track that rounds to north as 0°, not 360°', () => {
const stubs = services();
stubs.aircraftPresentation = { tr3bTypeLabel: (id, type) => type };
+5 -2
View File
@@ -187,9 +187,12 @@ export class MilitaryFlightRecords {
fixEpochMs: aircraft.positionTimeMs ?? observedAtMs,
};
}
absence(id, { complete, likelyLanded }) {
absence(id, { complete, retainIds, likelyLanded }) {
// Partial admissions and position-less rows do not prove absence, but
// retention remains bounded; a likely-landed position-less contact
// still takes the landed limit.
if (
!complete &&
(!complete || (retainIds?.has(id) && !likelyLanded)) &&
Date.now() - (this.data.get(id)?.observedReceiptMs ?? 0) < 300000
)
return 'retain';
+24
View File
@@ -99,6 +99,30 @@ test('military records keep bounded partial retention and release metadata only
);
});
test('complete military snapshots age out vanished aircraft and retain position-less ones unless landed', () => {
const records = store();
for (const id of ['aaa001', 'aaa002', 'aaa003'])
records.receive(observation({ id, reference: id }), context());
// aaa001 and aaa003 are still listed without a position; aaa002 vanished.
const absent = (id, likelyLanded = false) =>
records.absence(id, {
complete: true,
retainIds: new Set(['aaa001', 'aaa003']),
likelyLanded,
});
assert.deepEqual(
[absent('aaa002'), absent('aaa002'), absent('aaa002')],
['stale', 'stale', 'remove'],
);
assert.equal(absent('aaa001'), 'retain');
assert.equal(records.missingPolls.has('aaa001'), false);
// A landed contact that lost its position takes the landed fast-cull.
assert.equal(absent('aaa003', true), 'remove');
// Position-less retention stays bounded like partial admission.
records.data.get('aaa001').observedReceiptMs = Date.now() - 300001;
assert.equal(absent('aaa001'), 'stale');
});
test('military emergency status follows the latest poll instead of sticking', () => {
const records = store();
const first = records.receive(
+1
View File
@@ -259,6 +259,7 @@ export function createMilitarySnapshotRenderer({
if (currentIcaos.has(icao24)) continue;
const absence = records.absence(icao24, {
complete: snapshot.complete,
retainIds: snapshot.retainIds,
likelyLanded: queries._likelyLanded(icao24),
});
if (absence === 'retain') continue;
+10 -7
View File
@@ -3,13 +3,16 @@
*
* The credential panel has its own stricter gate (`admitKeySetupRequest` in
* keySetupCore.mjs): loopback socket + local Host + exact Origin + JSON
* Content-Type. The four endpoints here — `/api/realtime/token` (mints an
* OpenAI Realtime token = spends money), `/api/openai/hud-summary` (OpenAI
* spend), `/api/google/nearby-places` (Google spend), and
* `/api/realtime/debug-log` (appends to a local JSONL) — were previously
* ungated, so a hostile web page open in the same browser could reach them
* cross-site: a `fetch` carries a foreign `Origin`, and an `<img>`/navigation
* carries NO `Origin` at all but does carry `Sec-Fetch-Site: cross-site`.
* Content-Type. The endpoints behind this gate — `/api/realtime/token` (mints
* an OpenAI Realtime token = spends money), `/api/openai/hud-summary` (OpenAI
* spend), `/api/google/nearby-places` (Google spend),
* `/api/realtime/debug-log` (appends to a local JSONL) and the quota-bearing
* routes SECURITY.md lists with them — were previously ungated, so a hostile
* web page open in the same browser could reach them cross-site: a `fetch`
* carries a foreign `Origin`, and an `<img>`/navigation carries NO `Origin`
* at all but does carry `Sec-Fetch-Site: cross-site`. The keyless map routes
* (`/api/terrain/heights`, `/api/overpass`) reuse this core through
* `admitSameSiteKeyless` with the forwarding-header refusal switched off.
*
* This module is the pure core that refuses those shapes while keeping the
* documented `HOST=0.0.0.0` LAN opt-in and non-browser loopback tools (the two
+12 -1
View File
@@ -40,9 +40,11 @@ export function normalizeOpenSkyAircraft(row) {
};
}
const readsbId = (row) => cleanText(row?.hex).toLowerCase();
/** Normalize readsb observations. The caller supplies the source snapshot epoch. */
export function normalizeReadsbAircraft(row, snapshotTimeMs) {
const id = cleanText(row?.hex).toLowerCase();
const id = readsbId(row);
const latitude = finite(row?.lat),
longitude = finite(row?.lon);
if (!id || !coordinates(latitude, longitude)) return null;
@@ -129,6 +131,14 @@ export function openSkySnapshot(
};
}
/**
* readsb omits lat/lon for a contact heard without a current position. Both
* must be absent; a half or unreadable position still marks the row malformed.
*/
function readsbUnpositionedId(row) {
return row?.lat == null && row?.lon == null ? readsbId(row) : '';
}
export function readsbSnapshot(
payload,
{
@@ -143,6 +153,7 @@ export function readsbSnapshot(
payload?.ac,
(row) => normalizeReadsbAircraft(row, observedAtMs),
'adsb.lol',
readsbUnpositionedId,
);
const ageMs = observedAtMs == null ? null : Math.max(0, now - observedAtMs);
return {
+30 -8
View File
@@ -102,13 +102,34 @@ test('position-less OpenSky rows are seen, not missing; malformed rows stay inco
assert.throws(() => openSkySnapshot({ states: [unpositioned] }), {
code: 'malformed',
});
// Military readsb admission keeps its existing semantics.
const military = readsbSnapshot(
{ ac: [{ hex: 'abc123', lat: 30, lon: -97 }, { hex: 'def456' }] },
{ observedAtMs: null },
);
assert.equal(military.complete, false);
assert.equal(military.retainIds.size, 0);
});
test('position-less readsb rows are seen, not missing; malformed rows stay incomplete', () => {
// readsb omits lat/lon for a contact heard without a current position; a
// live adsb.lol /v2/mil fetch had 82 such rows of 239.
const positioned = { hex: 'ae638d', lat: 20.3, lon: -158.5, seen_pos: 0.2 };
const heard = { hex: 'ADFAC3', type: 'mode_s', rr_lat: 19.9, seen: 24.2 };
const lastPosition = {
hex: '3b752d',
lastPosition: { lat: 45.3, lon: 5.98, seen_pos: 61.56 },
};
const read = (ac) => readsbSnapshot({ ac }, { observedAtMs: null });
const snapshot = read([positioned, heard, lastPosition]);
assert.equal(snapshot.records.length, 1);
assert.equal(snapshot.complete, true);
assert.deepEqual([...snapshot.retainIds], ['adfac3', '3b752d']);
assert.equal(snapshot.rejectedCount, 2);
// No usable id, only half a position, an unreadable one, or no row at all.
for (const row of [
{ ...heard, hex: ' ' },
{ ...heard, lat: 30 },
{ ...heard, lon: -97 },
{ ...heard, lat: 'x', lon: 'y' },
null,
])
assert.equal(read([positioned, heard, row]).complete, false);
// A feed with no positions at all still cannot prove absence.
assert.throws(() => read([heard]), { code: 'malformed' });
});
test('readsb units and position/contact ages are normalized once against the source epoch', () => {
@@ -346,7 +367,8 @@ test('classification identities include positionless aircraft without admitting
snapshot.records.map((record) => record.id),
['def456'],
);
assert.equal(snapshot.complete, false);
assert.equal(snapshot.complete, true);
assert.deepEqual([...snapshot.retainIds], ['abc123']);
});
test('identity lookup validates its response and honors body-parse cancellation', async () => {
+85 -13
View File
@@ -1,11 +1,27 @@
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
/**
* Wall-clock budget for EACH observed frame, regardless of FPS. Both the settle
* frame (measured from the forced moveEnd) and the reveal frame (measured from
* the settle frame that hid the contact) must land inside it, so neither can be
* repaired by the one-second idle rotation refresh before it is read. The
* refresh clock starts at the fleet tick (preRender) of the frame the budget is
* measured from, so the budget must leave room for that frame's own render
* time: half the refresh interval tolerates frames up to ~500 ms, and a ~300 ms
* software-rendered frame (headless SwiftShader) fits with margin.
*/
export const ARRIVAL_OBSERVATION_MS = 500;
/** A measured rotation error at or above this fails the arrival check. */
export const ARRIVAL_MAX_ERROR_DEG = 10;
/** Attempts allowed when every earlier one timed out without a wrong angle. */
export const ARRIVAL_MAX_ATTEMPTS = 2;
/**
* Observe the first completed moveEnd frame and the first subsequent reveal.
* The deadline starts when moveEnd is forced, before its rotation baseline, so
* waiting for either frame cannot let the one-second idle refresh mask a bug.
* `read` snapshots visibility and rotation error against the current position.
* The settle deadline starts when moveEnd is forced, before its rotation
* baseline; the reveal deadline restarts at the settle frame, which is where
* the contact is hidden. `read` snapshots visibility and rotation error
* against the current position.
*/
export function observeArrivalRotation({
scene,
@@ -18,6 +34,7 @@ export function observeArrivalRotation({
}) {
return new Promise((resolve, reject) => {
const startedAt = now();
let phaseStartedAt = startedAt;
let moveEnd = null;
let moveEndMs = null;
let revealFrames = 0;
@@ -34,24 +51,42 @@ export function observeArrivalRotation({
cleanup();
resolve(result);
};
const expired = () => ({
// A timeout keeps every angle it measured: the settle angle before a
// reveal timeout, and the angle on a late frame itself (a late frame is
// read first; the idle refresh can only repair an angle, so a wrong one
// seen late was wrong on time too). `arrivalAngleViolated` then makes the
// timeout final instead of retryable.
const expired = (late = null) => ({
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
elapsedMs: now() - startedAt,
moveEndMs,
...(moveEnd
? { dMoveEnd: moveEnd.rotationError }
: late
? { dMoveEnd: late.rotationError }
: {}),
...(moveEnd && late?.shown ? { dReveal: late.rotationError } : {}),
flipFrame: -1,
});
const armDeadline = () => {
clearTimer(timer);
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
};
remove = scene.postRender.addEventListener(() => {
if (done) return;
try {
const elapsedMs = now() - startedAt;
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
finish(expired());
const at = now();
const elapsedMs = at - startedAt;
const sample = read();
if (at - phaseStartedAt >= ARRIVAL_OBSERVATION_MS) {
finish(expired(sample));
return;
}
const sample = read();
if (moveEnd === null) {
moveEnd = sample;
moveEndMs = elapsedMs;
phaseStartedAt = at;
armDeadline();
startReveal();
} else {
revealFrames++;
@@ -76,7 +111,7 @@ export function observeArrivalRotation({
reject(error);
}
});
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
armDeadline();
try {
startMoveEnd();
scene.requestRender();
@@ -87,14 +122,51 @@ export function observeArrivalRotation({
});
}
/**
* Whether an observation measured a rotation error over the threshold on any
* frame it captured, including the settle frame of a reveal that then timed
* out. Such an observation is a demonstrated failure, never inconclusive.
*/
export function arrivalAngleViolated(observation) {
const limit = (ARRIVAL_MAX_ERROR_DEG * Math.PI) / 180;
return [observation?.dMoveEnd, observation?.dReveal].some(
(error) => Number.isFinite(error) && error >= limit,
);
}
/**
* Observe arrival rotation, retrying only an inconclusive timeout. A wall-clock
* timeout means the frame landed where the idle refresh could already have
* repaired it, so it earns another attempt; an observation that measured a
* wrong angle (or succeeded) is final, so a later clean attempt can never
* replace it. `prepare` runs before every attempt; the result carries
* `attempts`.
*/
export async function observeArrivalRotationWithRetry({
prepare = async () => {},
observe = observeArrivalRotation,
maxAttempts = ARRIVAL_MAX_ATTEMPTS,
...options
}) {
let observed;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await prepare();
observed = { ...(await observe(options)), attempts: attempt };
if (!observed.error || arrivalAngleViolated(observed)) break;
}
return observed;
}
/** Test the captured frames without retrying a bad first observation. */
export function arrivalRotationPassed(observation) {
return (
!!observation &&
!observation.error &&
observation.shown === true &&
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
(observation.dMoveEnd * 180) / Math.PI < 10 &&
(observation.dReveal * 180) / Math.PI < 10
observation.moveEndMs < ARRIVAL_OBSERVATION_MS &&
observation.revealMs < ARRIVAL_OBSERVATION_MS &&
Number.isFinite(observation.dMoveEnd) &&
Number.isFinite(observation.dReveal) &&
!arrivalAngleViolated(observation)
);
}
+231 -8
View File
@@ -1,19 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as arrivalModule from './trackArrivalObservation.mjs';
import {
ARRIVAL_OBSERVATION_MS,
arrivalAngleViolated,
arrivalRotationPassed,
observeArrivalRotation,
observeArrivalRotationWithRetry,
} from './trackArrivalObservation.mjs';
function fixture() {
function fixture(start = observeArrivalRotation) {
let time = 0;
let state = { shown: true, rotationError: 0 };
const listeners = new Set();
const timers = new Map();
let renders = 0;
let moveEnds = 0;
const observation = observeArrivalRotation({
const observation = start({
scene: {
postRender: {
addEventListener(listener) {
@@ -110,7 +114,8 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
f.frame(8, { shown: true, rotationError: 0 });
f.frame(480);
f.expire(500);
// The reveal budget is measured from the 8 ms settle frame.
f.expire(508);
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(arrivalRotationPassed(result), false);
@@ -123,20 +128,46 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
test('a late visible frame fails even if the timeout callback is delayed', async () => {
const f = fixture();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(501, { shown: true, rotationError: 0 });
f.frame(509, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.elapsedMs, 501);
assert.equal(result.elapsedMs, 509);
assert.equal(arrivalRotationPassed(result), false);
});
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
test('a slow software renderer passes when each frame stays inside its own budget', async () => {
// Headless SwiftShader frames run ~300 ms: settle lands at 300 ms and the
// reveal at 600 ms from moveEnd. Both stay inside the 1 s idle refresh.
const f = fixture();
f.frame(400, { shown: true, rotationError: 0 });
f.frame(300, { shown: true, rotationError: 0 });
f.expire(500); // the settle frame re-armed the deadline to 800 ms
f.frame(600, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.moveEndMs, 400);
assert.equal(result.moveEndMs, 300);
assert.equal(result.revealMs, 300);
assert.equal(result.elapsedMs, 600);
assert.equal(result.flipFrame, 1);
assert.equal(arrivalRotationPassed(result), true);
assert.equal(f.resources.timers, 0);
});
test('a settle frame later than its budget fails as a moveEnd timeout', async () => {
const f = fixture();
f.frame(500, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.moveEndMs, null);
assert.equal(arrivalRotationPassed(result), false);
});
test('a reveal later than its own budget fails even after a fast settle frame', async () => {
const f = fixture();
f.frame(400, { shown: true, rotationError: 0 });
f.frame(901, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.moveEndMs, 400);
assert.equal(result.elapsedMs, 901);
assert.equal(arrivalRotationPassed(result), false);
});
@@ -149,3 +180,195 @@ test('missing moveEnd frames fail without waiting for a render callback', async
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
// Let a retry's next attempt register its postRender listener.
const settle = () => new Promise((resolve) => setImmediate(resolve));
function retryFixture(extra = {}) {
const counts = { prepares: 0 };
const f = fixture((options) =>
observeArrivalRotationWithRetry({
...options,
...extra,
async prepare() {
counts.prepares++;
},
}),
);
f.counts = counts;
return f;
}
test('a wrong settle angle followed by a reveal timeout is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
// 0.5 rad ≈ 29°: a real threshold violation on the settle frame.
f.frame(8, { shown: true, rotationError: 0.5 });
f.expire(508); // the reveal never comes inside its budget
await settle();
// An otherwise clean second attempt, which must never be started.
f.frame(516, { shown: true, rotationError: 0 });
f.frame(524, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.dMoveEnd, 0.5, 'the measured settle angle is kept');
assert.equal(arrivalAngleViolated(result), true);
assert.equal(result.attempts, 1);
assert.equal(f.counts.prepares, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
test('an inconclusive reveal timeout after a correct settle earns one more attempt', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
f.expire(508);
await settle();
f.frame(516, { shown: true, rotationError: 0 });
f.frame(596, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.attempts, 2);
assert.equal(f.counts.prepares, 2);
assert.equal(f.resources.moveEnds, 2);
assert.equal(arrivalRotationPassed(result), true);
});
test('timeouts stop after the attempt limit and still fail', async () => {
const f = retryFixture();
await settle();
f.expire(500);
await settle();
f.expire(1000);
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.attempts, 2);
assert.equal(arrivalRotationPassed(result), false);
assert.equal(f.resources.listeners, 0);
assert.equal(f.resources.timers, 0);
});
test('a missing or non-finite angle never passes', () => {
const clean = {
shown: true,
moveEndMs: 8,
revealMs: 80,
dMoveEnd: 0,
dReveal: 0,
};
assert.equal(arrivalRotationPassed(clean), true);
for (const field of ['dMoveEnd', 'dReveal']) {
assert.equal(arrivalRotationPassed({ ...clean, [field]: NaN }), false);
assert.equal(
arrivalRotationPassed({ ...clean, [field]: undefined }),
false,
);
}
});
test('a wrong angle on a late reveal frame is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
f.frame(100, { shown: true, rotationError: 0 });
// Shown at 600 ms (500 ms after settle) still wearing the tampered nose.
f.frame(600, { shown: true, rotationError: 1.5 });
await settle();
f.frame(608, { shown: true, rotationError: 0 });
f.frame(688, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /reveal.*500 ms/);
assert.equal(result.dReveal, 1.5);
assert.equal(result.attempts, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('a wrong angle on a late settle frame is final, not retried into a pass', async () => {
const f = retryFixture();
await settle();
f.frame(926, { shown: true, rotationError: 1.5 });
await settle();
f.frame(934, { shown: true, rotationError: 0 });
f.frame(1014, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.match(result.error, /moveEnd.*500 ms/);
assert.equal(result.dMoveEnd, 1.5);
assert.equal(result.attempts, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('a late hidden reveal frame records no angle and stays retryable', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
// Still hidden, still tampered: not a measured reveal angle.
f.frame(600, { shown: false });
const first = f.observation;
await settle();
f.frame(608, { shown: true, rotationError: 0 });
f.frame(688, { shown: true, rotationError: 0 });
const result = await first;
assert.equal(result.attempts, 2);
assert.equal(result.dReveal, 0);
assert.equal(arrivalRotationPassed(result), true);
});
test('a late frame with correct angles stays an inconclusive, retryable timeout', async () => {
const f = retryFixture();
await settle();
f.frame(926, { shown: true, rotationError: 0 });
await settle();
f.frame(934, { shown: true, rotationError: 0 });
f.frame(1014, { shown: true, rotationError: 0 });
const result = await f.observation;
assert.equal(result.attempts, 2);
assert.equal(arrivalRotationPassed(result), true);
});
test('a wrong first reveal on time is final: no retry', async () => {
const f = retryFixture();
await settle();
f.frame(8, { shown: true, rotationError: 0 });
f.frame(88, { shown: true, rotationError: 1.5 });
const result = await f.observation;
assert.equal(result.error, undefined);
assert.equal(result.attempts, 1);
assert.equal(f.resources.moveEnds, 1);
assert.equal(arrivalRotationPassed(result), false);
});
test('maxAttempts bounds inconclusive retries', async () => {
const f = retryFixture({ maxAttempts: 3 });
for (let attempt = 1; attempt <= 3; attempt++) {
await settle();
f.expire(attempt * 500);
}
const result = await f.observation;
assert.equal(result.attempts, 3);
assert.equal(f.counts.prepares, 3);
assert.equal(arrivalRotationPassed(result), false);
});
test('the 10° limit is inclusive', () => {
const limit = Math.PI / 18;
assert.equal(arrivalAngleViolated({ dMoveEnd: limit }), true);
assert.equal(arrivalAngleViolated({ dReveal: limit * 0.999 }), false);
});
test('every name the track harness imports from this module is exported', () => {
const harness = readFileSync(
new URL('../../scripts/track-regression.mjs', import.meta.url),
'utf8',
);
const names = [
...harness.matchAll(
/(?:import|const)\s*\{([^}]+)\}\s*(?:from\s*|=\s*await import\()[^;]*trackArrivalObservation\.mjs/g,
),
].flatMap((match) => match[1].split(',').map((name) => name.trim()));
assert.ok(names.includes('arrivalRotationPassed'));
assert.ok(names.includes('observeArrivalRotationWithRetry'));
for (const name of names)
assert.equal(typeof arrivalModule[name], 'function', name);
});
+2
View File
@@ -44,6 +44,8 @@ export const LAYER_NAME_KEYS = Object.freeze({
'air-quality': msg('Air quality'),
directions: msg('Directions'),
'alpr-cameras': msg('ALPR Cameras'),
'sea-marks': msg('Sea Marks'),
'ski-pistes': msg('Ski Pistes'),
});
/** The layer panel's own English names, where they differ from the module's. */
+2
View File
@@ -64,6 +64,8 @@ const PANEL_GROUPS = [
'local-datacenters',
'telegeography-submarine-cables',
'local-dams',
'sea-marks',
'ski-pistes',
],
},
{
+1 -1
View File
@@ -55,7 +55,7 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
// Voice geometry adds resolve_area, find_imagery, osm_query, the analyst
// area scopes and the osm-places layer; natural-region candidates and the
// tightened area tool wording follow review.
'1e07573229f7901148a8e2d30d5111dc359ed35156aa7878afac8f0d940badd8',
'd0c5ad733670972713a54cb10676c42b7c6e78834bfff52b473c0e8a45e742c3',
);
});
+12
View File
@@ -71,6 +71,18 @@ const layers = [
context: false,
noQuery: 'published third-party intel placemarks; not analyst records',
},
{
id: 'sea-marks',
aliases: ['seamarks', 'nautical charts', 'buoys', 'openseamap'],
context: false,
noQuery: 'raster tile overlay; no countable records',
},
{
id: 'ski-pistes',
aliases: ['ski slopes', 'ski runs', 'snow map', 'opensnowmap'],
context: false,
noQuery: 'raster tile overlay; no countable records',
},
{
id: 'flights',
track: true,