diff --git a/changelog.d/1578-space-missions-failed-entry.md b/changelog.d/1578-space-missions-failed-entry.md index 9ad1630db..5d8d67735 100644 --- a/changelog.d/1578-space-missions-failed-entry.md +++ b/changelog.d/1578-space-missions-failed-entry.md @@ -1,5 +1,4 @@ - Space Missions no longer locks the workspace when its mission index fails to - load. A 429, timeout or bad payload on `/api/launches` now rolls the entry - back to the previous layers and camera view instead of leaving an empty, - isolating mode, and the roster reads `30-DAY MISSION INDEX UNAVAILABLE` - rather than loading forever (German, @gekh, #1578). + load (429, timeout or bad payload): entry rolls back to the previous layers + and camera view and a toast says the index could not load (German, @gekh, + #1578). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c261c6205..76deeb9a0 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5336,7 +5336,7 @@ silently demoting every later lookup for the session. - Context is set for reading in every theme: mode, standby and action labels are 10 px, and the panel draws no glow halo. Outside Cyber, the two modes form one segmented control, the standby guidance is plain left-aligned text rather than a dashed placeholder, and the panel's section titles share the side panels' dock-label colour. Cyber keeps its separately framed mode buttons, its red panel titles, and in layout-focus a framed, centred standby card over its radar art. - The expanded desktop header omits the redundant `ON` label; the active mode button carries state. Expanded Contact results also omit the duplicate `GLOBAL CONTEXT` / `CONTEXT ONLY` status row and begin with the selected subject and its 250 km scope. Global Context does not fabricate a selected-entity model preview: the provisional hand-authored aircraft wireframe was removed because it was not geometry extracted from the selected entity's actual asset. - Dependency ownership is reversible: disabling Global Context releases only dependencies it enabled, while user-enabled layers remain on. This also removes the Military-layer suppression handoff when Global Context owned Military, allowing an already-enabled civilian Flights layer to resume its normal mixed rendering. If OpenSky is unavailable and has no last-good cache, Flights requests a capped 250 nm adsb.lol point snapshot around the current view anchor and labels that provenance explicitly; it never relabels military-feed rows as civilian data. If both inputs fail, Flights remains `UNKNOWN`. -- Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. That enable includes the first mission-index load: an HTTP error, timeout, network failure or malformed payload before any index has loaded fails the enable, so Context rolls back to the pre-entry layers instead of committing an empty, isolating mode; the camera returns to its pre-entry view unless it moved after the entry flight. A valid empty index still enters, and a refresh failure after an index loaded keeps its records and the mode. The roster reads `LOADING 30-DAY MISSION INDEX` while that first load is pending and `30-DAY MISSION INDEX UNAVAILABLE` once it failed. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally. +- Space Missions is replay-isolated: Rocket Launches and Satellites are the only Data Layers permitted while the mode is active. Direct UI and voice entry capture the same pre-entry snapshot; internal dependency and restoration enables do not create a user-owned Context session. Entry waits for incompatible layers to shut down, direct incompatible enables are blocked before lifecycle work, and the entry gate remains active through the complete Rocket Launches enable. That enable includes the first mission-index load: an HTTP error, timeout, network failure or malformed payload before any index has loaded fails the enable, so Context rolls back to the pre-entry layers instead of committing an empty, isolating mode; the camera returns to its pre-entry view unless navigation took it, and a toast says the mission index could not load. A valid empty index still enters, and a refresh failure after an index loaded keeps its records and the mode. Exiting cancels the layer's own camera flights but never one navigation started. A newer same-target ON request takes ownership of the pending entry without releasing its isolation snapshot, including when it arrives while the prior request is awaiting the adoption guard. A caller abort, resource cancellation, newer OFF, or layer teardown waits for exact manager settlement and restores that snapshot without resurrecting Rocket Launches. If an abort lands after only part of a restore settles, Context completes the same exact target without the stale caller signal and then replays newer explicit layer intent. Dedicated Voice Context cancellation reports a stable cancelled result plus the current Context state; generic layer visibility additionally exposes manager phase, reason, successor, and lifecycle details. Once an exact voice visibility intent commits, a newer voice turn cannot relabel it as cancelled while Context settlement completes; pre-commit aborts remain cancellable and final lifecycle mismatches remain failures. Clear Selected Layers reserves its complete captured OFF set before sequential teardown; a newer absolute request of any origin supersedes only its layer reservation and remains authoritative. A rejected layer teardown retains the truthful enabled state, rolls already-stopped siblings back to the captured pre-entry set, and aborts replay; rapid exit/re-entry serializes the full Satellite enabled-state and parameter restore before a new snapshot is taken. Contacts remains additive and restores user-enabled layers normally. - Enabled Data Layer controls report normalized feed health on the button (`LOADING`, `DEGRADED`, `STALE`, `FALLBACK`, or `UNAVAILABLE`) while the metadata line retains the source and reason. A partial CelesTrak group failure keeps the usable catalog and reports `DEGRADED`; a total outage keeps last-good catalog data visible but reports `UNAVAILABLE`. - On activation it focuses the nearest currently observed aircraft across the civilian and military feeds, with military winning an exact distance tie; if none are available, it focuses an observed AIS vessel. The aircraft search is deliberately uncapped and one refresh-tick retry handles initially empty feeds. This is an attention-priority navigation shortcut, not a high-risk, affiliation, or threat classification. - A selected aircraft, AIS vessel, or mapped installation gets a 250 km **context window** with nearby cohort counts, nearest examples, source labels, and stale/unavailable reasons. It emits `NEARBY` or `UNKNOWN`; no detection, engagement, affiliation, or sensor-activity conclusion is calculated. diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 10d7ab268..1cd3834d4 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -301,6 +301,7 @@ "AVAILABLE MISSIONS": "المهام المتاحة", "SELECT A MISSION TO INSPECT": "اختر مهمة لفحصها", "LOADING 30-DAY MISSION INDEX": "جارٍ تحميل فهرس مهام 30 يومًا", + "Space Missions unavailable: the mission index could not load. Try again later.": "المهام الفضائية غير متاحة: تعذّر تحميل فهرس المهام. حاول مرة أخرى لاحقًا.", "30-DAY MISSION INDEX UNAVAILABLE": "فهرس مهام 30 يومًا غير متاح", "RADIO": "الراديو", "ENABLE": "تفعيل", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index ca4df37f7..d7e73583e 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -760,6 +760,7 @@ "AVAILABLE MISSIONS": "VERFÜGBARE MISSIONEN", "SELECT A MISSION TO INSPECT": "MISSION ZUM ANSEHEN WÄHLEN", "LOADING 30-DAY MISSION INDEX": "30-TAGE-MISSIONSINDEX WIRD GELADEN", + "Space Missions unavailable: the mission index could not load. Try again later.": "Raummissionen nicht verfügbar: Der Missionsindex konnte nicht geladen werden. Später erneut versuchen.", "30-DAY MISSION INDEX UNAVAILABLE": "30-TAGE-MISSIONSINDEX NICHT VERFÜGBAR", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB: VORSCHAU · ENTER / LEERTASTE: AUSWAHL", "Internet radio and local SDR": "Internetradio und lokales SDR", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 5f43c2a35..a9ce5da9f 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -192,6 +192,7 @@ "AVAILABLE MISSIONS": "MISIONES DISPONIBLES", "SELECT A MISSION TO INSPECT": "SELECCIONA UNA MISIÓN PARA INSPECCIONAR", "LOADING 30-DAY MISSION INDEX": "CARGANDO ÍNDICE DE MISIONES DE 30 DÍAS", + "Space Missions unavailable: the mission index could not load. Try again later.": "Misiones espaciales no disponibles: no se pudo cargar el índice de misiones. Inténtalo más tarde.", "30-DAY MISSION INDEX UNAVAILABLE": "ÍNDICE DE MISIONES DE 30 DÍAS NO DISPONIBLE", "RADIO": "RADIO", "ENABLE": "ACTIVAR", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index a610754ed..2d64b3e2a 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -2805,6 +2805,7 @@ "AVAILABLE MISSIONS": "MISSIONS DISPONIBLES", "SELECT A MISSION TO INSPECT": "CHOISIR UNE MISSION À EXAMINER", "LOADING 30-DAY MISSION INDEX": "CHARGEMENT DE L'INDEX DES MISSIONS À 30 JOURS", + "Space Missions unavailable: the mission index could not load. Try again later.": "Missions spatiales indisponibles : impossible de charger l’index des missions. Réessayez plus tard.", "30-DAY MISSION INDEX UNAVAILABLE": "INDEX DES MISSIONS À 30 JOURS INDISPONIBLE", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB APERÇU · ENTER / SPACE SÉLECTIONNE", "Internet radio and local SDR": "Radio Internet et SDR local", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 4ab1283fc..d349fe1e9 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -2805,6 +2805,7 @@ "AVAILABLE MISSIONS": "MISSIONI DISPONIBILI", "SELECT A MISSION TO INSPECT": "SELEZIONA UNA MISSIONE DA ESAMINARE", "LOADING 30-DAY MISSION INDEX": "CARICAMENTO INDICE MISSIONI A 30 GIORNI", + "Space Missions unavailable: the mission index could not load. Try again later.": "Missioni spaziali non disponibili: impossibile caricare l’indice delle missioni. Riprova più tardi.", "30-DAY MISSION INDEX UNAVAILABLE": "INDICE MISSIONI A 30 GIORNI NON DISPONIBILE", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB: ANTEPRIMA · ENTER / SPACE: SELEZIONA", "Internet radio and local SDR": "Radio internet e SDR locale", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index b278db058..5d0585cf7 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2663,6 +2663,7 @@ "AVAILABLE MISSIONS": "利用可能なミッション", "SELECT A MISSION TO INSPECT": "ミッションを選択して詳細を表示", "LOADING 30-DAY MISSION INDEX": "30日間のミッション一覧を読込中", + "Space Missions unavailable: the mission index could not load. Try again later.": "宇宙ミッションを利用できません:ミッション一覧を読み込めませんでした。後でもう一度お試しください。", "30-DAY MISSION INDEX UNAVAILABLE": "30日間のミッション一覧を取得できません", "TAB PREVIEWS · ENTER / SPACE SELECTS": "Tabでプレビュー · Enter / Spaceで選択", "Internet radio and local SDR": "インターネットラジオとローカルSDR", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index bd0ca7299..6ddf3238e 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -2807,6 +2807,7 @@ "AVAILABLE MISSIONS": "MISSÕES DISPONÍVEIS", "SELECT A MISSION TO INSPECT": "SELECIONE UMA MISSÃO PARA INSPECIONAR", "LOADING 30-DAY MISSION INDEX": "CARREGANDO ÍNDICE DE MISSÕES DE 30 DIAS", + "Space Missions unavailable: the mission index could not load. Try again later.": "Missões espaciais indisponíveis: não foi possível carregar o índice de missões. Tente novamente mais tarde.", "30-DAY MISSION INDEX UNAVAILABLE": "ÍNDICE DE MISSÕES DE 30 DIAS INDISPONÍVEL", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB MOSTRA PRÉVIA · ENTER / SPACE SELECIONA", "Internet radio and local SDR": "Rádio pela internet e SDR local", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index e5374f3c0..861e4245d 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -722,6 +722,7 @@ "Contact Context actions": "Действия контекста контакта", "Available Space Missions": "Доступные космические миссии", "LOADING 30-DAY MISSION INDEX": "ЗАГРУЗКА ИНДЕКСА МИССИЙ ЗА 30 ДНЕЙ", + "Space Missions unavailable: the mission index could not load. Try again later.": "Космические миссии недоступны: не удалось загрузить список миссий. Повторите попытку позже.", "30-DAY MISSION INDEX UNAVAILABLE": "ИНДЕКС МИССИЙ ЗА 30 ДНЕЙ НЕДОСТУПЕН", "Radio playback": "Воспроизведение радио", "Radio volume": "Громкость радио", diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index b94a91dd5..67efb91bb 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -2875,6 +2875,7 @@ "AVAILABLE MISSIONS": "ДОСТУПНІ МІСІЇ", "SELECT A MISSION TO INSPECT": "ВИБЕРІТЬ МІСІЮ ДЛЯ ПЕРЕГЛЯДУ", "LOADING 30-DAY MISSION INDEX": "ЗАВАНТАЖЕННЯ ІНДЕКСУ МІСІЙ ЗА 30 ДНІВ", + "Space Missions unavailable: the mission index could not load. Try again later.": "Космічні місії недоступні: не вдалося завантажити список місій. Спробуйте пізніше.", "30-DAY MISSION INDEX UNAVAILABLE": "ІНДЕКС МІСІЙ ЗА 30 ДНІВ НЕДОСТУПНИЙ", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB — ОГЛЯД · ENTER / SPACE — ВИБІР", "Internet radio and local SDR": "Інтернет-радіо та локальний SDR", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 0875775b2..8832de1fe 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2562,6 +2562,7 @@ "AVAILABLE MISSIONS": "可用任务", "SELECT A MISSION TO INSPECT": "选择一项任务以查看", "LOADING 30-DAY MISSION INDEX": "正在加载 30 天任务索引", + "Space Missions unavailable: the mission index could not load. Try again later.": "太空任务不可用:无法加载任务索引。请稍后重试。", "30-DAY MISSION INDEX UNAVAILABLE": "30 天任务索引不可用", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB 预览 · ENTER / 空格键选择", "Internet radio and local SDR": "网络电台与本地 SDR", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index c6bff6cb6..dce0b2e75 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -2550,6 +2550,7 @@ "AVAILABLE MISSIONS": "可用任務", "SELECT A MISSION TO INSPECT": "選擇一項任務以查看", "LOADING 30-DAY MISSION INDEX": "正在加載 30 天任務索引", + "Space Missions unavailable: the mission index could not load. Try again later.": "太空任務無法使用:無法載入任務索引。請稍後再試。", "30-DAY MISSION INDEX UNAVAILABLE": "30 天任務索引不可用", "TAB PREVIEWS · ENTER / SPACE SELECTS": "TAB 預覽 · ENTER / 空格鍵選擇", "Internet radio and local SDR": "網絡電臺與本地 SDR", diff --git a/src/layers/launches/controls.js b/src/layers/launches/controls.js index 019154269..d8b4c6d6a 100644 --- a/src/layers/launches/controls.js +++ b/src/layers/launches/controls.js @@ -17,6 +17,7 @@ export function createControls({ state: layerState, services, parts, source }) { parts.panel.clearMissionRosterHover(); parts.replay.stopMissionReplay(); parts.selection.stopMissionZoomAnchor(); + layerState._cameraReleased = true; }, /** diff --git a/src/layers/launches/failedEntry.test.mjs b/src/layers/launches/failedEntry.test.mjs index 7878a8e2d..52adcb792 100644 --- a/src/layers/launches/failedEntry.test.mjs +++ b/src/layers/launches/failedEntry.test.mjs @@ -134,7 +134,7 @@ async function fixture(t) { dataSources: new Cesium.DataSourceCollection(), scene: { canvas, preRender: new Cesium.Event(), drillPick: () => [] }, }; - // Only scene presentation is stubbed. + // Scene presentation is stubbed; ingestion, lifecycle and panel are real. const parts = { overlays: { shortMissionLabel: (name) => name, @@ -454,3 +454,40 @@ test('a refresh failure after a loaded index keeps the mode and its records', as await f.settle(); 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) => { + const f = await fixture(t); + f.upstream.status = 200; + assert.equal( + await f.manager.setEnabled('rocket-launches', true, { origin: 'user' }), + true, + ); + await f.settle(); + assert.ok(f.camera.flight, 'the 2.4 s entry flight is still in progress'); + + await f.manager.setEnabled('rocket-launches', false, { origin: 'user' }); + await f.settle(); + assert.equal( + f.camera.flight, + null, + 'leaving Space Missions stops the flight it started (main cancels it)', + ); +}); +// Append to src/layers/launches/failedEntry.test.mjs (uses its fixture()). +// 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) => { + const f = await fixture(t); + await f.manager.setEnabled('earthquakes', true, { origin: 'user' }); + + await f.controls.setContextMode('space-missions', { + claimVisualAuthority: false, + }); + await f.settle(); + assert.equal(f.controls.getContextModeState().mode, null, 'rolled back'); + assert.ok( + f.toasts.some((message) => /mission index/i.test(message)), + `visible feedback names the failure, got ${JSON.stringify(f.toasts)}`, + ); +}); diff --git a/src/layers/launches/policyHelpers.js b/src/layers/launches/policyHelpers.js index d4539289a..b0ec18373 100644 --- a/src/layers/launches/policyHelpers.js +++ b/src/layers/launches/policyHelpers.js @@ -229,6 +229,7 @@ export function createPolicyHelpers({ layerState._viewer.camera.positionCartographic?.height, ); const position = Cesium.Cartesian3.fromDegrees(launch.lon, launch.lat); + layerState._cameraReleased = false; layerState._viewer.camera.flyToBoundingSphere( new Cesium.BoundingSphere(position, 0), { diff --git a/src/layers/launches/replay.js b/src/layers/launches/replay.js index bf6ad84de..d2fe51bb4 100644 --- a/src/layers/launches/replay.js +++ b/src/layers/launches/replay.js @@ -314,8 +314,12 @@ export function createReplay({ state: layerState, services, parts, source }) { layerState._replayCameraToken++; if (layerState._replayCameraRemover) layerState._replayCameraRemover(); layerState._replayCameraRemover = null; - // Cancel only a replay's flight; any other flight runs on. - if (stoppedLaunchId && layerState._viewer?.camera) { + // Cancel the layer's own flights, never one navigation started after + // taking the camera. + if ( + (stoppedLaunchId || !layerState._cameraReleased) && + layerState._viewer?.camera + ) { layerState._viewer.camera.cancelFlight(); layerState._viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY); } diff --git a/src/layers/launches/selection.js b/src/layers/launches/selection.js index 0f7955551..6d7ae40cb 100644 --- a/src/layers/launches/selection.js +++ b/src/layers/launches/selection.js @@ -33,6 +33,7 @@ export function createSelection({ ); const distance = earthRadius / Math.max(Math.sin(angularRadius), 1e-4); const altitude = Math.max(earthRadius * 1.55, distance - earthRadius); + layerState._cameraReleased = false; viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromRadians( cartographic.longitude, @@ -191,6 +192,7 @@ export function createSelection({ Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()), new Cesium.Cartesian3(), ); + layerState._cameraReleased = false; layerState._viewer.camera.flyTo({ destination, orientation: { direction, up }, @@ -213,6 +215,7 @@ export function createSelection({ layerState._viewer.selectedEntity = layerState._dataSource.entities.getById( `rocket-launch:${launch.id}`, ); + layerState._cameraReleased = false; layerState._viewer.camera.flyToBoundingSphere( new Cesium.BoundingSphere(launchPosition, 0), { diff --git a/src/layers/launches/state.js b/src/layers/launches/state.js index 9ef571428..beb8d1bf7 100644 --- a/src/layers/launches/state.js +++ b/src/layers/launches/state.js @@ -23,6 +23,9 @@ export function createState({ services }) { state._entryLandedView = null; + /** Navigation took the camera; set until the layer flies again. */ + state._cameraReleased = false; + state._stale = false; state._orbitMatches = 0; diff --git a/src/ui/contextActions.js b/src/ui/contextActions.js index a621137ad..702130073 100644 --- a/src/ui/contextActions.js +++ b/src/ui/contextActions.js @@ -4,6 +4,18 @@ import { contextModeWord, } from '../contextModePolicy.js'; +/** Why a Space Missions entry failed, when its mission index never loaded. */ +export function spaceMissionsIndexFailure(dataManager) { + const stats = dataManager?.layers + ?.get('rocket-launches') + ?.module?.getStats?.(); + return stats?.error && stats.lastUpdate == null + ? t( + 'Space Missions unavailable: the mission index could not load. Try again later.', + ) + : null; +} + export async function _runUserFacingContextAction( operation, message = t('Context could not restore every layer; try again'), @@ -19,7 +31,10 @@ export async function _runUserFacingContextAction( onFailure: (error) => { if (this.destroyed) return; console.warn('[Context] user-facing transition failed', error); - this.showToast(localizedMessage || message); + this.showToast( + localizedMessage || + (typeof message === 'function' ? message() : message), + ); }, }); } finally { diff --git a/src/ui/contextBindings.js b/src/ui/contextBindings.js index cc09c49bf..c5e4fc52e 100644 --- a/src/ui/contextBindings.js +++ b/src/ui/contextBindings.js @@ -1,4 +1,5 @@ import { t } from '../i18n/index.js'; +import { spaceMissionsIndexFailure } from './contextActions.js'; import { shouldExpandGlobalContextPanel } from '../rightRailPolicy.js'; export function _initGlobalContextPanel() { @@ -62,9 +63,11 @@ export function _initGlobalContextPanel() { void this._runUserFacingContextAction( (notificationToken) => this._selectContextMode(nextMode, { notificationToken }), - t( - 'Space Missions could not complete the requested transition; try again', - ), + () => + (nextMode && spaceMissionsIndexFailure(this._dataManager)) || + t( + 'Space Missions could not complete the requested transition; try again', + ), ).then((succeeded) => { if ( !this.destroyed && diff --git a/src/ui/contextLayerChanges.js b/src/ui/contextLayerChanges.js index d4ddd6cd5..295242239 100644 --- a/src/ui/contextLayerChanges.js +++ b/src/ui/contextLayerChanges.js @@ -9,6 +9,7 @@ import { spaceMissionEntryCancellationDisposition, } from '../contextModePolicy.js'; import { localizeLayerName } from './layerNames.js'; +import { spaceMissionsIndexFailure } from './contextActions.js'; /** * A blocked-visibility reason in the UI language. Reasons stay English (voice @@ -120,10 +121,14 @@ export function _handleContextLayerChange(change) { return; } if (change?.type === 'visibility-failed') { - const failureMessage = t('{layerId} could not {action} cleanly', { - layerId: change.layerId, - action: change.enabled ? t('start') : t('stop'), - }); + const failureMessage = + (change.layerId === 'rocket-launches' && + change.enabled && + spaceMissionsIndexFailure(this._dataManager)) || + t('{layerId} could not {action} cleanly', { + layerId: change.layerId, + action: change.enabled ? t('start') : t('stop'), + }); // A failed direct Context-shell START has already had its siblings // cleared by the visibility guard. Wait outside the synchronous manager // notification for this queue to settle, then reconcile the complete