From 9f1a0611fa404a6eecc4fb75c81c76f697521086 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 23:27:47 +0400 Subject: [PATCH] fix(ui): the "kept in the link" notice is readable on Navara MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Navara the engine's persistent notice came up right after the share restore's NOT ON THIS ENGINE notice and replaced it at once, so the names of the linked layers this engine lacks were never seen (and a later finite notice would in turn have wiped the engine notice for good). - ShellFeedback keeps a persistent notice that a finite one covers and brings it back when the finite one ends; a persistent notice that arrives under a live finite one waits for it. - Notices take a dwellMs; "Kept in the link: …" stays 15 s. - scripts/qa-engine-notice.mjs (headless): a Cesium share link with a layer Navara lacks, opened with ?engine=navara: notice shown with the layer name, still up 14 s later, then the engine notice is back. 4/4 (before the fix the notice never appeared). T8-T9 review item 2. --- scripts/format-scope.json | 3 +- src/loadingFeedback.js | 8 +++- src/ui/shareRestoration.js | 6 +++ src/ui/shellFeedback.js | 22 ++++++++++- src/ui/shellLifecycle.test.mjs | 69 ++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 5 deletions(-) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 41856b54e..b256e624d 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1109,5 +1109,6 @@ "src/app/layers/qaSources.test.mjs", "scripts/qa-w1-layers.mjs", "src/bench/w1Feeds.js", - "scripts/qa-map-switch.mjs" + "scripts/qa-map-switch.mjs", + "scripts/qa-engine-notice.mjs" ] diff --git a/src/loadingFeedback.js b/src/loadingFeedback.js index df7766019..2d1db3053 100644 --- a/src/loadingFeedback.js +++ b/src/loadingFeedback.js @@ -130,7 +130,7 @@ export function createLoadingFeedbackState() { export function createGlobalStatusNotice( message, nowMs = 0, - { state = 'error', detail = '', persistent = false } = {}, + { state = 'error', detail = '', persistent = false, dwellMs } = {}, ) { const label = String(message || '').trim(); if (!label) return null; @@ -139,7 +139,11 @@ export function createGlobalStatusNotice( label, detail: String(detail || '').trim(), persistent: !!persistent, - dwellMs: persistent ? null : LOADING_FAILURE_DWELL_MS, + dwellMs: persistent + ? null + : Number.isFinite(dwellMs) && dwellMs > 0 + ? dwellMs + : LOADING_FAILURE_DWELL_MS, // A finite notice starts its dwell only when it first wins presentation. // Otherwise a higher-priority manager failure could consume the whole // deadline while this notice remained queued and invisible. diff --git a/src/ui/shareRestoration.js b/src/ui/shareRestoration.js index c7bd64cf6..dd64b6dcf 100644 --- a/src/ui/shareRestoration.js +++ b/src/ui/shareRestoration.js @@ -1,6 +1,9 @@ import { LayerStateCoordinator } from '../data/layerStateCoordinator.js'; import { APPLICATION_LAYER_DIRECTORY } from '../app/layers/directory.js'; +/** How long the "kept in the link" notice stays up (ms). */ +export const NOT_ON_THIS_ENGINE_DWELL_MS = 15_000; + /** Names of the layers a link turned on that this engine does not have. */ export function layersNotOnThisEngine(results) { return results @@ -189,6 +192,9 @@ export class ShareRestoration { this.showStatus('NOT ON THIS ENGINE', { state: 'retry', detail: `Kept in the link: ${names.join(', ')}`, + // Long enough to read a list of layer names; the engine's own + // persistent notice comes back afterwards (ShellFeedback). + dwellMs: NOT_ON_THIS_ENGINE_DWELL_MS, }); } cancelSelection() { diff --git a/src/ui/shellFeedback.js b/src/ui/shellFeedback.js index 359e885d1..27eff1ed5 100644 --- a/src/ui/shellFeedback.js +++ b/src/ui/shellFeedback.js @@ -17,6 +17,9 @@ export class ShellFeedback { this._trafficSyncFeedbackState = createTrafficSyncFeedbackState(); this._loadingFeedbackEvent = null; this._globalStatusNotice = null; + // A persistent notice (the Navara engine notice) that a finite one + // covers for its dwell; it comes back when the finite one ends. + this._coveredPersistentNotice = null; this._loadingFeedbackTicker = this._trafficChipTicker = this._toastTimer = @@ -111,7 +114,8 @@ export class ShellFeedback { Number.isFinite(this._globalStatusNotice?.hideAt) && now >= this._globalStatusNotice.hideAt ) { - this._globalStatusNotice = null; + this._globalStatusNotice = this._coveredPersistentNotice; + this._coveredPersistentNotice = null; } // Loading phases and universal notices both have time-driven transitions. // Compute this after arbitration: a queued finite notice starts its dwell @@ -138,7 +142,21 @@ export class ShellFeedback { _showGlobalStatusNotice(message, options = {}) { if (this.destroyed) return; const now = performance.now(); - this._globalStatusNotice = createGlobalStatusNotice(message, now, options); + const next = createGlobalStatusNotice(message, now, options); + const current = this._globalStatusNotice; + if (next?.persistent && current && !current.persistent) { + // A persistent notice arriving under a live finite one (the engine + // notice after a share link's "kept in the link") waits for it. + this._coveredPersistentNotice = next; + this._updateGlobalLoadingFeedback(now); + return; + } + if (next && !next.persistent) { + if (current?.persistent) this._coveredPersistentNotice = current; + } else { + this._coveredPersistentNotice = null; + } + this._globalStatusNotice = next; this._updateGlobalLoadingFeedback(now); } diff --git a/src/ui/shellLifecycle.test.mjs b/src/ui/shellLifecycle.test.mjs index 7d48ffb06..5e6af5ae6 100644 --- a/src/ui/shellLifecycle.test.mjs +++ b/src/ui/shellLifecycle.test.mjs @@ -404,3 +404,72 @@ test('busy traffic completion is polled promptly without accelerating idle or gl f.restore(); } }); + +test('a finite notice covers the persistent engine notice only for its dwell', () => { + const f = fixture(); + const realNow = performance.now; + let now = 1_000; + performance.now = () => now; + try { + const owner = new ShellFeedback({ readLayers: () => [] }); + const detail = () => f.nodes.get('global-loading-detail').textContent; + owner._showGlobalStatusNotice('Experimental Navara engine', { + state: 'retry', + detail: 'No data layers on this engine yet', + persistent: true, + }); + assert.equal(detail(), 'No data layers on this engine yet'); + owner._showGlobalStatusNotice('NOT ON THIS ENGINE', { + state: 'retry', + detail: 'Kept in the link: CCTV', + dwellMs: 15_000, + }); + assert.equal(detail(), 'Kept in the link: CCTV'); + now += 14_000; + owner._updateGlobalLoadingFeedback(now); + assert.equal(detail(), 'Kept in the link: CCTV', 'still up after 14 s'); + now += 2_000; + owner._updateGlobalLoadingFeedback(now); + owner._updateGlobalLoadingFeedback(now); + assert.equal( + detail(), + 'No data layers on this engine yet', + 'the engine notice comes back', + ); + assert.equal(owner._globalStatusNotice?.persistent, true); + owner.destroy(); + } finally { + performance.now = realNow; + f.restore(); + } +}); + +test('a persistent notice that arrives under a finite one waits for it', () => { + const f = fixture(); + const realNow = performance.now; + let now = 1_000; + performance.now = () => now; + try { + const owner = new ShellFeedback({ readLayers: () => [] }); + const detail = () => f.nodes.get('global-loading-detail').textContent; + owner._showGlobalStatusNotice('NOT ON THIS ENGINE', { + state: 'retry', + detail: 'Kept in the link: Earthquakes', + dwellMs: 15_000, + }); + owner._showGlobalStatusNotice('Experimental Navara engine', { + state: 'retry', + detail: 'No data layers on this engine yet', + persistent: true, + }); + assert.equal(detail(), 'Kept in the link: Earthquakes'); + now += 16_000; + owner._updateGlobalLoadingFeedback(now); + owner._updateGlobalLoadingFeedback(now); + assert.equal(detail(), 'No data layers on this engine yet'); + owner.destroy(); + } finally { + performance.now = realNow; + f.restore(); + } +});