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"