Skip to content

Commit f678a81

Browse files
tom-cloydclaude
andcommitted
Fix: a newer release could stay hidden behind one already shown that day
_should_show_update_notice() tracked only a last-shown date, so once the banner had shown once for the daily/weekly/monthly window, any later release published that same period stayed hidden until the window reset — even though it was a genuinely different, newer release than the one already seen. Found via a live report: a Spin5 install on 2157 didn't surface a same-day 2306 release because 2157 had already been shown once that day. Now the last-shown *tag* is tracked alongside the date, and a release whose tag doesn't match what was last shown always gets through immediately, regardless of the frequency window — the setting only throttles repeat notices about a release you've already seen. Also gives the Update Now button a downloading spinner, since the confirm-then-nothing-visible gap during the real download made it look like the click hadn't registered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d3f6085 commit f678a81

5 files changed

Lines changed: 83 additions & 9 deletions

File tree

tests/test_web.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,38 @@ def test_home_page_shows_update_available_banner(client: TestClient, monkeypatch
19281928
assert "Update now" not in resp.text
19291929

19301930

1931+
def test_update_notice_frequency_gate_lets_a_newer_release_through(
1932+
client: TestClient, monkeypatch: pytest.MonkeyPatch,
1933+
) -> None:
1934+
"""The daily/weekly/monthly notification frequency throttles repeat
1935+
notices about the *same* release, not whether a *different, newer*
1936+
release that showed up since gets shown — otherwise the default daily
1937+
setting would hide a second same-day release until the next day."""
1938+
from numa_app.services import update_check as _update_check
1939+
1940+
monkeypatch.setattr(
1941+
_update_check, "check_for_update",
1942+
lambda *a, **kw: {"tag": "v2099-01-01-0000", "url": "https://example.invalid/v2099-01-01-0000"},
1943+
)
1944+
resp = client.get("/")
1945+
assert "UPDATE AVAILABLE:" in resp.text
1946+
1947+
# Same release again on a same-day reload: suppressed — daily allows
1948+
# only one notice per day for a given release.
1949+
resp = client.get("/")
1950+
assert "UPDATE AVAILABLE:" not in resp.text
1951+
1952+
# A newer release than the one already shown today must get through
1953+
# immediately, not wait for tomorrow's daily window.
1954+
monkeypatch.setattr(
1955+
_update_check, "check_for_update",
1956+
lambda *a, **kw: {"tag": "v2099-02-02-0000", "url": "https://example.invalid/v2099-02-02-0000"},
1957+
)
1958+
resp = client.get("/")
1959+
assert "UPDATE AVAILABLE:" in resp.text
1960+
assert "v2099-02-02-0000" in resp.text
1961+
1962+
19311963
def test_update_now_button_shown_only_for_packaged_install(client: TestClient, monkeypatch: pytest.MonkeyPatch) -> None:
19321964
from numa_app.services import update_check as _update_check
19331965
from numa_app.services import self_update as _self_update

user-manual.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# NutriMagnus User Manual
22

3-
*Updated 2026-08-31:2157* / Reading time: 4 hours, 25 minutes
3+
*Updated 2026-08-31:2330* / Reading time: 4 hours, 26 minutes
44

55
*Last full audit: 2026-08-30*
66

@@ -2605,6 +2605,24 @@ Each entry below has a bold title and a plain-language description — anywhere
26052605

26062606
#### August 31 program updates
26072607

2608+
**FIX: A NEWER RELEASE COULD STAY HIDDEN BEHIND ONE ALREADY SEEN THAT DAY**
2609+
2610+
The daily/weekly/monthly notification-frequency setting was meant to throttle repeat notices about the *same* release, not hide a *different, newer* one that showed up later the same day — but that's what it was doing: once the banner had shown once today, a second release published an hour later wouldn't surface until tomorrow. It now always shows a release you haven't been told about yet, regardless of the frequency window; the frequency setting only limits repeat notices about a release you've already seen.
2611+
2612+
**THE UPDATE NOW BUTTON NOW SHOWS A DOWNLOADING SPINNER**
2613+
2614+
Clicking Update Now used to leave the button sitting there with no feedback while the new binary downloaded in the background — easy to mistake for nothing having happened, especially on a slower connection. It now disables itself and shows a spinner with "Downloading…" the moment you confirm, so it's clear the update is actually in progress.
2615+
2616+
```
2617+
Scope: web/backend.py (_should_show_update_notice() now takes the candidate
2618+
release's tag and only applies the frequency window when it matches the tag
2619+
last shown — a new prefs.json key, update_notice_last_shown_tag, tracks
2620+
this). web/templates/home.html (Update Now form's confirm handler moved out
2621+
of an inline onsubmit into a proper <script> block, disables the button and
2622+
swaps in a Bootstrap spinner on confirm). tests/test_web.py (new coverage
2623+
for the frequency-vs-newer-release interaction).
2624+
```
2625+
26082626
**CHOOSE HOW OFTEN YOU'RE TOLD ABOUT NEW VERSIONS, AND ALWAYS SEE YOUR CURRENT ONE**
26092627

26102628
Settings now has an "Update Notifications" section where you can set how often the "new version available" banner shows up on the home page: daily (the default), weekly, or monthly. The banner's build note also names that setting directly, with a link to change it. Separately, the home page now always shows a line under the Welcome heading — "Current version date: yyyy-mm-dd:hhmm" — so you can check exactly what you're running, down to the minute, without scrolling to the page footer; whenever a build note is set it follows in parentheses as "(Version note: ...)" on that same line. The build note no longer gets its own standalone box further up the page. See [What you see on the home page](#home-page-tour) for the full rundown, including exactly when the update check itself runs.

version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "2026-08-31:2306"
1+
VERSION = "2026-08-31:2330"
22

33
# A short, plain-language note shown next to VERSION on the home page and
44
# used to describe this build in the update-available banner (see
@@ -9,4 +9,4 @@
99
# "minor problem fixes"
1010
# "minor function added or improved"
1111
# "significant improvements implemented — see top of User Manual for details"
12-
NEW_VERSION_NOTE = "test of new version messaging"
12+
NEW_VERSION_NOTE = "minor problem fixes"

web/backend.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -687,12 +687,21 @@ def _current_update_notify_frequency() -> str:
687687
return freq if freq in _VALID_UPDATE_NOTIFY_FREQS else "daily"
688688

689689

690-
def _should_show_update_notice() -> bool:
690+
def _should_show_update_notice(tag: str) -> bool:
691691
"""Whether the "update available" banner should be shown on this page load,
692692
per the saved notification-frequency preference (daily/weekly/monthly) —
693693
gates only how often the already-cached update_check result is surfaced to
694-
the user, not how often GitHub itself is polled."""
694+
the user, not how often GitHub itself is polled. A release newer than the
695+
one last shown always gets through regardless of the frequency window —
696+
the setting throttles repeat notices about the *same* release, not
697+
whether you're told about a *different, newer* one that showed up since."""
695698
prefs = _load_prefs_file()
699+
if prefs.get("update_notice_last_shown_tag") != tag:
700+
_save_prefs_file({
701+
"update_notice_last_shown_at": datetime.date.today().isoformat(),
702+
"update_notice_last_shown_tag": tag,
703+
})
704+
return True
696705
interval_days = _UPDATE_NOTIFY_FREQ_DAYS[_current_update_notify_frequency()]
697706
last_shown = prefs.get("update_notice_last_shown_at")
698707
if last_shown:
@@ -1359,7 +1368,7 @@ async def index(request: Request, updated: int = 0, update_error: str = ""):
13591368
# check would (correctly, but confusingly) still report the release
13601369
# just installed as "available." Skip it until the next real launch.
13611370
update_available = None if updated else await run_in_threadpool(_update_check.check_for_update, VERSION)
1362-
if update_available and not _should_show_update_notice():
1371+
if update_available and not _should_show_update_notice(update_available["tag"]):
13631372
update_available = None
13641373
return templates.TemplateResponse(
13651374
request, "home.html", {

web/templates/home.html

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,26 @@
3333
<a href="/settings#update-notifications">change via Settings</a>)</span></small>
3434
{% endif %}
3535
{% if self_update_available %}
36-
<form method="post" action="/update-now" class="mt-2 mb-0"
37-
onsubmit="return confirm('Download and install NutriMagnus ' + '{{ update_available.tag }}' + ' now? This replaces the running program; your data is untouched. You will need to quit and reopen NutriMagnus afterward.');">
38-
<button type="submit" class="btn btn-sm btn-primary">Update now</button>
36+
<form method="post" action="/update-now" class="mt-2 mb-0" id="update-now-form">
37+
<button type="submit" id="update-now-btn" class="btn btn-sm btn-primary">Update now</button>
3938
<a href="{{ update_available.url }}" target="_blank" rel="noopener" class="ms-2">what's new</a>
4039
</form>
40+
<script>
41+
(function () {
42+
var form = document.getElementById('update-now-form');
43+
var btn = document.getElementById('update-now-btn');
44+
form.addEventListener('submit', function (event) {
45+
if (!confirm('Download and install NutriMagnus {{ update_available.tag }} now? ' +
46+
'This replaces the running program; your data is untouched. ' +
47+
'You will need to quit and reopen NutriMagnus afterward.')) {
48+
event.preventDefault();
49+
return;
50+
}
51+
btn.disabled = true;
52+
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Downloading…';
53+
});
54+
})();
55+
</script>
4156
{% else %}
4257
See <a href="{{ update_available.url }}" target="_blank" rel="noopener">what's new on GitHub</a> to download it.
4358
{% endif %}

0 commit comments

Comments
 (0)