From 684225422cad5f1e9589b31e7c454a2fb45b68cc Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Mon, 27 Jul 2026 22:55:40 +0000 Subject: [PATCH] Distinguish "staged, not yet applied" from "up to date" in firmware check update_available only compared the latest Gitea release against what's staged, not what the frame is actually running -- so once a release was staged (manually or via auto-update) but the frame hadn't woken up and applied it yet, "Check now" reported "Up to date" even though the device was still on the old version. Report the frame's actual running version and use it to show a distinct "staged, applies on next wake" message instead. --- server/app/routers/api_frames.py | 1 + server/app/static/frame_config.js | 9 ++- server/tests/test_firmware_check.py | 89 +++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 server/tests/test_firmware_check.py diff --git a/server/app/routers/api_frames.py b/server/app/routers/api_frames.py index afe8455..396dbad 100644 --- a/server/app/routers/api_frames.py +++ b/server/app/routers/api_frames.py @@ -384,6 +384,7 @@ def api_firmware_check( "board": frame.device_board_variant or None, "latest_version": frame.firmware_gitea_latest_version or None, "staged_version": frame.firmware_available_version or None, + "running_version": frame.device_firmware_version or None, "update_available": update_available, } diff --git a/server/app/static/frame_config.js b/server/app/static/frame_config.js index c60e052..9ec0589 100644 --- a/server/app/static/frame_config.js +++ b/server/app/static/frame_config.js @@ -339,8 +339,15 @@ async function loadFirmwareCheck(force) { statusEl.textContent = 'Waiting for the frame to check in before it can look up the right build.'; btn.style.display = 'none'; } else if (data.update_available) { - statusEl.textContent = `Update available: v${data.latest_version}.`; + statusEl.textContent = `Update available: v${data.latest_version}` + + (data.running_version ? ` (currently running v${data.running_version}).` : '.'); btn.style.display = 'inline-block'; + } else if (data.latest_version && data.running_version && data.running_version !== data.latest_version) { + // Already staged (or auto-applied) but the frame hasn't woken up + // and picked it up yet -- not "up to date" until it actually has. + statusEl.textContent = `v${data.latest_version} staged -- applies next time the frame wakes ` + + `(currently running v${data.running_version}).`; + btn.style.display = 'none'; } else if (data.latest_version) { statusEl.textContent = `Up to date (v${data.latest_version}).`; btn.style.display = 'none'; diff --git a/server/tests/test_firmware_check.py b/server/tests/test_firmware_check.py new file mode 100644 index 0000000..0ec47a7 --- /dev/null +++ b/server/tests/test_firmware_check.py @@ -0,0 +1,89 @@ +"""POST /api/frames/{id}/firmware/check -- the "Check now" button's +endpoint. update_available compares the latest Gitea release against +what's *staged* (firmware_available_version), not what the device is +actually running (device_firmware_version) -- those can differ once a +release has been staged/auto-applied but the frame hasn't woken up and +picked it up yet. running_version lets the UI tell "up to date" apart +from "staged, waiting for the frame to apply it" instead of collapsing +both into the same message.""" + +from __future__ import annotations + +from app import gitea_releases +from app.models import Frame + +from .conftest import csrf_headers + + +def _setup_frame(db_session, monkeypatch, latest_version, **overrides): + """firmware_update_checked_at starts at 0, so the endpoint always + tries a real Gitea fetch on a fresh frame regardless of force= -- + stub it out rather than hitting the network.""" + monkeypatch.setattr( + gitea_releases, "fetch_latest_release", lambda *a, **k: {"version": latest_version, "assets": {}} + ) + frame = db_session.get(Frame, 1) + frame.firmware_update_repo_url = "https://git.example.com/owner/repo" + frame.device_board_variant = "devkit" + for key, value in overrides.items(): + setattr(frame, key, value) + db_session.commit() + return frame + + +def test_up_to_date_when_running_matches_latest(client, db_session, monkeypatch): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + _setup_frame( + db_session, + monkeypatch, + "1.4.1", + firmware_available_version="1.4.1", + device_firmware_version="1.4.1", + ) + + resp = client.post("/api/frames/1/firmware/check", headers=csrf_headers(client)) + assert resp.status_code == 200 + data = resp.json() + assert data["update_available"] is False + assert data["latest_version"] == "1.4.1" + assert data["running_version"] == "1.4.1" + + +def test_staged_but_not_yet_running_is_not_update_available(client, db_session, monkeypatch): + """A release already staged (e.g. by a previous auto-update) but not + yet applied by the device isn't "an update is available" -- there's + nothing left to fetch/stage -- but it also isn't silently "up to + date" from the UI's perspective, since running_version still lags.""" + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + _setup_frame( + db_session, + monkeypatch, + "1.4.1", + firmware_available_version="1.4.1", + device_firmware_version="1.3.0", + ) + + resp = client.post("/api/frames/1/firmware/check", headers=csrf_headers(client)) + assert resp.status_code == 200 + data = resp.json() + assert data["update_available"] is False + assert data["latest_version"] == "1.4.1" + assert data["staged_version"] == "1.4.1" + assert data["running_version"] == "1.3.0" + + +def test_update_available_reports_running_version(client, db_session, monkeypatch): + client.post("/setup", data={"username": "alice", "password": "hunter22"}) + _setup_frame( + db_session, + monkeypatch, + "1.4.1", + firmware_available_version="1.3.0", + device_firmware_version="1.3.0", + ) + + resp = client.post("/api/frames/1/firmware/check", headers=csrf_headers(client)) + assert resp.status_code == 200 + data = resp.json() + assert data["update_available"] is True + assert data["running_version"] == "1.3.0"