From a5de3d7622d1bc6cc11dc0a0ce65773f2fe8dd6a Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Fri, 17 Apr 2026 09:22:34 +0200 Subject: [PATCH] fix(settings): close the two self-update UX gaps from 2026-04-16 VM test Drive upd-current from the /api/furtka/update/check response so a post-update Check reflects the new installed version without Ctrl+F5, and arm a 45s fallback location.reload on apply-click so the page still comes up on the new version when the mid-apply API restart drops the /update-state.json poll before stage=done is observed. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 6 +++--- assets/www/settings/index.html | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64e7d0..bfca0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ## [Unreleased] -### Known UX gaps (to fix in the next release) +### Fixed -- **Settings page "Installed" field doesn't refresh right after a self-update.** After `/settings` → Update now → success, the browser's `refresh()` against `/status.json` reads a page-load snapshot rather than the new value. A force-reload (Ctrl+F5) shows the correct version. Fix idea: have the update-check endpoint response also drive `upd-current` (we already set `upd-latest` from it). -- **Auto-reload on update completion is unreliable.** The JS polls `/update-state.json` and calls `location.reload()` 5s after seeing `stage: done`. On the 2026-04-16 VM test the browser never auto-reloaded — user reloaded manually. Probable cause: the API restart mid-apply drops the polling connection between the browser and the page before the done state is observed. Fix idea: fallback `setTimeout(reload, 45_000)` on apply-click regardless of poll outcome. +- **Settings page "Installed" field now refreshes after a self-update.** The `/api/furtka/update/check` response already carries `current` — the settings JS now drives `upd-current` from it the same way it drives `upd-latest`, so clicking "Check for updates" after a successful update reflects the new installed version without a force-reload. +- **Auto-reload on update completion is now reliable.** Clicking "Update now" arms a 45 s fallback `setTimeout(location.reload)` in addition to the existing `/update-state.json` polling loop. If the mid-apply API restart drops the poll connection before `stage: done` is ever observed (as seen on the 2026-04-16 VM test), the fallback still brings the page up on the new version. The fallback is cleared on `done` (5 s reload wins) or `rolled_back` (user needs the error visible). ## [26.3-alpha] - 2026-04-16 diff --git a/assets/www/settings/index.html b/assets/www/settings/index.html index 1bb7ba3..e8d93c5 100644 --- a/assets/www/settings/index.html +++ b/assets/www/settings/index.html @@ -113,6 +113,7 @@ }; let pollHandle = null; + let fallbackReloadHandle = null; const statusEl = document.getElementById('update-status'); const checkBtn = document.getElementById('check-updates-btn'); const applyBtn = document.getElementById('apply-update-btn'); @@ -135,6 +136,7 @@ return; } document.getElementById('upd-latest').textContent = data.latest || '—'; + document.getElementById('upd-current').textContent = data.current || '—'; if (data.update_available) { applyBtn.hidden = false; applyBtn.textContent = `Update to ${data.latest}`; @@ -169,6 +171,10 @@ // Poll /update-state.json (served by Caddy, unaffected by the // API restart the updater is about to trigger) every 2s. pollHandle = setInterval(pollUpdateState, 2000); + // Fallback: reload regardless of whether polling observes 'done'. + // The mid-apply API restart can drop the poll connection before + // the terminal state is ever seen by this page. + fallbackReloadHandle = setTimeout(() => location.reload(), 45000); } catch (e) { setStatus(`Network error: ${e.message}`, true); applyBtn.disabled = false; @@ -185,9 +191,11 @@ setStatus(label, s.stage === 'rolled_back'); if (s.stage === 'done') { clearInterval(pollHandle); + clearTimeout(fallbackReloadHandle); setTimeout(() => location.reload(), 5000); } else if (s.stage === 'rolled_back') { clearInterval(pollHandle); + clearTimeout(fallbackReloadHandle); if (s.reason) { setStatus(`${label} — ${s.reason}`, true); }