mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
fix(launches): hand the camera over before stopping, and only blame a fresh index error
- releaseCameraOwnership marks the camera released before stopping, so a flight navigation asked to preserve survives; exit also releases it, so an inactive layer never cancels another owner's flight. - The failure toast names the mission index only when its error is from the current Context attempt (stats.errorAt), not an earlier one. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
13219828e3
commit
efa4374644
7 files changed
+41
-10
No files matched your search
Vendored
+2
-1
@@ -14,10 +14,10 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
releaseCameraOwnership() {
|
releaseCameraOwnership() {
|
||||||
// Navigation takes the camera; a failed entry must not restore the old view.
|
// Navigation takes the camera; a failed entry must not restore the old view.
|
||||||
layerState._entryCameraView = null;
|
layerState._entryCameraView = null;
|
||||||
|
layerState._cameraReleased = true;
|
||||||
parts.panel.clearMissionRosterHover();
|
parts.panel.clearMissionRosterHover();
|
||||||
parts.replay.stopMissionReplay();
|
parts.replay.stopMissionReplay();
|
||||||
parts.selection.stopMissionZoomAnchor();
|
parts.selection.stopMissionZoomAnchor();
|
||||||
layerState._cameraReleased = true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +60,7 @@ export function createControls({ state: layerState, services, parts, source }) {
|
|||||||
orbitMatches: layerState._orbitMatches,
|
orbitMatches: layerState._orbitMatches,
|
||||||
lastUpdate: layerState._lastUpdate,
|
lastUpdate: layerState._lastUpdate,
|
||||||
error: layerState._lastError,
|
error: layerState._lastError,
|
||||||
|
errorAt: layerState._lastErrorAt,
|
||||||
stale: layerState._stale,
|
stale: layerState._stale,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import http from 'node:http';
|
|||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { DataLayerManager } from '../../data/manager.js';
|
import { DataLayerManager } from '../../data/manager.js';
|
||||||
import { ContextControls } from '../../ui/contextControls.js';
|
import { ContextControls } from '../../ui/contextControls.js';
|
||||||
|
import { spaceMissionsIndexFailure } from '../../ui/contextActions.js';
|
||||||
import { createLaunchSource } from './source.js';
|
import { createLaunchSource } from './source.js';
|
||||||
import { createState } from './state.js';
|
import { createState } from './state.js';
|
||||||
import { createModel } from './model.js';
|
import { createModel } from './model.js';
|
||||||
@@ -454,7 +455,6 @@ test('a refresh failure after a loaded index keeps the mode and its records', as
|
|||||||
await f.settle();
|
await f.settle();
|
||||||
assert.deepEqual(f.view(), framed);
|
assert.deepEqual(f.view(), framed);
|
||||||
});
|
});
|
||||||
// Append to src/layers/launches/failedEntry.test.mjs (uses its fixture()).
|
|
||||||
test('exiting during a successful entry flight cancels the mission-owned flight', async (t) => {
|
test('exiting during a successful entry flight cancels the mission-owned flight', async (t) => {
|
||||||
const f = await fixture(t);
|
const f = await fixture(t);
|
||||||
f.upstream.status = 200;
|
f.upstream.status = 200;
|
||||||
@@ -473,10 +473,7 @@ test('exiting during a successful entry flight cancels the mission-owned flight'
|
|||||||
'leaving Space Missions stops the flight it started (main cancels it)',
|
'leaving Space Missions stops the flight it started (main cancels it)',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
// Append to src/layers/launches/failedEntry.test.mjs (uses its fixture()).
|
// The rollback hides the roster, so the toast has to name the cause.
|
||||||
// Encodes the "surface the reason where the user acted" fix: the roster that
|
|
||||||
// carries 30-DAY MISSION INDEX UNAVAILABLE is hidden by the rollback, so the
|
|
||||||
// lasting feedback (the toast) has to name the cause.
|
|
||||||
test('a failed first load tells the user the mission index is unavailable', async (t) => {
|
test('a failed first load tells the user the mission index is unavailable', async (t) => {
|
||||||
const f = await fixture(t);
|
const f = await fixture(t);
|
||||||
await f.manager.setEnabled('earthquakes', true, { origin: 'user' });
|
await f.manager.setEnabled('earthquakes', true, { origin: 'user' });
|
||||||
@@ -491,3 +488,29 @@ test('a failed first load tells the user the mission index is unavailable', asyn
|
|||||||
`visible feedback names the failure, got ${JSON.stringify(f.toasts)}`,
|
`visible feedback names the failure, got ${JSON.stringify(f.toasts)}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('navigation taking the camera keeps a mission flight it asked to preserve', async (t) => {
|
||||||
|
const f = await fixture(t);
|
||||||
|
f.upstream.status = 200;
|
||||||
|
await f.manager.setEnabled('rocket-launches', true, { origin: 'user' });
|
||||||
|
await f.settle();
|
||||||
|
const flight = f.camera.flight;
|
||||||
|
assert.ok(flight, 'the entry flight is in progress');
|
||||||
|
f.missions.releaseCameraOwnership();
|
||||||
|
assert.equal(f.camera.flight, flight, 'navigation decides about the flight');
|
||||||
|
// After exit an inactive layer never cancels another owner's flight.
|
||||||
|
await f.manager.setEnabled('rocket-launches', false, { origin: 'user' });
|
||||||
|
await f.settle();
|
||||||
|
f.camera.flyTo({ destination: new Cesium.Cartesian3(1, 1, 1) });
|
||||||
|
const other = f.camera.flight;
|
||||||
|
f.missions.releaseCameraOwnership();
|
||||||
|
assert.equal(f.camera.flight, other);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an earlier attempt's index error is not blamed for a later failure", async (t) => {
|
||||||
|
const f = await fixture(t);
|
||||||
|
await f.manager.setEnabled('rocket-launches', true, { origin: 'user' });
|
||||||
|
await f.settle();
|
||||||
|
assert.match(spaceMissionsIndexFailure(f.manager), /mission index/);
|
||||||
|
assert.equal(spaceMissionsIndexFailure(f.manager, Date.now() + 1), null);
|
||||||
|
});
|
||||||
@@ -228,6 +228,7 @@ export function createIngestion({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (layerState._enabled && token === layerState._lifecycleToken) {
|
if (layerState._enabled && token === layerState._lifecycleToken) {
|
||||||
layerState._lastError = error.message;
|
layerState._lastError = error.message;
|
||||||
|
layerState._lastErrorAt = Date.now();
|
||||||
console.warn('[Data:RocketLaunches] Fetch error:', error);
|
console.warn('[Data:RocketLaunches] Fetch error:', error);
|
||||||
// No index yet: fail the enable so Context rolls back.
|
// No index yet: fail the enable so Context rolls back.
|
||||||
// After a loaded index, a failed refresh keeps its records.
|
// After a loaded index, a failed refresh keeps its records.
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export function createLifecycle({
|
|||||||
parts.ingestion.clearPostTleRetry();
|
parts.ingestion.clearPostTleRetry();
|
||||||
parts.panel.clearMissionRosterHover();
|
parts.panel.clearMissionRosterHover();
|
||||||
parts.replay.stopMissionReplay();
|
parts.replay.stopMissionReplay();
|
||||||
|
layerState._cameraReleased = true;
|
||||||
parts.selection.stopMissionZoomAnchor();
|
parts.selection.stopMissionZoomAnchor();
|
||||||
if (layerState._dataSource) layerState._dataSource.show = false;
|
if (layerState._dataSource) layerState._dataSource.show = false;
|
||||||
parts.orbitRendering.syncMissionOrbitPrimitiveVisibility();
|
parts.orbitRendering.syncMissionOrbitPrimitiveVisibility();
|
||||||
@@ -310,6 +311,7 @@ export function createLifecycle({
|
|||||||
await parts.ingestion.restoreSatelliteDependency();
|
await parts.ingestion.restoreSatelliteDependency();
|
||||||
parts.panel.clearMissionRosterHover();
|
parts.panel.clearMissionRosterHover();
|
||||||
parts.replay.stopMissionReplay();
|
parts.replay.stopMissionReplay();
|
||||||
|
layerState._cameraReleased = true;
|
||||||
parts.selection.stopMissionZoomAnchor();
|
parts.selection.stopMissionZoomAnchor();
|
||||||
parts.ingestion.clearPostTleRetry();
|
parts.ingestion.clearPostTleRetry();
|
||||||
if (layerState._clickHandler) layerState._clickHandler.destroy();
|
if (layerState._clickHandler) layerState._clickHandler.destroy();
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export function createState({ services }) {
|
|||||||
|
|
||||||
state._lastError = null;
|
state._lastError = null;
|
||||||
|
|
||||||
|
state._lastErrorAt = null;
|
||||||
|
|
||||||
state._entryCameraView = null;
|
state._entryCameraView = null;
|
||||||
|
|
||||||
state._entryLandedView = null;
|
state._entryLandedView = null;
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import {
|
|||||||
contextModeWord,
|
contextModeWord,
|
||||||
} from '../contextModePolicy.js';
|
} from '../contextModePolicy.js';
|
||||||
|
|
||||||
/** Why a Space Missions entry failed, when its mission index never loaded. */
|
/** Why a Space Missions entry failed: its mission index never loaded, and
|
||||||
export function spaceMissionsIndexFailure(dataManager) {
|
* the error is no older than `since` (an earlier attempt's is not the cause). */
|
||||||
|
export function spaceMissionsIndexFailure(dataManager, since = 0) {
|
||||||
const stats = dataManager?.layers
|
const stats = dataManager?.layers
|
||||||
?.get('rocket-launches')
|
?.get('rocket-launches')
|
||||||
?.module?.getStats?.();
|
?.module?.getStats?.();
|
||||||
return stats?.error && stats.lastUpdate == null
|
return stats?.error && stats.lastUpdate == null && stats.errorAt >= since
|
||||||
? t(
|
? t(
|
||||||
'Space Missions unavailable: the mission index could not load. Try again later.',
|
'Space Missions unavailable: the mission index could not load. Try again later.',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ export function _initGlobalContextPanel() {
|
|||||||
const nextMode =
|
const nextMode =
|
||||||
this._contextMode === 'space-missions' ? null : 'space-missions';
|
this._contextMode === 'space-missions' ? null : 'space-missions';
|
||||||
this._claimContextVisualAuthority();
|
this._claimContextVisualAuthority();
|
||||||
|
const startedAt = Date.now();
|
||||||
void this._runUserFacingContextAction(
|
void this._runUserFacingContextAction(
|
||||||
(notificationToken) =>
|
(notificationToken) =>
|
||||||
this._selectContextMode(nextMode, { notificationToken }),
|
this._selectContextMode(nextMode, { notificationToken }),
|
||||||
() =>
|
() =>
|
||||||
(nextMode && spaceMissionsIndexFailure(this._dataManager)) ||
|
(nextMode && spaceMissionsIndexFailure(this._dataManager, startedAt)) ||
|
||||||
t(
|
t(
|
||||||
'Space Missions could not complete the requested transition; try again',
|
'Space Missions could not complete the requested transition; try again',
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in new issue
Block a user