mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge remote-tracking branch 'upstream/main' into fix/363-geocode-proxy
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
5083a74413
542 files changed
+96845
-3034
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { HUD_LAYOUTS } from '../hudLayoutPolicy.js';
|
||||
|
||||
// Canonical action arguments. Descriptive wording is supplied separately.
|
||||
const schemas = [
|
||||
{
|
||||
@@ -137,7 +139,9 @@ const schemas = [
|
||||
'local-dams',
|
||||
'telegeography-submarine-cables',
|
||||
'local-firms',
|
||||
'fire-perimeters',
|
||||
'alpr-cameras',
|
||||
'local-adsb',
|
||||
],
|
||||
},
|
||||
enabled: {
|
||||
@@ -169,6 +173,7 @@ const schemas = [
|
||||
'local-dams',
|
||||
'telegeography-submarine-cables',
|
||||
'local-firms',
|
||||
'fire-perimeters',
|
||||
'alpr-cameras',
|
||||
],
|
||||
},
|
||||
@@ -310,11 +315,26 @@ const schemas = [
|
||||
},
|
||||
layout: {
|
||||
type: 'string',
|
||||
enum: ['tactical', 'operator', 'minimal'],
|
||||
enum: [...HUD_LAYOUTS],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'set_cyber_sonar',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
rings: { type: 'integer', minimum: 3, maximum: 12 },
|
||||
rangePct: { type: 'integer', minimum: 60, maximum: 120 },
|
||||
intensityPct: { type: 'integer', minimum: 0, maximum: 100 },
|
||||
opacityPct: { type: 'integer', minimum: 35, maximum: 100 },
|
||||
sectorDeg: { type: 'integer', minimum: 8, maximum: 60 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'set_detection',
|
||||
parameters: {
|
||||
@@ -771,6 +791,10 @@ const schemas = [
|
||||
'ais-live-vessels',
|
||||
'local-firms',
|
||||
'earthquakes',
|
||||
'satellites',
|
||||
'local-datacenters',
|
||||
'local-dams',
|
||||
'fire-perimeters',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -860,6 +884,21 @@ const schemas = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'next_satellite_pass',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['target'],
|
||||
properties: {
|
||||
target: { type: 'string', minLength: 1, maxLength: 120 },
|
||||
latitude: { type: 'number', minimum: -90, maximum: 90 },
|
||||
longitude: { type: 'number', minimum: -180, maximum: 180 },
|
||||
minElevationDeg: { type: 'number', minimum: 5, maximum: 60 },
|
||||
visibleOnly: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function freeze(value) {
|
||||
|
||||
@@ -15,13 +15,21 @@ const stable = (value) =>
|
||||
)
|
||||
: value;
|
||||
|
||||
test('the complete Realtime tool payload retains its pre-extraction contract and wording', () => {
|
||||
test('the complete Realtime tool payload pins the additive analyst, satellite, Local ADS-B and Cyber release', () => {
|
||||
const digest = createHash('sha256')
|
||||
.update(JSON.stringify(stable(GEV_REALTIME_TOOLS)))
|
||||
.update(
|
||||
JSON.stringify(
|
||||
stable(
|
||||
GEV_REALTIME_TOOLS.filter((tool) => tool.name !== 'set_cyber_sonar'),
|
||||
),
|
||||
),
|
||||
)
|
||||
.digest('hex');
|
||||
assert.equal(
|
||||
digest,
|
||||
'956381c3456d3644ed7c9cda72910dc68a34d9191e0b3e414ee200c348245214',
|
||||
// Re-derived for the additive `local-adsb` set_layer_visibility value and
|
||||
// the Cyber HUD layout; the separate sonar tool is excluded above.
|
||||
'590d537d93e132ac64ac5e211ad5bb9d7d1b1f22e2dd963dda5465fab4510a3b',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -76,3 +84,40 @@ test('metadata cannot add tools, fields, types or enum values', () => {
|
||||
])
|
||||
assert.throws(() => createActionTools(descriptions), TypeError);
|
||||
});
|
||||
|
||||
test('all legacy action arguments are byte-identical after removing the deliberate additions', () => {
|
||||
const legacy = structuredClone(GEV_ACTION_SCHEMAS).filter(
|
||||
(tool) => !['next_satellite_pass', 'set_cyber_sonar'].includes(tool.name),
|
||||
);
|
||||
const layers = legacy.find((tool) => tool.name === 'analyst_query').parameters
|
||||
.properties.layers.items;
|
||||
layers.enum = layers.enum.filter(
|
||||
(key) =>
|
||||
![
|
||||
'satellites',
|
||||
'local-datacenters',
|
||||
'local-dams',
|
||||
'fire-perimeters',
|
||||
].includes(key),
|
||||
);
|
||||
// Local ADS-B is an additive set_layer_visibility enum value.
|
||||
const visibility = legacy.find((tool) => tool.name === 'set_layer_visibility')
|
||||
.parameters.properties.layerId;
|
||||
visibility.enum = visibility.enum.filter(
|
||||
(key) => !['local-adsb', 'fire-perimeters'].includes(key),
|
||||
);
|
||||
for (const tool of legacy) {
|
||||
for (const value of Object.values(tool.parameters.properties)) {
|
||||
if (value.enum)
|
||||
value.enum = value.enum.filter((key) => key !== 'fire-perimeters');
|
||||
}
|
||||
}
|
||||
// Independently derived by executing trusted c9f9896 actionSchemas in the restricted container.
|
||||
const hud = legacy.find((tool) => tool.name === 'set_hud').parameters
|
||||
.properties.layout;
|
||||
hud.enum = hud.enum.filter((layout) => layout !== 'cyber');
|
||||
assert.equal(
|
||||
createHash('sha256').update(JSON.stringify(legacy)).digest('hex'),
|
||||
'820fff21658f6907e1010b2b79c5431a77f4e34afd2277d62d8de46c368b6f8c',
|
||||
);
|
||||
});
|
||||
+137
-1
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
layerSnapshot,
|
||||
layerSnapshots,
|
||||
feedProvenanceEnvelope,
|
||||
} from '../data/layerSnapshot.js';
|
||||
import { readLayerLifecycleSummary } from './layerSummary.js';
|
||||
export { readLayerLifecycleSummary } from './layerSummary.js';
|
||||
import { defaultGeospatial } from '../search/defaults.js';
|
||||
@@ -212,6 +217,9 @@ const LAYER_ALIASES = new Map([
|
||||
['submarine cables', 'telegeography-submarine-cables'],
|
||||
['cables', 'telegeography-submarine-cables'],
|
||||
['telegeography', 'telegeography-submarine-cables'],
|
||||
['fire perimeters', 'fire-perimeters'],
|
||||
['perimeters', 'fire-perimeters'],
|
||||
['wildfire perimeters', 'fire-perimeters'],
|
||||
['firms', 'local-firms'],
|
||||
['fires', 'local-firms'],
|
||||
['active fires', 'local-firms'],
|
||||
@@ -221,6 +229,12 @@ const LAYER_ALIASES = new Map([
|
||||
['license plate readers', 'alpr-cameras'],
|
||||
['license plate cameras', 'alpr-cameras'],
|
||||
['plate readers', 'alpr-cameras'],
|
||||
['local-adsb', 'local-adsb'],
|
||||
['local adsb', 'local-adsb'],
|
||||
['local ads-b', 'local-adsb'],
|
||||
['my receiver', 'local-adsb'],
|
||||
['my antenna', 'local-adsb'],
|
||||
['my sdr', 'local-adsb'],
|
||||
]);
|
||||
|
||||
const CITY_ALIASES = new Map([
|
||||
@@ -964,6 +978,10 @@ export function createGevActionRunner({
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'next_satellite_pass') {
|
||||
return nextSatellitePass(viewer, dataManager, args);
|
||||
}
|
||||
|
||||
if (name === 'next_iss_pass') {
|
||||
return nextIssPass(viewer, dataManager, args);
|
||||
}
|
||||
@@ -1040,6 +1058,17 @@ export function createGevActionRunner({
|
||||
return { ...out, hud: styleManager.getControlState().hud };
|
||||
}
|
||||
|
||||
if (name === 'set_cyber_sonar') {
|
||||
if (typeof styleManager?.setCyberSonar !== 'function') {
|
||||
return {
|
||||
ok: false,
|
||||
action: 'set_cyber_sonar',
|
||||
error: 'Cyber sonar controls are unavailable.',
|
||||
};
|
||||
}
|
||||
return { action: 'set_cyber_sonar', ...styleManager.setCyberSonar(args) };
|
||||
}
|
||||
|
||||
if (name === 'set_detection') {
|
||||
const result = styleManager.setDetection({
|
||||
enabled: typeof args.enabled === 'boolean' ? args.enabled : undefined,
|
||||
@@ -2655,6 +2684,64 @@ function nextIssPass(viewer, dataManager, args) {
|
||||
durationMin: Math.max(1, Math.round((pass.setMs - pass.riseMs) / 60000)),
|
||||
peakElevationDeg: Math.round(pass.maxElevDeg),
|
||||
riseDirection: compassDir(pass.riseAzDeg),
|
||||
visible: typeof pass.visible === 'boolean' ? pass.visible : null,
|
||||
visibilityNote:
|
||||
'Geometric illumination estimate only; weather, brightness and orbital-element age affect actual visibility.',
|
||||
setIso: new Date(pass.setMs).toISOString(),
|
||||
peakIso: new Date(pass.maxElevMs).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function nextSatellitePass(viewer, dataManager, args) {
|
||||
const layer = dataManager?.layers?.get('satellites')?.module;
|
||||
const identity = layer?.resolveSatelliteForPass?.(args.target) || {
|
||||
status: 'not-found',
|
||||
};
|
||||
if (identity.status !== 'ok')
|
||||
return {
|
||||
ok: false,
|
||||
action: 'next_satellite_pass',
|
||||
...identity,
|
||||
error:
|
||||
identity.status === 'ambiguous'
|
||||
? 'Several loaded satellites match. Choose a NORAD ID from candidates.'
|
||||
: 'No loaded satellite matches. Enable satellites and use an exact name or NORAD ID.',
|
||||
};
|
||||
// Reuse the legacy location fallback and result formatting, substituting only
|
||||
// this explicitly resolved catalog identity and the optional visibility filter.
|
||||
const adapter = {
|
||||
layers: new Map([
|
||||
[
|
||||
'satellites',
|
||||
{
|
||||
module: {
|
||||
getNextIssPass: (options) =>
|
||||
layer.getNextSatellitePass(identity.noradId, {
|
||||
...options,
|
||||
requireVisible: args.visibleOnly === true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
const result = nextIssPass(viewer, adapter, args);
|
||||
if (result.error) {
|
||||
result.error = result.error.replace(
|
||||
/ISS/g,
|
||||
identity.name || String(identity.noradId),
|
||||
);
|
||||
if (args.visibleOnly === true)
|
||||
result.error +=
|
||||
' Search required estimated illumination under a dark sky.';
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
action: 'next_satellite_pass',
|
||||
noradId: identity.noradId,
|
||||
name: identity.name,
|
||||
visibleOnly: args.visibleOnly === true,
|
||||
horizonHours: 24,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3025,7 +3112,13 @@ function getCurrentViewState(
|
||||
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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4152,6 +4245,29 @@ function analystProviders(
|
||||
? mod.getAnalystRecords(requestedLimit) || []
|
||||
: mod.getAnalystRecords() || [];
|
||||
},
|
||||
getLayerSnapshot(layerKey) {
|
||||
const row = dataManager.getAll?.().find((layer) => layer.id === layerKey);
|
||||
if (row) return layerSnapshot(row);
|
||||
const module = dataManager.layers?.get(layerKey)?.module;
|
||||
return layerSnapshot({
|
||||
id: layerKey,
|
||||
enabled: dataManager.isEnabled?.(layerKey),
|
||||
stats: module?.getStats?.() || {},
|
||||
});
|
||||
},
|
||||
getRecordCoverage(layerKey, rows) {
|
||||
if (!['satellites', 'local-datacenters', 'local-dams'].includes(layerKey))
|
||||
return null;
|
||||
const module = dataManager.layers.get(layerKey)?.module;
|
||||
const loaded = module?.getStats?.().count;
|
||||
return {
|
||||
basis: 'bounded-loaded-records',
|
||||
recordsExamined: rows.length,
|
||||
loadedCount: Number.isFinite(loaded) ? loaded : null,
|
||||
sourceTruncated: Number.isFinite(loaded) ? loaded > rows.length : null,
|
||||
note: 'Counts and ranks apply only to these examined loaded records, not all satellites or infrastructure; distance is ground great-circle distance.',
|
||||
};
|
||||
},
|
||||
resolveRegionRing,
|
||||
/**
|
||||
* The active Contacts subject, when there is one — the centre the operator
|
||||
@@ -4207,6 +4323,7 @@ async function runAnalystQuery(
|
||||
return {
|
||||
ok: false,
|
||||
action: 'analyst_query',
|
||||
...(result.code ? { code: result.code } : {}),
|
||||
error: result.error,
|
||||
coverage: result.coverage,
|
||||
};
|
||||
@@ -4243,6 +4360,12 @@ async function runAnalystQuery(
|
||||
'distanceKm',
|
||||
'confidence',
|
||||
'place',
|
||||
'noradId',
|
||||
'satelliteClass',
|
||||
'group',
|
||||
'river',
|
||||
'output',
|
||||
'capacity',
|
||||
]) {
|
||||
if (r[k] !== null && r[k] !== undefined) compact[k] = r[k];
|
||||
}
|
||||
@@ -4282,7 +4405,18 @@ async function runAnalystQuery(
|
||||
args,
|
||||
result,
|
||||
);
|
||||
if (entityWindow) return entityWindow;
|
||||
if (entityWindow) {
|
||||
const provenance = feedProvenanceEnvelope(
|
||||
layerSnapshots(dataManager.getAll?.() || []).filter(
|
||||
(layer) => layer.enabled && ['flights', 'military'].includes(layer.id),
|
||||
),
|
||||
);
|
||||
return {
|
||||
...entityWindow,
|
||||
feedProvenance: provenance,
|
||||
feedState: provenance.overall,
|
||||
};
|
||||
}
|
||||
|
||||
const contactsWindow = activeContactsWindow(dataManager);
|
||||
const aircraftQueried = (result.coverage?.layersQueried || []).some(
|
||||
@@ -4321,6 +4455,8 @@ async function runAnalystQuery(
|
||||
items,
|
||||
summary: result.summary,
|
||||
coverage: result.coverage,
|
||||
feedProvenance: result.coverage?.feedProvenance || null,
|
||||
feedState: result.coverage?.feedProvenance?.overall || null,
|
||||
// The panel's own numbers, carried so the answer can match what the
|
||||
// operator is looking at regardless of how the model reads the note.
|
||||
// Flattened alongside the object so the count and its subject cannot be
|
||||
|
||||
@@ -3165,6 +3165,30 @@ test('ALPR common names toggle only the registered camera layer through the norm
|
||||
}
|
||||
});
|
||||
|
||||
test('Local ADS-B common names toggle only the receiver layer through the normal voice action', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const viewer = { clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||
camera: { moveEnd: { addEventListener() {} } } };
|
||||
const calls = [];
|
||||
let enabled = false;
|
||||
const dataManager = {
|
||||
layers: new Map([['local-adsb', { module: {} }]]),
|
||||
getAll: () => [{ id: 'local-adsb', name: 'Local ADS-B' }],
|
||||
isEnabled: () => enabled,
|
||||
setEnabled: async (id, value) => { calls.push([id, value]); enabled = value; return true; },
|
||||
};
|
||||
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager });
|
||||
for (const alias of ['local-adsb', 'local ADS-B', 'Local ADSB', 'my receiver', 'my antenna', 'my SDR']) {
|
||||
for (const value of [true, false]) {
|
||||
const result = await runner('set_layer_visibility', { layerId: alias, enabled: value });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.layerId, 'local-adsb');
|
||||
assert.deepEqual(calls.at(-1), ['local-adsb', value]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('ISS voice lookup uses the registered satellite instance', async () => {
|
||||
const calls = [];
|
||||
const viewer = {
|
||||
@@ -3182,3 +3206,91 @@ test('ISS voice lookup uses the registered satellite instance', async () => {
|
||||
assert.deepEqual(calls, [{ latDeg: 30, lonDeg: -97, minElevDeg: 15 }]);
|
||||
assert.match(result.error, /No ISS pass above 15/);
|
||||
});
|
||||
|
||||
test('analyst_query and get_current_view_state carry stale feed provenance', async () => {
|
||||
globalThis.window = globalThis.window || { clearTimeout, setTimeout, requestIdleCallback: null };
|
||||
const now = Date.now();
|
||||
const flights = {
|
||||
id: 'flights',
|
||||
source: 'OpenSky Network',
|
||||
getStats: () => ({
|
||||
source: 'OpenSky Network',
|
||||
stale: true,
|
||||
count: 12,
|
||||
lastUpdate: now - 240_000,
|
||||
}),
|
||||
getAnalystRecords: () => ([
|
||||
{ id: 'SWA1', icao24: 'aaa001', lat: 30.27, lon: -97.74, altitudeM: 11000, onGround: false },
|
||||
]),
|
||||
};
|
||||
const viewer = {
|
||||
clock: { onTick: { addEventListener: () => () => {} } },
|
||||
scene: { canvas: { addEventListener() {}, removeEventListener() {} } },
|
||||
camera: {
|
||||
moveEnd: { addEventListener() {} },
|
||||
positionWC: Cesium.Cartesian3.fromDegrees(-97.7, 30.2, 1000),
|
||||
positionCartographic: { height: 300_000, latitude: 0.52, longitude: -1.71 },
|
||||
},
|
||||
};
|
||||
const dataManager = {
|
||||
layers: new Map([['flights', { module: flights }]]),
|
||||
isEnabled: (id) => id === 'flights',
|
||||
getAll: () => [{
|
||||
id: 'flights',
|
||||
name: 'Live Flights',
|
||||
enabled: true,
|
||||
source: 'OpenSky Network',
|
||||
stats: flights.getStats(),
|
||||
}],
|
||||
};
|
||||
const runner = createGevActionRunner({
|
||||
viewer,
|
||||
styleManager: {
|
||||
activeStyle: 'normal',
|
||||
getContextModeState: () => ({ mode: null, active: false }),
|
||||
getCockpitState: () => ({ active: false }),
|
||||
getControlState: () => null,
|
||||
},
|
||||
dataManager,
|
||||
});
|
||||
const view = await runner('get_current_view_state');
|
||||
assert.equal(view.layers[0].feedState, 'stale');
|
||||
assert.equal(view.feedProvenance.overall, 'stale');
|
||||
assert.match(view.feedProvenance.note, /STALE/);
|
||||
|
||||
const query = await runner('analyst_query', {
|
||||
layers: ['flights'],
|
||||
scope: { kind: 'view' },
|
||||
limit: 5,
|
||||
});
|
||||
assert.equal(query.ok, true);
|
||||
assert.equal(query.feedState, 'stale');
|
||||
assert.equal(query.feedProvenance.overall, 'stale');
|
||||
assert.equal(query.coverage.layersQueried[0].feedState, 'stale');
|
||||
assert.match(query.feedProvenance.note, /do not describe this as live/i);
|
||||
});
|
||||
|
||||
|
||||
test('general satellite pass resolves once, refuses ambiguity, and preserves ISS call semantics', async () => {
|
||||
const calls = [];
|
||||
const pass = { riseMs: Date.now() + 60000, setMs: Date.now() + 360000, maxElevMs: Date.now() + 180000, maxElevDeg: 30, riseAzDeg: 90, visible: false };
|
||||
const layer = {
|
||||
resolveSatelliteForPass(target) { return target === 'starlink' ? { status: 'ambiguous', candidates: [{ noradId: 1 }, { noradId: 2 }] } : { status: 'ok', noradId: 25544, name: 'ISS' }; },
|
||||
getNextSatellitePass(id, options) { calls.push({ id, ...options }); return { status: 'ok', pass }; },
|
||||
getNextIssPass(options) { calls.push(options); return { status: 'ok', pass }; },
|
||||
};
|
||||
const viewer = { clock: { onTick: { addEventListener: () => () => {} } }, scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, camera: { moveEnd: { addEventListener() {} } } };
|
||||
const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager: { layers: new Map([['satellites', { module: layer }]]) } });
|
||||
const ambiguous = await runner('next_satellite_pass', { target: 'starlink' });
|
||||
assert.equal(ambiguous.status, 'ambiguous');
|
||||
assert.equal(calls.length, 0);
|
||||
const args = { latitude: 30, longitude: -97, minElevationDeg: 15 };
|
||||
const general = await runner('next_satellite_pass', { target: '25544', visibleOnly: true, ...args });
|
||||
assert.equal(general.action, 'next_satellite_pass');
|
||||
assert.deepEqual(calls[0], { id: 25544, latDeg: 30, lonDeg: -97, minElevDeg: 15, requireVisible: true });
|
||||
const iss = await runner('next_iss_pass', args);
|
||||
assert.deepEqual(calls[1], { latDeg: 30, lonDeg: -97, minElevDeg: 15 });
|
||||
for (const key of ['observer', 'riseIso', 'minutesFromNow', 'durationMin', 'peakElevationDeg', 'riseDirection']) assert.deepEqual(iss[key], general[key]);
|
||||
assert.equal(iss.visible, false);
|
||||
assert.equal(iss.action, 'next_iss_pass');
|
||||
});
|
||||
Reference in new issue
Block a user