Merge remote-tracking branch 'upstream/main' into core-rb2

# Conflicts:
#	CHANGELOG.md
#	build/vite.js
#	scripts/package-boundaries.json
#	server/standalone/vite.config.js
#	src/app/scene.js
#	src/layers/vessels/queries.js
#	src/standalone/application.js
#	src/tooling/viteBuild.test.mjs
#	src/ui/cockpitInstruments.js
#	src/ui/layerPanel.js
#	src/ui/panelPositionControls.js
This commit is contained in:
Meir Michanie committed 2026-10-10 23:15:54 +02:00
commit 71f7a58169
475 files changed
+67826 -4965

No files matched your search

+50
View File
@@ -75,6 +75,56 @@ export function createControls({ state: layerState, services, parts, source }) {
);
},
/**
* Return the exact retained aircraft cohorts behind the current Contacts
* panel snapshot. Voice reads this immutable copy instead of re-scanning
* live layers, so panel and spoken counts share one evaluation timestamp.
*/
getAircraftQuerySnapshot() {
if (!layerState.enabled || !layerState.subject || !layerState.results)
return null;
// Build every public count from this exact results object before
// returning. A refresh may replace `layerState.results` as soon as the
// caller awaits, so consumers must not make a second read later and
// accidentally combine two evaluation timestamps.
const panelSnapshot = parts.model.buildAwarenessContextSnapshot(
layerState.results,
parts.model.navigationState(),
{
subjectPresent: !layerState.subjectMissing,
},
);
const cohorts = {};
for (const cohort of layerState.results.cohorts || []) {
if (!['flights', 'military'].includes(cohort?.id)) continue;
cohorts[cohort.id] = {
count: cohort.summary?.count ?? null,
complete:
Number.isFinite(cohort.summary?.count) &&
cohort.summary?.complete !== false,
truncated:
!Number.isFinite(cohort.summary?.count) ||
cohort.summary?.truncated === true,
// These records carry structured values such as Cartesian positions.
// A shallow spread would let a voice consumer mutate the retained
// Contacts cohort through the advertised read-only snapshot.
items: structuredClone(cohort.summary?.navigationNearest || []),
source: cohort.source || null,
provenance: cohort.provenance
? structuredClone(cohort.provenance)
: null,
reason: cohort.summary?.reason || null,
};
}
return {
subject: structuredClone(layerState.results.subject),
evaluatedAt: layerState.results.evaluatedAt,
radiusM: layerState.results.radiusM,
cohorts,
contactsWindow: parts.model.contactsWindowFromSnapshot(panelSnapshot),
};
},
/**
* Release Contact-owned camera tracking without discarding the selected
* subject. Reset-to-globe uses this route so the normal Context FOCUS action
+11
View File
@@ -144,6 +144,9 @@ export function createModel({ state: layerState, services, parts, source }) {
function contactsWindowFromSnapshot(snapshot) {
if (!snapshot?.subject) return null;
const aircraftCohorts = (snapshot.cohorts || []).filter((cohort) =>
['flights', 'military'].includes(cohort?.id),
);
const countFor = (cohortId) => {
const cohort = Array.isArray(snapshot.cohorts)
? snapshot.cohorts.find((item) => item?.id === cohortId)
@@ -164,6 +167,12 @@ export function createModel({ state: layerState, services, parts, source }) {
flights,
military,
vessels: countFor('ais-live-vessels'),
complete:
aircraftCohorts.length === 2 &&
aircraftCohorts.every(
(cohort) =>
Number.isFinite(cohort.count) && cohort.complete !== false,
),
};
}
@@ -190,6 +199,8 @@ export function createModel({ state: layerState, services, parts, source }) {
coverage: cohort.coverage || null,
relationship: cohort.summary.relationship,
count: cohort.summary.count,
complete: cohort.summary.complete !== false,
truncated: cohort.summary.truncated === true,
reason: cohort.summary.reason,
nearest: cohort.summary.nearest.slice(),
})),
+3
View File
@@ -25,6 +25,7 @@ export function createNavigation({
{
displayLimit = AWARENESS_MAX_EXAMPLES,
navigationLimit = AWARENESS_MAX_NAVIGATION_EXAMPLES,
complete = true,
} = {},
) {
const summary = summarizeAwarenessCohort(items, {
@@ -34,6 +35,8 @@ export function createNavigation({
if (summary.count === null) return summary;
return {
...summary,
complete: Boolean(complete),
truncated: !complete,
nearest: summary.nearest.slice(0, displayLimit),
navigationNearest: summary.nearest,
};
+10 -1
View File
@@ -6,6 +6,15 @@ import {
import { t } from '../../i18n/index.js';
import { AWARENESS_PAGE_SIZE, AWARENESS_PAGE_ROTATE_MS } from './policy.js';
/** Format a cohort total without presenting a retained cap as exact. */
export function formatAwarenessCount(summary = {}) {
if (!Number.isFinite(summary.count)) return '?';
const count = summary.count.toLocaleString('en-US');
return summary.complete === false || summary.truncated === true
? `At least ${count}`
: count;
}
export function createPanel({ state: layerState, services, parts, source }) {
function ensurePanel() {
if (layerState.panel) return layerState.panel;
@@ -62,7 +71,7 @@ export function createPanel({ state: layerState, services, parts, source }) {
function rowHtml(cohort) {
const summary = cohort.summary;
const count = summary.count === null ? '?' : String(summary.count);
const count = formatAwarenessCount(summary);
const page = layerState.cohortPages.get(cohort.id) || 0;
const nearest = summary.nearest
.slice(page, page + AWARENESS_PAGE_SIZE)
+2 -2
View File
@@ -38,8 +38,8 @@ export const AWARENESS_MAX_EXAMPLES = 10;
// so these caps sit far above any realistic in-range population rather than at
// the display limit. They are still FINITE: an unbounded materialization would
// let a pathological feed sort an eleven-thousand-contact array on every
// refresh. Truthfulness does not depend on them — `summarizeAwarenessCohort`
// derives `count` from the full in-range set before any slice.
// refresh. Callers sample beyond this cap and carry explicit completeness, so
// a saturated cohort is a lower bound rather than a silently exact count.
export const AWARENESS_QUERY_LIMIT = 20000;
+30 -9
View File
@@ -35,7 +35,7 @@ export function createQueries({ state: layerState, services, parts, source }) {
// setInstallationStatus is only ever called with
// loading/zoom-in/ready/stale/empty/unavailable.
// - ais-live-vessels is what the predicate below is FOR. Its enable() and
// update() both resolve as soon as the first /api/ais-live poll answers,
// update() both resolve as soon as the first /api/vessels poll answers,
// so the lifecycle settles to `enabled` — but until the server-side socket
// delivers a position, firstConnectPhase is 'loading' and getStats()
// reports loading: true, lastUpdate: null, count 0, and an UNDEFINED
@@ -53,7 +53,12 @@ export function createQueries({ state: layerState, services, parts, source }) {
neverAnswered ||
['unavailable', 'zoom-in'].includes(stats.status) ||
Boolean(stats.error && stats.count === 0);
return { available: !unavailable, stale: Boolean(stats.stale), stats };
return {
available: !unavailable,
enabled,
stale: Boolean(stats.stale),
stats,
};
}
function collectSourceStates() {
@@ -147,8 +152,10 @@ export function createQueries({ state: layerState, services, parts, source }) {
* @param {object} [options]
* @param {number} [options.radiusM=AWARENESS_RADIUS_M] Window radius.
* @param {object|null} [options.subject=null] Contact at the centre, excluded.
* @returns {{flights: Array, military: Array, aircraft: number}|null} Cohorts
* plus the combined aircraft count, or null without a position.
* @returns {{flights: Array, military: Array, aircraft: number,
* completeByLayer: {flights: boolean, military: boolean}}|null} Cohorts,
* their lower-bound completeness, and the combined aircraft count, or
* null without a position.
*/
function collectAircraftProximityWindow(
@@ -156,21 +163,35 @@ export function createQueries({ state: layerState, services, parts, source }) {
{ radiusM = AWARENESS_RADIUS_M, subject = null } = {},
) {
if (!position) return null;
const flights = flightsLayer
.getNearby(position, radiusM, AWARENESS_QUERY_LIMIT, {
// Ask for one usable row beyond the retained limit so a capped cohort is
// reported as a lower bound instead of an exact count. The selected
// subject can consume one slot before exclusion, hence the extra two.
const sampleLimit = AWARENESS_QUERY_LIMIT + 2;
const flightSample = flightsLayer
.getNearby(position, radiusM, sampleLimit, {
includeHidden: true,
})
.filter(
(item) => !subject || !isSame(subject, item, 'flights', 'icao24'),
);
const military = militaryFlightsLayer
.getNearby(position, radiusM, AWARENESS_QUERY_LIMIT, {
const militarySample = militaryFlightsLayer
.getNearby(position, radiusM, sampleLimit, {
includeHidden: true,
})
.filter(
(item) => !subject || !isSame(subject, item, 'military', 'icao24'),
);
return { flights, military, aircraft: flights.length + military.length };
const flights = flightSample.slice(0, AWARENESS_QUERY_LIMIT);
const military = militarySample.slice(0, AWARENESS_QUERY_LIMIT);
return {
flights,
military,
aircraft: flights.length + military.length,
completeByLayer: {
flights: flightSample.length <= AWARENESS_QUERY_LIMIT,
military: militarySample.length <= AWARENESS_QUERY_LIMIT,
},
};
}
return {
sourceState,
+33
View File
@@ -6,6 +6,39 @@ import { createSubject } from './subject.js';
import { createControls } from '../installations/controls.js';
import { createModel } from '../installations/model.js';
import { summarizeAwarenessCohort } from '../../data/militaryAwarenessEngine.js';
import { AWARENESS_QUERY_LIMIT } from './policy.js';
test('aircraft window marks a capped layer as a lower bound', () => {
const rows = Array.from({ length: AWARENESS_QUERY_LIMIT + 1 }, (_, index) => ({
icao24: `f${index}`,
distance: index,
}));
let requested = null;
const services = {
flights: {
getNearby: (_position, _radius, limit) => {
requested = limit;
return rows.slice(0, limit);
},
},
military: { getNearby: () => [] },
};
const parts = {
navigation: {
summarizeAwarenessCohortForNavigation: (items, source) =>
summarizeAwarenessCohort(items, source),
},
};
const queries = createQueries({ state: {}, services, parts });
const window = queries.collectAircraftProximityWindow({ center: true });
assert.equal(requested, AWARENESS_QUERY_LIMIT + 2);
assert.equal(window.flights.length, AWARENESS_QUERY_LIMIT);
assert.equal(window.aircraft, AWARENESS_QUERY_LIMIT);
assert.deepEqual(window.completeByLayer, {
flights: false,
military: true,
});
});
test('Contacts excludes square corners and measures from the current moving subject', () => {
const records = [
+46 -2
View File
@@ -53,11 +53,23 @@ export function createSubject({ state: layerState, services, parts, source }) {
const militaryState = sourceStates.military;
const vesselsState = sourceStates['ais-live-vessels'];
const installationsState = sourceStates['military-installations'];
const evaluatedAt = Date.now();
const retainedProvenance = (id, label, sourceState, fallbackSource) => {
const retainedSource = sourceState.stats.source || fallbackSource;
return {
id,
name: label,
enabled: sourceState.enabled,
source: retainedSource,
stats: { ...sourceState.stats, source: retainedSource },
};
};
// Same engine the voice analyst calls — see collectAircraftProximityWindow.
const { flights, military } = parts.queries.collectAircraftProximityWindow(
const aircraftWindow = parts.queries.collectAircraftProximityWindow(
position,
{ subject },
);
const { flights, military, completeByLayer } = aircraftWindow;
const vessels = aisLiveVesselsLayer
.getNearby(position, AWARENESS_RADIUS_M, AWARENESS_QUERY_LIMIT)
.filter(
@@ -77,31 +89,57 @@ export function createSubject({ state: layerState, services, parts, source }) {
);
return {
subject,
evaluatedAt: Date.now(),
evaluatedAt,
radiusM: AWARENESS_RADIUS_M,
cohorts: [
{
id: 'flights',
label: t('layers.awareness.cohort.flights'),
source: flightsState.stats.source || SOURCE_LABEL.flights,
provenance: retainedProvenance(
'flights',
'Flights',
flightsState,
SOURCE_LABEL.flights,
),
summary: parts.navigation.summarizeAwarenessCohortForNavigation(
flights,
flightsState,
{
navigationLimit: AWARENESS_QUERY_LIMIT,
complete: completeByLayer.flights,
},
),
},
{
id: 'military',
label: t('layers.awareness.cohort.military'),
source: militaryState.stats.source || SOURCE_LABEL.military,
provenance: retainedProvenance(
'military',
'Military flights',
militaryState,
SOURCE_LABEL.military,
),
summary: parts.navigation.summarizeAwarenessCohortForNavigation(
military,
militaryState,
{
navigationLimit: AWARENESS_QUERY_LIMIT,
complete: completeByLayer.military,
},
),
},
{
id: 'ais-live-vessels',
label: t('layers.awareness.cohort.vessels'),
source: vesselsState.stats.source || SOURCE_LABEL['ais-live-vessels'],
provenance: retainedProvenance(
'ais-live-vessels',
'AIS vessels',
vesselsState,
SOURCE_LABEL['ais-live-vessels'],
),
summary: parts.navigation.summarizeAwarenessCohortForNavigation(
vessels,
vesselsState,
@@ -113,6 +151,12 @@ export function createSubject({ state: layerState, services, parts, source }) {
source:
installationsState.stats.source ||
SOURCE_LABEL['military-installations'],
provenance: retainedProvenance(
'military-installations',
'Mapped installations',
installationsState,
SOURCE_LABEL['military-installations'],
),
coverage:
localizeCoverageLabel(installationsState.stats.coverageLabel) ||
t('layers.awareness.coverage.viewport'),