Merge branch 'main' into feat/transit-icon-heading

This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-10 12:24:46 -08:00
commit 05daaf267a
191 files changed
+16103 -740

No files matched your search

+4
View File
@@ -112,6 +112,8 @@
"src/data/terrainHeightsProxy.test.mjs",
"src/data/tomtomTiles.js",
"src/data/tomtomTiles.test.mjs",
"src/data/tileBudget.js",
"src/data/tileBudget.test.mjs",
"src/data/firmsCsv.js",
"src/data/firmsCsv.test.mjs",
"src/tooling/environmentProviders.test.mjs",
@@ -1075,6 +1077,7 @@
"src/layers/traffic/roadModes.test.mjs",
"scripts/qa-traffic-motion.mjs",
"src/layers/traffic/motion.test.mjs",
"src/layers/traffic/lineRebuild.test.mjs",
"scripts/qa-journey-recorder.mjs",
"scripts/qa-alpr-journey.mjs",
"scripts/qa-context-arrivals.mjs",
@@ -1291,6 +1294,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
+83 -6
View File
@@ -73,6 +73,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -242,6 +243,11 @@
"modules": ["src/data/adsbLolFallback.js", "src/sources/adsbFields.js"],
"external": []
},
"planespotters-source": {
"exports": ["./sources/planespotters"],
"modules": ["src/data/planespottersPhoto.js"],
"external": []
},
"place-providers": {
"runtime": "node",
"exports": ["./server/providers/places"],
@@ -319,6 +325,7 @@
"src/sources/httpBody.js",
"src/i18n/marker.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"server/providers/common/same-site.js",
"src/localRequestGate.mjs"
],
@@ -326,7 +333,7 @@
},
"traffic-source": {
"exports": ["./sources/traffic"],
"modules": ["src/data/tomtomTiles.js"],
"modules": ["src/data/tomtomTiles.js", "src/data/tileBudget.js"],
"external": []
},
"firms-csv": {
@@ -650,6 +657,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -698,6 +706,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -739,6 +748,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -782,6 +792,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -809,6 +820,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -839,6 +851,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -876,6 +889,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -945,6 +959,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -972,6 +987,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1174,6 +1190,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1366,6 +1383,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1394,7 +1412,8 @@
"src/sources/live/contract.js",
"src/sources/live/index.js",
"src/sources/live/standalone.js",
"src/sources/live/vessels.js"
"src/sources/live/vessels.js",
"src/data/planespottersPhoto.js"
],
"external": []
},
@@ -1440,6 +1459,7 @@
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
"src/data/trailContinuity.js",
"src/layers/flights/records.js",
"src/layers/flights/recordPolicy.js",
"src/layers/flights/snapshotRenderer.js",
@@ -1452,6 +1472,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1461,7 +1482,8 @@
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/time/worldClock.js",
"src/time/julian.js"
"src/time/julian.js",
"src/data/planespottersPhoto.js"
],
"external": ["cesium"]
},
@@ -1502,6 +1524,7 @@
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js",
"src/data/trailContinuity.js",
"src/layers/military/records.js",
"src/layers/military/recordPolicy.js",
"src/layers/military/snapshotRenderer.js",
@@ -1513,6 +1536,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1520,6 +1544,7 @@
"src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js",
"src/time/worldClock.js",
"src/time/julian.js"
],
@@ -1590,6 +1615,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1627,6 +1653,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1668,6 +1695,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1708,6 +1736,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1756,6 +1785,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1787,6 +1817,7 @@
"src/layers/installations/viewport.js",
"src/data/inputOwnership.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
@@ -1807,6 +1838,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1854,6 +1886,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1875,6 +1908,7 @@
"src/data/cameraSensitivity.js",
"src/data/flowMatch.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/data/trafficBounds.js",
"src/geoDistance.js",
"src/data/trafficFlowStyle.js",
@@ -1916,6 +1950,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -1953,6 +1988,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2010,6 +2046,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2081,6 +2118,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2105,8 +2143,10 @@
"src/layers/alpr/overlay.js",
"src/layers/alpr/source.js",
"src/data/inputOwnership.js",
"src/data/displayText.js",
"src/layers/alpr/records.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/layers/alpr/tileRecords.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
@@ -2120,6 +2160,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2170,6 +2211,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2200,6 +2242,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2264,6 +2307,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2342,6 +2386,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -2578,6 +2623,7 @@
"src/data/telegeographySubmarineCables.js",
"src/data/terrainHeights.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/data/tr3bRegistry.js",
"src/data/trackedCamera.js",
"src/data/trackedModelRegime.js",
@@ -2589,6 +2635,7 @@
"src/data/trafficPresetStyle.js",
"src/data/trafficQueue.js",
"src/data/trailRenderer.js",
"src/data/trailContinuity.js",
"src/data/vesselLabels.js",
"src/data/vesselTypes.js",
"src/firstRunExperience.js",
@@ -3011,6 +3058,7 @@
"src/voice/chatPreferences.js",
"src/voice/panelLayout.js",
"src/voice/voiceMiniPreview.js",
"src/data/displayText.js",
"src/voice/gevActions.js",
"src/agent/worldPacket.js",
"src/voice/speech.js",
@@ -3290,6 +3338,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -3433,7 +3482,8 @@
"src/layers/satellites/classes.js",
"src/layers/satellites/sensors.js",
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js"
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js"
],
"external": [
"hls.js",
@@ -3574,6 +3624,7 @@
"src/data/scenePick.js",
"src/data/spriteOrder.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/data/tr3bRegistry.js",
"src/data/trackedCamera.js",
"src/data/trackedModelRegime.js",
@@ -3584,6 +3635,7 @@
"src/data/trafficPresetStyle.js",
"src/data/trafficQueue.js",
"src/data/trailRenderer.js",
"src/data/trailContinuity.js",
"src/data/vesselLabels.js",
"src/data/vesselTypes.js",
"src/keylessGeocoder.js",
@@ -3857,6 +3909,7 @@
"src/search/coordinateParser.js",
"src/search/presetGeocoder.js",
"src/app/localGeojsonServices.js",
"src/data/displayText.js",
"src/layers/alpr/records.js",
"src/layers/earthquakes/records.js",
"src/app/layers/perimeters.js",
@@ -4025,6 +4078,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4092,7 +4146,8 @@
"src/layers/satellites/classes.js",
"src/layers/satellites/sensors.js",
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js"
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js"
],
"external": [
"hls.js",
@@ -4171,6 +4226,7 @@
"src/sources/phaseTiming.js",
"src/sources/militaryTileGeometry.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/data/installationNames.js",
"src/annotations/outlineRungs.js",
"src/sources/openFreeMapOutlines.js",
@@ -4207,6 +4263,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4240,6 +4297,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4294,6 +4352,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4332,6 +4391,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4438,6 +4498,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4511,6 +4572,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4531,9 +4593,11 @@
"exports": ["./layers/alpr/source"],
"modules": [
"src/layers/alpr/policy.js",
"src/data/displayText.js",
"src/layers/alpr/records.js",
"src/layers/alpr/source.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/layers/alpr/tileRecords.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
@@ -4572,6 +4636,7 @@
"src/data/militaryInstallationData.js",
"src/data/installationNames.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/sources/capability.js",
"src/sources/httpBody.js",
"src/i18n/marker.js",
@@ -4614,6 +4679,7 @@
"exports": ["./layers/traffic/source"],
"modules": [
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/layers/traffic/flowDecode.js",
"src/layers/traffic/flowSource.js",
"src/layers/traffic/footprint.js",
@@ -4799,6 +4865,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4854,6 +4921,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4917,6 +4985,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -4941,6 +5010,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5011,6 +5081,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5060,6 +5131,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5188,6 +5260,7 @@
"src/sources/vectorTiles.js",
"src/sources/phaseTiming.js",
"src/data/tomtomTiles.js",
"src/data/tileBudget.js",
"src/sources/capability.js",
"src/data/militaryInstallationData.js",
"src/layers/launches/source.js",
@@ -5226,6 +5299,7 @@
"src/layers/submarineCables/bundledSource.js",
"src/layers/alpr/source.js",
"src/layers/alpr/policy.js",
"src/data/displayText.js",
"src/layers/alpr/records.js",
"src/layers/alpr/tileRecords.js",
"src/sources/infrastructureData.js",
@@ -5233,7 +5307,8 @@
"src/layers/satellites/classes.js",
"src/layers/satellites/sensors.js",
"src/data/ucsSatellites.js",
"src/data/imagingSensors.js"
"src/data/imagingSensors.js",
"src/data/planespottersPhoto.js"
],
"external": ["@mapbox/vector-tile", "pbf"]
},
@@ -5264,6 +5339,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
@@ -5310,6 +5386,7 @@
"src/i18n/locales/de.json",
"src/i18n/locales/es.json",
"src/i18n/locales/fr.json",
"src/i18n/locales/he.json",
"src/i18n/locales/it.json",
"src/i18n/locales/ja.json",
"src/i18n/locales/pt.json",
+3
View File
@@ -4,6 +4,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const shotsDir = path.join(repoRoot, 'qa-shots', 'cockpit-utility');
@@ -29,6 +30,7 @@ const browser = await puppeteer.launch({
],
});
const page = await browser.newPage();
await stubPlanespotters(page);
const failures = [];
const consoleErrors = [];
const localHttpErrors = [];
@@ -825,6 +827,7 @@ try {
// a legitimate retained snapshot cannot satisfy or invalidate a FIRST-fetch
// assertion when camera motion supersedes the manager's initial update.
const readinessPage = await browser.newPage();
await stubPlanespotters(readinessPage);
let deferredInstallationReadiness;
try {
await readinessPage.setViewport({ width: 1440, height: 900 });
+2
View File
@@ -28,6 +28,7 @@
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
import { createScreencast, moveCamera } from './qa-journey-recorder.mjs';
const argv = process.argv.slice(2);
@@ -77,6 +78,7 @@ const browser = await puppeteer.launch({
async function openPage({ aircraft = false } = {}) {
const page = await browser.newPage();
await stubPlanespotters(page);
await page.setViewport({ width: 1440, height: 900 });
page.on('console', (message) => {
if (message.type() === 'error') result.consoleErrors.push(message.text());
+2
View File
@@ -27,6 +27,7 @@
*/
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -223,6 +224,7 @@ async function main() {
const errors = [];
try {
const page = await browser.newPage();
await stubPlanespotters(page);
await page.setViewport({ width: 1440, height: 900 });
page.on('pageerror', (e) => errors.push(e.message));
const { lon, lat } = CENTER;
+2
View File
@@ -21,6 +21,7 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
const argv = process.argv.slice(2);
const getOpt = (name, fallback = null) => {
@@ -651,6 +652,7 @@ async function main() {
],
});
const page = await browser.newPage();
await stubPlanespotters(page);
await page.setViewport(VIEWPORT);
const consoleMessages = [];
page.on('console', (message) => {
+2
View File
@@ -99,6 +99,7 @@
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
// ---------------------------------------------------------------------------
// Args (same shape as track-regression.mjs)
@@ -466,6 +467,7 @@ async function main() {
const failedResponses = [];
try {
const page = await browser.newPage();
await stubPlanespotters(page);
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
+12 -12
View File
@@ -239,11 +239,11 @@ function satisfiesEngines(version, range) {
* re-resolve the interpreter from PATH and hand the gate a different Node than
* the one the check believes it selected.
* @param {string} version e.g. "v24.19.0"
* @returns {boolean} true only for a parseable Node 24.
* @returns {boolean} true for a parseable supported Node 24 or 26.
*/
function isCalibratedAllocationRuntime(version) {
const [maj] = String(version || '').trim().replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10));
return maj === 24;
return maj === 24 || maj === 26;
}
/**
@@ -546,7 +546,7 @@ const check = (spec) => { CHECKS.push(spec); };
// ─── A · REPO GATES ───────────────────────────────────────────────────────
check({
id: 'A1', group: 'A', desc: 'Node runtime satisfies package.json engines (allocation budgets are pinned to Node 24)',
id: 'A1', group: 'A', desc: 'Node runtime satisfies package.json engines (allocation budgets cover supported Node majors)',
run: async () => {
const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'));
const range = pkg.engines?.node || '(unset)';
@@ -557,7 +557,7 @@ check({
if (satisfied) return pass(`node ${process.versions.node} satisfies "${range}"`);
// An off-range runtime is an environment problem, not a product defect —
// but it silently disables the allocation gate, so say so loudly. A3 then
// runs that gate under a discovered Node 24.
// runs that gate under a discovered calibrated Node 24 fallback.
return env.node24
? skip(`node ${process.versions.node} is OUTSIDE "${range}"; A3 runs the allocation gate under ${env.node24.label}, but prefer running the whole L9 pass on Node 24`, 'ENV')
: fail(`node ${process.versions.node} is OUTSIDE "${range}" and no Node 24 runtime was found — the allocation gate cannot run at all`);
@@ -579,13 +579,13 @@ check({
});
check({
id: 'A3', group: 'A', desc: 'Allocation microbenchmarks actually EXECUTE (they silently skip off Node 24)',
id: 'A3', group: 'A', desc: 'Allocation microbenchmarks actually EXECUTE on supported Node majors',
run: async () => {
// npm test prints "[unit] SKIPPED n allocation microbenchmarks" and still
// exits 0 when the runtime is not Node 24. A green suite is therefore NOT
// proof the gate ran. Force it, under Node 24 when one is discoverable.
// exits 0 when the runtime is outside the calibrated supported majors. A green
// suite is therefore NOT proof the gate ran. Force it under a calibrated runtime.
//
// ALWAYS invoke the Node 24 BINARY directly — never `npm test`. npm
// ALWAYS invoke the calibrated Node binary directly — never `npm test`. npm
// re-resolves the interpreter from PATH and can land back on the system
// Node even when THIS process is already 24: running the whole matrix under
// `mise exec [email protected] --`, the npm shell-out still re-execed system
@@ -597,10 +597,10 @@ check({
const v = await sh(bin, ['--version'], { timeoutMs: 60000 });
const version = (v.out || '').trim();
if (!isCalibratedAllocationRuntime(version)) {
// run-unit-tests.mjs refuses to measure calibrated budgets off Node 24,
// run-unit-tests.mjs refuses to measure calibrated budgets off supported Node majors,
// so a wrong binary means the gate never ran. That is this check failing
// to establish its own claim — not evidence about the product.
return crash(`the runtime selected for the allocation gate (${label}) reports ${version || 'no parseable version'}, not Node 24 — the gate would refuse or silently skip, so this check verified nothing`);
return crash(`the runtime selected for the allocation gate (${label}) reports ${version || 'no parseable version'}, not Node 24 or 26 — the gate would refuse or silently skip, so this check verified nothing`);
}
const r = await sh(bin, [resolve(REPO_ROOT, 'scripts/run-unit-tests.mjs')], {
timeoutMs: 600000, env: { GEV_REQUIRE_ALLOCATION_GATE: '1' },
@@ -611,9 +611,9 @@ check({
};
const [maj] = process.versions.node.split('.').map(Number);
if (maj === 24) return runGate(process.execPath, 'this runtime');
if (maj === 24 || maj === 26) return runGate(process.execPath, 'this runtime');
if (env.node24) return runGate(env.node24.bin, env.node24.label);
return skip(`no Node 24 runtime found (running ${process.versions.node}); the gate SKIPS silently — install Node 24 and re-run`, 'OWNER-RUN');
return skip(`no calibrated Node 24/26 runtime found (running ${process.versions.node}); the gate SKIPS silently — install a supported Node runtime and re-run`, 'OWNER-RUN');
},
});
+78
View File
@@ -0,0 +1,78 @@
/**
* Keep browser QA off third-party aircraft photo services.
*
* Tracking a civil flight asks Planespotters for the aircraft's photo. QA
* runs track synthetic hexes, so a real request only adds a third-party
* dependency and, when the service refuses a headless browser, a console
* error that fails the "no console errors" checks. Every photo lookup is
* answered with no photos instead, without leaving the browser.
*/
/** Origin of the aircraft photo lookup. */
export const PLANESPOTTERS_ORIGIN = 'https://api.planespotters.net';
/** The answer every stubbed lookup gets: no photos for this aircraft. */
export const PLANESPOTTERS_EMPTY_BODY = JSON.stringify({ photos: [] });
/** Whether a URL is a photo lookup the stub answers. */
export function isPlanespottersRequest(url) {
try {
return new URL(String(url)).origin === PLANESPOTTERS_ORIGIN;
} catch {
return false;
}
}
/**
* Answer an intercepted photo lookup from a page that already uses request
* interception. Returns false for any other request, which the caller
* handles as before.
* @param {import('puppeteer').HTTPRequest} request
* @returns {boolean} Whether the request was answered.
*/
export function respondToPlanespotters(request) {
if (!isPlanespottersRequest(request.url())) return false;
request.respond({
status: 200,
contentType: 'application/json',
headers: { 'Access-Control-Allow-Origin': '*' },
body: PLANESPOTTERS_EMPTY_BODY,
});
return true;
}
/**
* Stub photo lookups in a page that does not intercept requests: wraps
* `fetch` before any app code runs, so a lookup never reaches the network.
* Composes with other `fetch` shims in either order.
* @param {import('puppeteer').Page} page
* @returns {Promise<void>}
*/
export async function stubPlanespotters(page) {
await page.evaluateOnNewDocument(
(origin, body) => {
const original = window.fetch.bind(window);
window.fetch = (input, init) => {
const href =
typeof input === 'string' ? input : input?.url || String(input);
let target = '';
try {
target = new URL(href, location.href).origin;
} catch {
/* not a URL */
}
if (target === origin) {
return Promise.resolve(
new Response(body, {
status: 200,
headers: { 'Content-Type': 'application/json' },
}),
);
}
return original(input, init);
};
},
PLANESPOTTERS_ORIGIN,
PLANESPOTTERS_EMPTY_BODY,
);
}
+2
View File
@@ -32,6 +32,7 @@
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { stubPlanespotters } from './qa-photo-stub.mjs';
import { classifyAircraft, CLASS_SCALE_2D, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
// Hangar fleet (2026-08-16): real per-class GLBs render at scale 1; military
// heavies (airliner/quadjet/glider) render airplane.glb at 1 x class; only
@@ -186,6 +187,7 @@ async function main() {
const httpErrors = [];
try {
const page = await browser.newPage();
await stubPlanespotters(page);
await page.setViewport({ width: 1280, height: 800 });
page.on('console', (msg) => {
if (msg.type() === 'error') {
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env node
/**
* Trail freeze gate: freezing a trail chunk never shows a blank frame.
*
* A tracked aircraft's trail is cut into chunks; every chunk but the newest
* is frozen into static geometry, which Cesium builds asynchronously. Until
* that geometry is in the scene, the chunk keeps being drawn dynamically.
*
* This gate drives the real app and a real Cesium scene: it draws a trail of
* CHUNK_POINTS points (all dynamic), then adds one point so the first chunk
* freezes, and on every rendered frame after that it checks two things:
* 1. every frozen chunk is either still drawn by a dynamic polyline or its
* static geometry is in the scene (DataSourceDisplay reports DONE);
* 2. the number of trail-coloured pixels never drops below 90% of the
* count measured just before the freeze.
* It also checks that clearing the trail releases every chunk entity.
*
* The camera is held still while the gate runs.
*
* Needs a running dev server:
* node scripts/qa-trail-freeze.mjs [--url http://localhost:4173]
*/
import puppeteer from 'puppeteer';
import { chromeExecutablePath } from './qa-chrome.mjs';
const urlFlag = process.argv.indexOf('--url');
const APP_URL =
urlFlag > 0 ? process.argv[urlFlag + 1] : 'http://localhost:4173';
const browser = await puppeteer.launch({
headless: 'new',
protocolTimeout: 300_000,
executablePath: chromeExecutablePath(),
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--use-gl=angle',
'--use-angle=swiftshader',
'--disable-dev-shm-usage',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--window-size=960,640',
],
});
let failed = false;
const report = (name, ok, detail) => {
if (!ok) failed = true;
console.log(
` [${ok ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`,
);
};
try {
const page = await browser.newPage();
await page.setViewport({ width: 960, height: 640 });
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.waitForFunction(
() =>
window.__godsEyeView?.viewer &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 180_000 },
);
const result = await page.evaluate(async () => {
// The exact Cesium module the renderer uses, so classes are shared.
const source = await (await fetch('/src/data/trailRenderer.js')).text();
const cesiumUrl = source.match(/from\s+["']([^"']*cesium[^"']*)["']/)[1];
const Cesium = await import(cesiumUrl);
const { createTrail, CHUNK_POINTS } =
await import('/src/data/trailRenderer.js');
const viewer = window.__godsEyeView.viewer;
const scene = viewer.scene;
const nextFrame = () =>
new Promise((resolve) => {
const remove = scene.postRender.addEventListener(() => {
remove();
resolve();
});
scene.requestRender();
});
// A line across the middle of the current view, in mid-air in front of
// the camera, so terrain and buildings never hide it.
viewer.camera.cancelFlight?.();
const camera = viewer.camera;
const ahead = Cesium.Cartesian3.add(
camera.positionWC,
Cesium.Cartesian3.multiplyByScalar(
camera.directionWC,
3_000,
new Cesium.Cartesian3(),
),
new Cesium.Cartesian3(),
);
const points = Array.from({ length: CHUNK_POINTS + 1 }, (_, i) =>
Cesium.Cartesian3.add(
ahead,
Cesium.Cartesian3.multiplyByScalar(
camera.rightWC,
-1_200 + (2_400 * i) / CHUNK_POINTS,
new Cesium.Cartesian3(),
),
new Cesium.Cartesian3(),
),
);
const added = [];
const removed = [];
const add = viewer.entities.add.bind(viewer.entities);
const remove = viewer.entities.remove.bind(viewer.entities);
viewer.entities.add = (definition) => {
const entity = add(definition);
if (String(entity.id).startsWith('gev-trail:')) added.push(entity);
return entity;
};
viewer.entities.remove = (entity) => {
if (String(entity?.id).startsWith('gev-trail:')) removed.push(entity);
return remove(entity);
};
const gl = scene.context._gl;
const width = gl.drawingBufferWidth;
const height = gl.drawingBufferHeight;
const pixels = new Uint8Array(width * height * 4);
// Magenta trail: count pixels that are clearly magenta.
const countTrailPixels = () => {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let count = 0;
for (let p = 0; p < pixels.length; p += 4) {
const r = pixels[p];
const g = pixels[p + 1];
const b = pixels[p + 2];
if (r > 150 && b > 150 && g < 110) count++;
}
return count;
};
const trail = createTrail(viewer, { color: '#ff00ff', width: 8 });
trail.setPositions(points.slice(0, CHUNK_POINTS));
for (let i = 0; i < 20; i++) await nextFrame();
const before = trail._chunksForTest();
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let lit = 0;
for (let p = 0; p < pixels.length; p += 4)
if (pixels[p] || pixels[p + 1] || pixels[p + 2]) lit++;
const cam = viewer.camera.positionCartographic;
const diagnostics = {
lit,
size: [width, height],
camera: [
Cesium.Math.toDegrees(cam.longitude),
Cesium.Math.toDegrees(cam.latitude),
cam.height,
],
preserveDrawingBuffer: gl.getContextAttributes().preserveDrawingBuffer,
};
let baseline = 0;
for (let i = 0; i < 5; i++) {
await nextFrame();
baseline = Math.max(baseline, countTrailPixels());
}
trail.setPositions(points);
const frames = [];
const sphere = new Cesium.BoundingSphere();
let resolvedAt = null;
for (let i = 0; i < 400; i++) {
await nextFrame();
const state = trail._chunksForTest();
const covered = state.frozen.every(
(chunk) =>
chunk.standIn ||
viewer.dataSourceDisplay.getBoundingSphere(
chunk.entity,
false,
sphere,
) === Cesium.BoundingSphereState.DONE,
);
frames.push({ covered, pixels: countTrailPixels() });
if (resolvedAt == null && state.frozen.every((c) => !c.standIn))
resolvedAt = i;
if (resolvedAt != null && i > resolvedAt + 30) break;
}
trail.clear();
for (let i = 0; i < 3; i++) await nextFrame();
const frozenAfterClear = trail._chunksForTest().frozen.length;
trail.destroy();
const leaked = added.filter((entity) => !removed.includes(entity)).length;
viewer.entities.add = add;
viewer.entities.remove = remove;
return {
chunkPoints: CHUNK_POINTS,
diagnostics,
frozenBefore: before.frozen.length,
baseline,
frames,
resolvedAt,
frozenAfterClear,
entities: added.length,
leaked,
};
});
if (process.env.GEV_TEST_VERBOSE)
console.log(JSON.stringify(result.diagnostics));
const minPixels = Math.min(...result.frames.map((frame) => frame.pixels));
report(
'the trail draws before the freeze',
result.baseline > 200 && result.frozenBefore === 0,
`baseline=${result.baseline} trail pixels, frozen chunks=${result.frozenBefore}`,
);
report(
'the frozen chunk reaches the scene as static geometry',
result.resolvedAt != null,
`stand-in dropped after ${result.resolvedAt} frames`,
);
report(
'every frame after the freeze draws the frozen chunk (dynamic copy or static geometry)',
result.frames.every((frame) => frame.covered),
`${result.frames.length} frames checked`,
);
report(
'no blank frame: trail pixels never drop below 90% of the pre-freeze count',
minPixels >= 0.9 * result.baseline,
`min=${minPixels} baseline=${result.baseline}`,
);
report(
'clearing and destroying release every trail entity',
result.frozenAfterClear === 0 && result.leaked === 0,
`entities created=${result.entities} leaked=${result.leaked}`,
);
} catch (error) {
report('gate ran', false, error.message);
} finally {
await browser.close();
}
console.log(failed ? 'RESULT: FAIL' : 'RESULT: PASS');
process.exit(failed ? 1 : 0);
+11 -11
View File
@@ -8,15 +8,16 @@ export const ALLOCATION_TEST_FILES = Object.freeze([
'src/overlays/worldOverlayAllocation.test.mjs',
]);
/** Whether this runtime matches the one the allocation budgets were calibrated on. */
/** Whether this runtime matches a supported major the allocation budgets are calibrated on. */
export function isCalibratedAllocationRuntime(version = process.versions.node) {
return Number.parseInt(String(version).split('.')[0], 10) === 24;
const major = Number.parseInt(String(version).split('.')[0], 10);
return major === 24 || major === 26;
}
/** Require the runtime on which allocation budgets were calibrated. */
export function assertNode24AllocationRuntime(version = process.versions.node) {
/** Require a runtime on which allocation budgets are calibrated. */
export function assertCalibratedAllocationRuntime(version = process.versions.node) {
if (!isCalibratedAllocationRuntime(version)) {
throw new Error(`Allocation budgets require the calibrated Node 24 runtime; received ${version}`);
throw new Error(`Allocation budgets require calibrated Node 24 or 26; received ${version}`);
}
return version;
}
@@ -92,19 +93,18 @@ export function runUnitTests() {
const parallelStatus = runTests(['--test', ...plan.parallel]);
if (parallelStatus !== 0) return parallelStatus;
// The GC-bracketed budgets are calibrated on Node 24 and are meaningless on
// other allocators. A contributor's suite must stay green on any supported
// engine (package.json permits >=24), so uncalibrated runtimes skip the
// The GC-bracketed budgets are calibrated on every supported Node major.
// Unsupported runtimes may still run ordinary tests, so uncalibrated runtimes skip the
// probes with a warning. Set GEV_REQUIRE_ALLOCATION_GATE=1 (pinned CI /
// release batteries) to make an uncalibrated runtime a hard failure.
if (!isCalibratedAllocationRuntime()) {
if (process.env.GEV_REQUIRE_ALLOCATION_GATE === '1') {
assertNode24AllocationRuntime();
assertCalibratedAllocationRuntime();
}
console.warn(
`[unit] SKIPPED ${ALLOCATION_TEST_FILES.length} allocation microbenchmarks: `
+ `budgets are calibrated for Node 24, running ${process.versions.node}. `
+ 'Run under Node 24 (or set GEV_REQUIRE_ALLOCATION_GATE=1 to fail instead).',
+ `budgets are calibrated for Node 24 and 26, running ${process.versions.node}. `
+ 'Run under Node 24 or 26 (or set GEV_REQUIRE_ALLOCATION_GATE=1 to fail instead).',
);
return 0;
}
+156 -21
View File
@@ -42,8 +42,9 @@
* and a real mouse click on the tracked plane is a
* NO-OP (no deselect, no camera spike).
* M3. AGE-OUT RELEASE — when the tracked plane's fixes stop arriving
* (3 missed polls via the shim), tracking clears and
* the camera is RELEASED IN PLACE: viewer.trackedEntity
* it stays tracked (STALE, then SIGNAL LOST) until
* 10 min after its newest fix; then tracking clears
* and the camera is RELEASED IN PLACE: viewer.trackedEntity
* undefined, NO jump (owner decision 2026-07-02 —
* the old ~80 km overview flyTo is gone).
*
@@ -94,6 +95,7 @@
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import { respondToPlanespotters } from './qa-photo-stub.mjs';
import { arrivalAngleViolated, arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.mjs';
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
@@ -287,6 +289,8 @@ async function main() {
});
return;
}
// Aircraft photo lookups never reach the third party: no photos.
if (respondToPlanespotters(request)) return;
if (url.origin === APP_ORIGIN && url.pathname === '/api/openai/hud-summary') {
request.respond({
status: 200,
@@ -359,7 +363,12 @@ async function main() {
'Synthetica', // 2 origin_country
Math.floor(Date.now() / 1000), // 3 time_position
Math.floor(Date.now() / 1000), // 4 last_contact
f.lon, // 5 longitude
// A plane given `moveFromMs` flies east at its reported speed
// (the trail-head check needs a moving icon); others hold still.
Number.isFinite(f.moveFromMs)
? f.lon + (f.vel * (Date.now() - f.moveFromMs)) / 1000
/ (111320 * Math.cos((f.lat * Math.PI) / 180))
: f.lon, // 5 longitude
f.lat, // 6 latitude
f.alt, // 7 baro_altitude (m)
f.onGround === true, // 8 on_ground (ground-traffic phase flips this live)
@@ -1495,6 +1504,110 @@ async function main() {
`max=${maxDelta.toFixed(3)} m over ${samples.length} frames (tol ${JITTER_TOL_M} m, mode=${modelUp ? 'model-matrix' : 'display-contract'})`);
}
// ============================================================
// TRAIL HEAD ENDS AT THE DRAWN ICON, every rendered frame.
// The head's last vertex is where the tracked plane is drawn (the
// display position, or the model's tail anchor while the model is the
// visual). getTrackedInfo reports the dead-reckoned position without
// the brief post-report easing; its gap is logged, not asserted.
// ============================================================
console.log('\nTrail head — ends at the drawn icon on every frame');
// The fixture planes repeat one position, so the icon holds and draws no
// head. Fly the tracked one east for this check; the display runs 30 s
// behind, so give it time to reach the moving reports.
await evalPage((icao) => {
const f = window.__SYNTH.flights.find((p) => p.icao === icao);
f.moveFromMs = Date.now();
}, trackedIcao);
await sleep(55_000);
const headSamples = await sampleFrames(page, 40, (icao) => {
const viewer = window.__godsEyeView.viewer;
const fl = window.__godsEyeView.dataManager.layers.get('flights').module;
const ent = viewer.trackedEntity;
const time = viewer.clock.currentTime;
const head = viewer.entities.values.find((e) => String(e.id).startsWith('gev-trail:fl-head-'));
const line = head?.polyline?.positions?.getValue(time);
const drawn = ent?.position?.getValue(time);
if (!line?.length || !drawn) return null;
const end = line[line.length - 1];
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
const ti = fl.getTrackedInfo();
let infoGap = null;
if (ti && ti.icao24 === icao) {
// WGS84 lat/lon → ECEF on the ellipsoid; the angle at the centre to
// the drawn position, times the mean radius, is the ground gap.
const a = 6378137, e2 = 6.69437999014e-3;
const lat = (ti.latitude * Math.PI) / 180, lon = (ti.longitude * Math.PI) / 180;
const n = a / Math.sqrt(1 - e2 * Math.sin(lat) ** 2);
const p = {
x: n * Math.cos(lat) * Math.cos(lon),
y: n * Math.cos(lat) * Math.sin(lon),
z: n * (1 - e2) * Math.sin(lat),
};
const dot = p.x * drawn.x + p.y * drawn.y + p.z * drawn.z;
const cos = dot / (Math.hypot(p.x, p.y, p.z) * Math.hypot(drawn.x, drawn.y, drawn.z));
infoGap = 6371008.8 * Math.acos(Math.min(1, cos));
}
// While the 3D model is the visual the head ends at the model's tail
// anchor, which sits on the drawn (pixel-size-enlarged) model.
const model = window.__findTrackedModel ? window.__findTrackedModel() : null;
const sphere = model && model.ready && model.show ? model.boundingSphere : null;
// boundingSphere leaves out the minimum-pixel-size enlargement that
// computedScale carries; the drawn model is that much larger.
const enlarge = sphere && Number.isFinite(model.computedScale) && model.scale > 0
? Math.max(1, model.computedScale / model.scale)
: 1;
const radius = sphere ? sphere.radius * enlarge : null;
return {
end: dist(end, drawn),
onModel: sphere ? dist(end, sphere.center) <= radius + 50 : false,
modelRadius: radius,
infoGap,
vertices: line.length,
};
}, trackedIcao);
// The same reading between frames, the way a page script polls it: the
// head holds the last rendered frame while getTrackedInfo dead-reckons
// the current instant. Logged only, to tell the two apart.
const betweenFrames = [];
for (let i = 0; i < 8; i++) {
await sleep(600);
betweenFrames.push(await evalPage((icao) => {
const viewer = window.__godsEyeView.viewer;
const head = viewer.entities.values.find((e) => String(e.id).startsWith('gev-trail:fl-head-'));
const line = head?.polyline?.positions?.getValue(viewer.clock.currentTime);
const drawn = viewer.trackedEntity?.position?.getValue(viewer.clock.currentTime);
if (!line?.length || !drawn) return null;
const end = line[line.length - 1];
return Math.hypot(end.x - drawn.x, end.y - drawn.y, end.z - drawn.z);
}, trackedIcao));
}
console.log(` (between frames: head end vs entity position, m: ${
betweenFrames.map((v) => (v == null ? '-' : v.toFixed(1))).join(' ')})`);
// Freeze the plane where it got to, so later checks see a still contact.
await evalPage((icao) => {
const f = window.__SYNTH.flights.find((p) => p.icao === icao);
f.lon += (f.vel * (Date.now() - f.moveFromMs)) / 1000
/ (111320 * Math.cos((f.lat * Math.PI) / 180));
delete f.moveFromMs;
}, trackedIcao);
const headUsable = headSamples.values.filter((v) => v != null);
if (headUsable.length < 10) {
skip('trail head: ends at the drawn icon on every frame',
`only ${headUsable.length} frames with a drawn head (timedOut=${headSamples.timedOut})`);
} else {
const worst = Math.max(...headUsable.map((v) => v.end));
const off = headUsable.filter((v) => !(v.end <= 50 || v.onModel));
const onModel = headUsable.filter((v) => v.end > 50 && v.onModel).length;
const radii = headUsable.map((v) => v.modelRadius).filter((v) => v != null);
const gaps = headUsable.map((v) => v.infoGap).filter((v) => v != null);
record('trail head: last vertex on the drawn icon (within 50 m, or on the drawn 3D model) on every frame',
off.length === 0,
`${off.length} of ${headUsable.length} frames off; max end-to-centre=${worst.toFixed(1)} m, ${onModel} frames ending on the model's tail (model radius ${
radii.length ? Math.max(...radii).toFixed(0) : 'n/a'} m); getTrackedInfo vs drawn icon max=${
gaps.length ? Math.max(...gaps).toFixed(1) : 'n/a'} m (logged only)`);
}
// ============================================================
// INVARIANT 2: NO PULL-OUT on a track switch
// tracking B while tracking A must not fly to ~80 km overview.
@@ -1741,7 +1854,8 @@ async function main() {
// ============================================================
// REGRESSION M3 (rewritten 2026-07-02 — deliberate behavior change):
// when the TRACKED plane ages out (3 missed polls), tracking must clear
// when the TRACKED plane ages out (10 min after its newest fix, past the
// 3 missed polls that remove an untracked plane), tracking must clear
// and the camera must be RELEASED IN PLACE — viewer.trackedEntity
// undefined and NO jump (the old contract flew an ~80 km overview; the
// owner field-ruled that wrong: "it randomly zooms way up and loses my
@@ -1762,27 +1876,48 @@ async function main() {
const beforeTracking = !!fl.getTrackedInfo();
// Simulate the age-out via the shim: the plane stops arriving.
window.__SYNTH.flights = window.__SYNTH.flights.filter((f) => f.icao !== icao);
// MISSING_POLL_LIMIT = 3 consecutive absent polls → removal + clear.
// Capture the camera pose right BEFORE the removing poll — the follow
// camera drifts with the coasting plane until the instant of release,
// so this is the "where the user was looking" reference.
// The tracked plane outlives MISSING_POLL_LIMIT (3 absent polls): it is
// kept until 10 min after its newest fix, so the camera keeps its target.
await fl.update(v);
await fl.update(v);
const c0 = v.camera.positionCartographic;
const atAgeOut = {
lonDeg: (c0.longitude * 180) / Math.PI,
latDeg: (c0.latitude * 180) / Math.PI,
height: c0.height,
};
await fl.update(v); // third miss → removal + clear + in-place release
return {
beforeTracking,
atAgeOut,
stillTrackedAfter: !!fl.getTrackedInfo(),
viewerStillTracking: !!v.trackedEntity,
};
await fl.update(v);
const trackedThroughMisses = !!fl.getTrackedInfo();
// Jump the page clock past the 10 min retention instead of waiting.
const realNow = Date.now;
Date.now = () => realNow() + 11 * 60 * 1000;
try {
// Let the follow camera settle on the stopped plane, then capture the
// pose right BEFORE the removing poll: the "where the user was
// looking" reference.
await new Promise((res) => {
const stop = v.scene.postRender.addEventListener(() => {
stop();
res();
});
v.scene.requestRender();
});
const c0 = v.camera.positionCartographic;
const atAgeOut = {
lonDeg: (c0.longitude * 180) / Math.PI,
latDeg: (c0.latitude * 180) / Math.PI,
height: c0.height,
};
await fl.update(v); // past retention → removal + clear + in-place release
return {
beforeTracking,
trackedThroughMisses,
atAgeOut,
stillTrackedAfter: !!fl.getTrackedInfo(),
viewerStillTracking: !!v.trackedEntity,
};
} finally {
Date.now = realNow;
}
}, m3Icao);
record('M3: the tracked plane stays tracked through 3 missed polls (kept 10 min after its newest fix)',
m3.trackedThroughMisses,
`trackedThroughMisses=${m3.trackedThroughMisses}`);
record('M3: tracking cleared after the tracked plane ages out (getTrackedInfo null)',
m3.beforeTracking && !m3.stillTrackedAfter,
`before=${m3.beforeTracking} trackedAfter=${m3.stillTrackedAfter}`);