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) <noreply@anthropic.com>
This commit is contained in:
Daniel Maksymilian Syrnicki 2026-04-17 09:22:34 +02:00
parent bf86ffaf4c
commit a5de3d7622
2 changed files with 11 additions and 3 deletions

View file

@ -7,10 +7,10 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r
## [Unreleased] ## [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). - **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 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. - **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 ## [26.3-alpha] - 2026-04-16

View file

@ -113,6 +113,7 @@
}; };
let pollHandle = null; let pollHandle = null;
let fallbackReloadHandle = null;
const statusEl = document.getElementById('update-status'); const statusEl = document.getElementById('update-status');
const checkBtn = document.getElementById('check-updates-btn'); const checkBtn = document.getElementById('check-updates-btn');
const applyBtn = document.getElementById('apply-update-btn'); const applyBtn = document.getElementById('apply-update-btn');
@ -135,6 +136,7 @@
return; return;
} }
document.getElementById('upd-latest').textContent = data.latest || '—'; document.getElementById('upd-latest').textContent = data.latest || '—';
document.getElementById('upd-current').textContent = data.current || '—';
if (data.update_available) { if (data.update_available) {
applyBtn.hidden = false; applyBtn.hidden = false;
applyBtn.textContent = `Update to ${data.latest}`; applyBtn.textContent = `Update to ${data.latest}`;
@ -169,6 +171,10 @@
// Poll /update-state.json (served by Caddy, unaffected by the // Poll /update-state.json (served by Caddy, unaffected by the
// API restart the updater is about to trigger) every 2s. // API restart the updater is about to trigger) every 2s.
pollHandle = setInterval(pollUpdateState, 2000); 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) { } catch (e) {
setStatus(`Network error: ${e.message}`, true); setStatus(`Network error: ${e.message}`, true);
applyBtn.disabled = false; applyBtn.disabled = false;
@ -185,9 +191,11 @@
setStatus(label, s.stage === 'rolled_back'); setStatus(label, s.stage === 'rolled_back');
if (s.stage === 'done') { if (s.stage === 'done') {
clearInterval(pollHandle); clearInterval(pollHandle);
clearTimeout(fallbackReloadHandle);
setTimeout(() => location.reload(), 5000); setTimeout(() => location.reload(), 5000);
} else if (s.stage === 'rolled_back') { } else if (s.stage === 'rolled_back') {
clearInterval(pollHandle); clearInterval(pollHandle);
clearTimeout(fallbackReloadHandle);
if (s.reason) { if (s.reason) {
setStatus(`${label} — ${s.reason}`, true); setStatus(`${label} — ${s.reason}`, true);
} }