From d404445da3ae031608e095f0e9dfbf29468d7efa Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 8 Nov 2026 14:37:29 -0800 Subject: [PATCH] fix(satellites): every class but Starlink shows by default Showing only Stations and Imaging by default hid GPS, GEO comsats, Hubble and the rest of what the layer showed before classes, for every existing user and saved view. Every class now starts on except Starlink, which stays off as the DENSE switch was. --- changelog.d/1558-satellite-classes.md | 2 +- docs/CURRENT-STATE.md | 2 +- src/data/satelliteClass.test.mjs | 27 +++++++++++++++----------- src/layers/satellites/classes.js | 11 +++++++---- src/layers/satellites/classes.test.mjs | 2 +- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/changelog.d/1558-satellite-classes.md b/changelog.d/1558-satellite-classes.md index a46d678ac..baaed9bff 100644 --- a/changelog.d/1558-satellite-classes.md +++ b/changelog.d/1558-satellite-classes.md @@ -1,4 +1,4 @@ - File satellites under six classes, Stations, Imaging, Telecom, Starlink, - Probes and Debris, each with a toggle; Stations and Imaging show by default. + Probes and Debris, each with a toggle; all but Starlink show by default. Classes come from CelesTrak SATCAT, the UCS Satellite Database and name rules (Denis Groshev, @ZloyS, #1558). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3e62496db..46b608a8d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5458,7 +5458,7 @@ silently demoting every later lookup for the session. - **Satellite classes** (`src/layers/satellites/classes.js`): every satellite is filed under one of six classes, each with its own point colour, legend swatch and toggle: STATIONS (warm white), IMAGING (cyan), TELECOM (violet; communications and navigation, so GPS, GLONASS, Galileo and BeiDou read as one family), STARLINK (dim slate), PROBES (muted blue-gray) and DEBRIS (receding brown-gray). That module is the single source of truth for class, label and colour, so the dot, the card and the legend swatch cannot disagree. Two deliberate palette rules: no class may sit in the 40–48° amber band, which is the app-wide known-military convention; and STARLINK stays far below PROBES in Rec.601 luminance so the dense shell stays separable when NVG/FLIR collapse the scene to one channel. The ISS keeps its long-standing red hero dot rather than the STATIONS colour — it carries a permanent name label and its card still reads `STATIONS · ISS`; it is a station whichever group ingested it, so a stations-feed outage cannot refile it. `src/data/satelliteClass.js` keeps the group-based labels beneath the classes: the constellation subtype and a group's own label for a satellite with no class. - Class is also a **text field**, not just a color: `TELECOM · GPS` / `STARLINK` / `STATIONS · ISS` leads the tracked card's detail block and replaces the raw CelesTrak tag on the detection-overlay label. Because that canvas composites above the post-FX chain, the class stays readable in NVG/FLIR after the in-scene dot colors are flattened. - The satellites row in DATA LAYERS carries per-layer sub-controls (`DataLayerManager._syncRowControls`, the first consumer of the optional `getRowControls()` layer hook): one chip per class (the STARLINK chip exposes the existing `catalog` param), a swatch legend with live per-class counts, and an info line while SATCAT is stale or unavailable (`Classes may lag: SATCAT is a cached copy` / `Classes may lag: SATCAT unavailable`). Default is the sparse core catalog. The chips are stateless — each declares the params to apply and the manager owns the write — so the Space Missions capture/restore path over the same param stays authoritative. Controls stay hidden while the layer is off. An explicit CORE or DENSE choice participates in versioned local and share-link state; temporary Space Missions overrides do not. -- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS and IMAGING on by default; TELECOM (communications and navigation), STARLINK, PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects and dead payloads) off. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay and the voice/framing position lists, and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/`, cached a day), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: operator, owner country and purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. Class visibility is a runtime param (`classes`) and is not yet part of share links; STARLINK maps to the existing `catalog: 'dense'` param, which is. +- **Class toggles** (`src/layers/satellites/classes.js`): the layer row carries one chip per class — STATIONS, IMAGING, TELECOM (communications and navigation), PROBES (science, technology development and demonstration, unknown purpose) and DEBRIS (rocket bodies, fragments, unknown objects and dead payloads) on by default, so the layer shows what it did before classes; STARLINK off, as DENSE was. Points of a hidden class stay built and are only hidden, so a toggle needs no reload; hidden classes are left out of the detection overlay and the voice/framing position lists, and the legend keys only the classes on screen. The class comes, strongest evidence first, from CelesTrak SATCAT (object type and operational status, fetched per group through `/api/celestrak-satcat/`, cached a day), the bundled UCS Satellite Database pack (UCS-Satellite-Database 5-1-2023: operator, owner country and purpose; `src/data/local_data/ucs_satellites/`), name patterns for satellites launched after May 2023, and finally the CelesTrak group. Only crewed stations and the craft that visit them, by name, are STATIONS: the CubeSats CelesTrak's stations group also carries after their deployment from the ISS are classed like any other satellite. A payload whose SATCAT record gives no status, with no UCS record and no recognisable name, that launched 20 or more years ago (SATCAT's launch date, else the TLE designator's year) is presumed dead and filed as debris; without a SATCAT record (a failed read) nothing is presumed. To cover imaging launched after the UCS snapshot the core catalog also reads CelesTrak's `resource`, `weather`, `planet`, `sar` and `science` groups. Retired stations and the shuttles have re-entered and have no orbit to show. Class visibility is a runtime param (`classes`) and is not yet part of share links; STARLINK maps to the existing `catalog: 'dense'` param, which is. - **The STARLINK chip (formerly DENSE) reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `STARLINK ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `STARLINK ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale. - **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert. - The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe. diff --git a/src/data/satelliteClass.test.mjs b/src/data/satelliteClass.test.mjs index 6e0645cda..ba9e8893b 100644 --- a/src/data/satelliteClass.test.mjs +++ b/src/data/satelliteClass.test.mjs @@ -412,7 +412,7 @@ test('a dependency owner hiding the points surrenders the whole row', async () = } }); -test('stations and imaging show by default; each class chip turns its class on and off', async () => { +test('every class but Starlink shows by default; each class chip turns its class on and off', async () => { const originalFetch = globalThis.fetch; const log = console.log; const warn = console.warn; @@ -434,35 +434,40 @@ test('stations and imaging show by default; each class chip turns its class on a text: async () => served[String(url)] || '', }); await satellitesLayer.update({ scene: { primitives: { add: (p) => p, remove() {} } } }); - assert.deepEqual(shown(), [25544, 40002], 'stations and imaging only'); + assert.deepEqual(shown(), [25544, 40001, 40002, 40003], 'all but Starlink'); assert.deepEqual( satellitesLayer.getRowControls().chips.map((entry) => entry.label), ['STATIONS', 'IMAGING', 'TELECOM', 'STARLINK', 'PROBES', 'DEBRIS'], ); assert.equal(chip('imaging').active, true); - assert.equal(chip('telecom').active, false); - assert.deepEqual(chip('telecom').params, { classes: { telecom: true } }); + assert.equal(chip('telecom').active, true); + assert.equal( + satellitesLayer.getRowControls().chips.find((entry) => entry.id === 'catalog').active, + false, + ); + assert.deepEqual(chip('telecom').params, { classes: { telecom: false } }); assert.deepEqual( satellitesLayer.getRowControls().legend.map((row) => row.klass), - ['stations', 'imaging'], + ['stations', 'imaging', 'telecom', 'debris'], 'the legend keys only what is on screen', ); satellitesLayer.setParams(chip('telecom').params); satellitesLayer.setParams(chip('debris').params); - assert.deepEqual(shown(), [25544, 40001, 40002, 40003]); - assert.equal(chip('debris').active, true); + assert.deepEqual(shown(), [25544, 40002], 'telecom and debris off'); + assert.equal(chip('debris').active, false); assert.deepEqual(satellitesLayer.getParams().classes, { stations: true, imaging: true, - telecom: true, + telecom: false, starlink: false, - probes: false, - debris: true, + probes: true, + debris: false, }); + satellitesLayer.setParams(chip('telecom').params); satellitesLayer.setParams(chip('imaging').params); - assert.deepEqual(shown(), [25544, 40001, 40003], 'imaging off'); + assert.deepEqual(shown(), [25544, 40001], 'telecom back on, imaging off'); assert.equal(satellitesLayer.setParams({ classes: 'all' }), false, 'malformed request refused'); } finally { console.log = log; diff --git a/src/layers/satellites/classes.js b/src/layers/satellites/classes.js index c42592ee8..07822089e 100644 --- a/src/layers/satellites/classes.js +++ b/src/layers/satellites/classes.js @@ -39,14 +39,17 @@ export const SATELLITE_CLASS_IDS = Object.freeze([ 'debris', ]); -/** Which classes show when nobody has chosen. */ +/** + * Which classes show when nobody has chosen: everything the layer showed + * before classes existed. Starlink stays off, as the DENSE switch was. + */ export const DEFAULT_SATELLITE_CLASSES = Object.freeze({ stations: true, imaging: true, - telecom: false, + telecom: true, starlink: false, - probes: false, - debris: false, + probes: true, + debris: true, }); /** diff --git a/src/layers/satellites/classes.test.mjs b/src/layers/satellites/classes.test.mjs index 7d52c694e..cf73fde14 100644 --- a/src/layers/satellites/classes.test.mjs +++ b/src/layers/satellites/classes.test.mjs @@ -32,7 +32,7 @@ test('six classes, with stations and imaging on by default and the rest off', () ]); assert.deepEqual( SATELLITE_CLASS_IDS.filter((id) => DEFAULT_SATELLITE_CLASSES[id]), - ['stations', 'imaging'], + ['stations', 'imaging', 'telecom', 'probes', 'debris'], ); for (const id of SATELLITE_CLASS_IDS) { const display = SATELLITE_CLASS_DISPLAY[id];