fix(cyclones): evict prior shared selection on storm switch

Regression: explicit storm A→B→A keeps one active record.
Reviewer finding: quanticsoul4772 (#847).
This commit is contained in:
Kevin Rajan committed 2026-10-16 23:01:44 -05:00
1 parent 28f3243090
commit a803be1af2
4 files changed
+29 -6

No files matched your search

@@ -0,0 +1,4 @@
- Explicit cyclone selections publish only the currently selected NHC/CPHC
storm into shared context. Switching storms evicts the previous identity
instead of leaving stale coordinates active (kvnloo, #847; reviewed by
quanticsoul4772).
+5
View File
@@ -218,6 +218,11 @@ not storm size or the full hazard area. Successful empty and unavailable states
are distinct. One owned native Cesium data source preserves polygon holes and
geographic seams and releases on disable; forecast animation is not implied.
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
Explicit cyclone selections publish one shared `weather-cyclones` record
with official advisory provenance and source position. Switching storms removes
the prior record; automatic row selection never claims global context, and
refreshes do not re-announce selection. Clearing or losing an explicitly selected
storm removes shared context rather than assigning a fallback.
Consult the linked official advisory for safety decisions.
Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state.
+5 -1
View File
@@ -79,8 +79,12 @@ export function createCyclonesLayer({
return null;
}
selectedContextEntity ||= { show: true };
const contextId = `cyclone:${storm.id}`;
context.removeEntityContextsForLayer?.('weather-cyclones', {
retainIds: new Set([contextId]),
});
const record = context.registerEntityContext?.(selectedContextEntity, {
id: `cyclone:${storm.id}`,
id: contextId,
layerId: 'weather-cyclones',
layerName: 'Cyclone advisories',
source: 'NOAA NHC / CPHC',
+15 -5
View File
@@ -419,11 +419,13 @@ test('explicit cyclone selection publishes shared context without auto-claiming
selectedEvents += 1;
return selectedContext;
},
removeEntityContextsForLayer(layerId) {
removeEntityContextsForLayer(layerId, { retainIds } = {}) {
for (const [id, record] of records) {
if (record.layerId === layerId) records.delete(id);
if (record.layerId === layerId && !retainIds?.has(id))
records.delete(id);
}
if (selectedContext?.layerId === layerId) selectedContext = null;
if (selectedContext && !records.has(selectedContext.id))
selectedContext = null;
},
};
const h = harness({ getSnapshot: async () => next }, { context });
@@ -446,10 +448,18 @@ test('explicit cyclone selection publishes shared context without auto-claiming
assert.equal(selectedContext?.longitude, -125.8);
assert.equal(selectedContext?.properties.advisoryNumber, '10');
h.layer.setParams({ stormId: 'ep152026' });
assert.deepEqual([...records.keys()], ['cyclone:ep152026']);
assert.equal(selectedContext?.id, 'cyclone:ep152026');
h.layer.setParams({ stormId: 'ep162026' });
assert.deepEqual([...records.keys()], ['cyclone:ep162026']);
assert.equal(selectedContext?.id, 'cyclone:ep162026');
assert.equal(selectedEvents, 3, 'each explicit storm switch announces once');
await h.layer.update();
assert.equal(
selectedEvents,
1,
3,
'refresh updates the selected record without re-announcing the click',
);
@@ -463,7 +473,7 @@ test('explicit cyclone selection publishes shared context without auto-claiming
assert.equal(records.size, 0);
h.layer.setParams({ stormId: 'ep152026' });
assert.equal(selectedEvents, 2);
assert.equal(selectedEvents, 4);
h.layer.setParams({ clear: true });
assert.equal(selectedContext, null);
assert.equal(records.size, 0);