From 928a29524757e91607b5eb7668e78f3ff512b3e5 Mon Sep 17 00:00:00 2001 From: L4XB Date: Fri, 25 Sep 2026 03:12:40 +0200 Subject: [PATCH] fix(control-pane): let the newest finished load decide the board A newer refresh that failed was shown, and then an older one that succeeded cleared the message, so the board looked live although no load that started after the failed one had succeeded. The board now follows the newest load that has finished, with its data or with its failure. A load that finishes after a newer one leaves the board alone: its data is not rendered and its failure is not reported. This also keeps a late response from replacing newer data. --- scripts/lib/control-pane/ui.js | 17 +++++++++-------- tests/lib/control-pane-ui.test.js | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/scripts/lib/control-pane/ui.js b/scripts/lib/control-pane/ui.js index 93d608371..690c8d981 100644 --- a/scripts/lib/control-pane/ui.js +++ b/scripts/lib/control-pane/ui.js @@ -439,10 +439,11 @@ function renderControlPaneHtml() { // The board keeps the last snapshot on screen, so a failed refresh has to // say that the data is no longer live, and since when. let loadedAt = null; - // Loads are numbered as they start, so a failed refresh can tell whether - // the board already shows data from a load that started after it. + // Loads are numbered as they start. The board follows the newest load that + // has finished, with its data or with its failure, so a load that finishes + // after a newer one leaves the board alone. let loadsStarted = 0; - let shownLoad = 0; + let newestFinished = 0; function showRefreshFailure(error) { const since = loadedAt ? ' The data below is from ' + loadedAt.toLocaleString() + '.' @@ -644,6 +645,8 @@ function renderControlPaneHtml() { if (state.query) url.searchParams.set('query', state.query); const response = await fetch(url); const snapshot = await readJsonResponse(response); + if (id < newestFinished) return; + newestFinished = id; $('#query').value = snapshot.knowledge.query || state.query; $('#db-path').textContent = snapshot.database.exists ? snapshot.dbPath : 'database missing'; state.allowActions = Boolean(snapshot.execution.allowActions); @@ -658,7 +661,6 @@ function renderControlPaneHtml() { executable: snapshot.execution.allowActions && action.executable }))); loadedAt = new Date(); - shownLoad = id; clearError('#app'); } @@ -703,10 +705,9 @@ function renderControlPaneHtml() { // load() numbers itself before its first await. const id = loadsStarted; refresh.catch(error => { - // Data from a load that started later is already on the board, so - // this failure no longer describes it. An older load that succeeded - // in the meantime does not make the board live. - if (shownLoad < id) showRefreshFailure(error); + if (id < newestFinished) return; + newestFinished = id; + showRefreshFailure(error); }); }, 15000); diff --git a/tests/lib/control-pane-ui.test.js b/tests/lib/control-pane-ui.test.js index d7f8a50b3..14809b96b 100644 --- a/tests/lib/control-pane-ui.test.js +++ b/tests/lib/control-pane-ui.test.js @@ -180,6 +180,26 @@ async function runTests() { passed++; else failed++; + if ( + await test('an older refresh that succeeds after a newer one failed leaves the failure up', async () => { + const page = openPage(snapshot); + await settle(); + + page.hold = true; + page.refresh(); + page.refresh(); + page.pending[1].fail(); + await settle(); + page.pending[0].succeed(); + await settle(); + const box = page.element('#app'); + assert.strictEqual(box.hidden, false, 'no load that started after the failed one has succeeded'); + assert.match(box.textContent, /Live refresh failed\. The data below is from /); + }) + ) + passed++; + else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }