diff --git a/server/README.md b/server/README.md index 250d11b..c4ac9c0 100644 --- a/server/README.md +++ b/server/README.md @@ -178,7 +178,8 @@ algorithm itself -- it just streams the response straight to the panel. - `GET /api/firmware/check` -- throttled (`gitea_releases.UPDATE_CHECK_INTERVAL_S`, 15 min) check of the configured Gitea repo's latest release for the frame's board variant (learned from the device, see `device_board_variant` - below -- not user-configured). `{"enabled": false}` if no repo URL is + below -- not user-configured). `?force=true` (the "Check now" button) + bypasses the throttle. `{"enabled": false}` if no repo URL is configured; otherwise `{"enabled": true, "board": "xiao" | null, "latest_version": "1.2.3" | null, "staged_version": "1.2.2" | null, "update_available": bool}`. `update_available` stays false until the diff --git a/server/app/main.py b/server/app/main.py index cb7ee90..b9d8931 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -529,7 +529,7 @@ def _apply_gitea_update(cfg: config.FrameConfig) -> str: @app.get("/api/firmware/check", dependencies=[Depends(require_access_token)]) -def api_firmware_check(): +def api_firmware_check(force: bool = False): """Throttled check of the configured Gitea repo's latest release (gitea_releases.UPDATE_CHECK_INTERVAL_S) -- cheap, since it only reads the release's tag name, not its binaries. If firmware_auto_update is @@ -538,13 +538,18 @@ def api_firmware_check(): Applying (auto or manual) needs to know the frame's board, which is learned from the device's own X-Frame-Board header rather than picked by the user -- update_available stays false until a device - has checked in at least once, regardless of what Gitea has.""" + has checked in at least once, regardless of what Gitea has. + + force=true (the "Check now" button) bypasses the throttle and always + hits Gitea -- otherwise a genuinely new release can sit invisible in + the UI for up to the full throttle interval even though it's already + live, since the passive poll won't look again until then.""" cfg = config.load() if not cfg.firmware_update_repo_url: return {"enabled": False} now = time.time() - if now - cfg.firmware_update_checked_at >= gitea_releases.UPDATE_CHECK_INTERVAL_S: + if force or now - cfg.firmware_update_checked_at >= gitea_releases.UPDATE_CHECK_INTERVAL_S: # Deliberately not updated on failure (see below) -- checked_at only # advances on a successful reach, so a Gitea outage gets retried # every poll instead of waiting out the full throttle interval. diff --git a/server/app/templates/index.html b/server/app/templates/index.html index 0fe9d7a..d33e99b 100644 --- a/server/app/templates/index.html +++ b/server/app/templates/index.html @@ -131,6 +131,7 @@ + @@ -541,13 +542,16 @@ } }); - async function loadFirmwareCheck() { + async function loadFirmwareCheck(force) { const statusEl = document.getElementById('firmware-gitea-status'); const btn = document.getElementById('firmware-update-btn'); const boardEl = document.getElementById('firmware-board'); try { - const resp = await fetch('/api/firmware/check'); + const resp = await fetch('/api/firmware/check' + (force ? '?force=true' : '')); if (!resp.ok) { + if (force) { + showStatus(false, await resp.text()); + } return; } const data = await resp.json(); @@ -557,6 +561,9 @@ if (!data.enabled) { statusEl.style.display = 'none'; btn.style.display = 'none'; + if (force) { + showStatus(false, 'No Gitea repo URL configured.'); + } return; } statusEl.style.display = 'block'; @@ -573,12 +580,21 @@ statusEl.textContent = 'No releases found yet.'; btn.style.display = 'none'; } + if (force) { + showStatus(true, 'Checked.'); + } } catch (e) { - // A failed check is silent -- the manual upload path still works - // regardless, and this just retries on the next poll. + // A failed passive poll is silent -- the manual upload path still + // works regardless, and this just retries on the next poll. An + // explicit "Check now" click still surfaces the error. + if (force) { + showStatus(false, e.message); + } } } + document.getElementById('firmware-check-now').addEventListener('click', () => loadFirmwareCheck(true)); + document.getElementById('firmware-update-btn').addEventListener('click', async () => { const btn = document.getElementById('firmware-update-btn'); btn.disabled = true;