Skip to content

Commit b735a89

Browse files
fix(masthead): restart_needed means a restart HELPS — behind boards get their own cue (#1722)
The filing guessed at "an mtime-vs-process-start comparison that trips on file touches that aren't builds". There is no such comparison. The flag was one expression: "restart_needed": bool(server.get("stale")) or bool(behind) Observed state had server_stale false, so `behind` was the trigger — a logical certainty from the code, not an inference: nothing else writes this flag. Two conditions were sharing one flag and one sentence, and they have DIFFERENT REMEDIES. A stale server is fixed by restarting the Monitor. A board on older firmware is fixed by FLASHING THE BOARD — restarting does nothing for it. The operator was told to do something that cannot work. The sharper half: the masthead ALREADY has the correct cue for behind boards — N boards on older fw (latest X) [tooltip: device @ fw, ...] — and it sits in an `else if` AFTER restart_needed. So while `behind` fed that flag, the accurate message was UNREACHABLE. Every behind board rendered "newer build - restart the Monitor" instead. The right words were written, shipped, and could never appear. Fix: restart_needed = server_stale. `behind` is untouched and still published in the firmware block; it simply no longer claims a restart is the answer, which lets the correct cue render for the first time. The tooltip loses "or a board is behind" — that clause was the conflation stated out loud — and now says restarting does not reflash a board. One test asserted `restart_needed is True` for a behind board. That was the defect written down as an expectation, so it is amended rather than deleted, with the reason in place: a reader hitting that diff should see a corrected model, not a fixture tweak. Deliberately NOT changed: when the server is stale AND boards are behind, the restart cue still wins the `else if`. That precedence predates this and showing both is a masthead-density question, not an honesty one — the remaining cue is true in that state. AC1's intent is pinned by a test asserting restart_needed tracks server_stale exactly, so a third condition cannot be folded in later without bringing its own cue and its own remedy. Refs #1712 Lane: DX
1 parent 8055bb9 commit b735a89

3 files changed

Lines changed: 89 additions & 3 deletions

File tree

tools/analytics/card_context.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,18 @@ def _versions_block(device_groups: list, server: dict) -> dict:
377377
},
378378
# running server predates the checked-out code -> a restart shows newer app
379379
"server_stale": bool(server.get("stale")),
380-
"restart_needed": bool(server.get("stale")) or bool(behind),
380+
# #1712: RESTART_NEEDED MEANS A RESTART HELPS. It used to be
381+
# `stale or behind`, which conflated two conditions with different remedies:
382+
# a stale server is fixed by restarting the Monitor, a board on older
383+
# firmware is fixed by FLASHING THE BOARD, and restarting does nothing for it.
384+
#
385+
# The masthead already carries the correct, specific cue for behind boards
386+
# ("N boards on older fw (latest X)") — but it sits in an `else if` after this
387+
# flag, so while `behind` fed this flag that accurate message was UNREACHABLE.
388+
# Every behind board rendered "newer build - restart the Monitor" instead, an
389+
# instruction that cannot work. `behind` is untouched and still published in
390+
# the firmware block; it simply no longer claims a restart is the answer.
391+
"restart_needed": bool(server.get("stale")),
381392
}
382393

383394

tools/analytics/dashboard_template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ <h2>Experiment Capture <span class="proposed" title="operator-driven; isolated f
10001000
const V = DASH.versions || {};
10011001
const appBit = V.app ? ` · app <span class="mono">${V.app}</span>` : '';
10021002
let cue = '';
1003-
if(V.restart_needed) cue = ` · <span class="vercue warn" title="the running server predates the checked-out code, or a board is behind — restart the Monitor to pick it up">↻ newer build — restart the Monitor</span>`;
1003+
if(V.restart_needed) cue = ` · <span class="vercue warn" title="the running server predates the checked-out code — restart the Monitor to pick it up (a board on older firmware is a separate cue; restarting does not reflash a board)">↻ newer build — restart the Monitor</span>`;
10041004
else if(V.firmware && V.firmware.behind && V.firmware.behind.length)
10051005
cue = ` · <span class="vercue warn" title="${V.firmware.behind.map(b=>b.device+' @ fw '+b.fw).join(', ')}">${V.firmware.behind.length} board${V.firmware.behind.length===1?'':'s'} on older fw${V.firmware.latest?' (latest '+V.firmware.latest+')':''}</span>`;
10061006
document.getElementById('subline').innerHTML =

tools/analytics/test_versions_masthead.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ def test_device_behind_latest_firmware_is_flagged(monkeypatch) -> None:
110110
assert v["firmware"]["latest"] == "0.7.1"
111111
behind = {b["device"] for b in v["firmware"]["behind"]}
112112
assert behind == {"old"} # only the lower board, and only because it parses
113-
assert v["restart_needed"] is True
113+
# #1712: this used to assert restart_needed is True — the defect, written down as
114+
# an expectation. A board on older firmware is fixed by FLASHING IT; restarting
115+
# the Monitor cannot help, and saying so trains the operator to ignore the cue.
116+
assert v["restart_needed"] is False
117+
assert v["server_stale"] is False
114118

115119

116120
def test_stale_server_sets_restart_needed(monkeypatch) -> None:
@@ -127,3 +131,74 @@ def test_no_false_behind_when_latest_unknown(monkeypatch) -> None:
127131
assert v["firmware"]["latest"] is None
128132
assert v["firmware"]["behind"] == []
129133
assert v["restart_needed"] is False
134+
135+
136+
# --------------------------------------------------------------------------- #
137+
# #1712 — restart_needed means A RESTART HELPS
138+
# --------------------------------------------------------------------------- #
139+
# Observed live 2026-08-20: the masthead showed "newer build - restart the Monitor"
140+
# while app == server == 0.8.1, app_server_match true and server_stale false. The
141+
# working tree, origin/main and the running process were all 98f17f3. Nothing newer
142+
# existed to restart into.
143+
144+
145+
def test_a_behind_board_does_not_ask_for_a_restart(monkeypatch) -> None:
146+
"""THE bug. `restart_needed` was `stale or behind`, conflating two remedies.
147+
148+
A stale server is fixed by restarting the Monitor. A board on older firmware is
149+
fixed by flashing the board. Telling the operator to restart for the second one is
150+
an instruction that cannot work — and a prompt that fires when no restart helps
151+
trains them to ignore it, which costs exactly when it is real.
152+
"""
153+
monkeypatch.setattr(card_context, "_declared_fw_version", lambda: "0.8.1")
154+
v = _versions_block(
155+
[_dev("classic", "0.8.1"), _dev("c5", "0.7.3")],
156+
{"version": "0.8.1", "stale": False},
157+
)
158+
assert v["firmware"]["behind"], "the fixture must actually have a behind board"
159+
assert v["restart_needed"] is False
160+
161+
162+
def test_the_behind_fact_is_still_published(monkeypatch) -> None:
163+
"""The fix must not silence a true statement — only stop it asking for a restart.
164+
165+
The masthead's own accurate cue ("N boards on older fw") reads this. It sits in an
166+
`else if` AFTER restart_needed, so while `behind` fed that flag the correct message
167+
was unreachable: every behind board rendered the restart prompt instead.
168+
"""
169+
monkeypatch.setattr(card_context, "_declared_fw_version", lambda: "0.8.1")
170+
v = _versions_block([_dev("c5", "0.7.3")], {"version": "0.8.1", "stale": False})
171+
assert [b["device"] for b in v["firmware"]["behind"]] == ["c5"]
172+
assert v["firmware"]["latest"] == "0.8.1"
173+
174+
175+
def test_matching_tree_and_server_render_no_restart_cue(monkeypatch) -> None:
176+
"""AC2: with tree == running build, the banner does not render."""
177+
monkeypatch.setattr(card_context, "_declared_fw_version", lambda: "0.8.1")
178+
v = _versions_block(
179+
[_dev("classic", "0.8.1")], {"version": "0.8.1", "stale": False}
180+
)
181+
assert v["app"] == v["server"] == "0.8.1"
182+
assert v["app_server_match"] is True
183+
assert v["restart_needed"] is False
184+
185+
186+
def test_a_genuinely_newer_checkout_still_triggers_it(monkeypatch) -> None:
187+
"""AC3, the regression that matters: the cue must still fire when it is true."""
188+
monkeypatch.setattr(card_context, "_declared_fw_version", lambda: "0.8.1")
189+
v = _versions_block([_dev("classic", "0.8.1")], {"version": "0.8.1", "stale": True})
190+
assert v["restart_needed"] is True
191+
192+
193+
def test_restart_needed_tracks_server_stale_exactly(monkeypatch) -> None:
194+
"""AC1's intent: the flag derives from the versions block's own facts.
195+
196+
The filing guessed at an mtime-vs-process-start comparison. There is none — the
197+
flag is one expression, and `behind` was the other half of it. Pinning the
198+
equivalence keeps a third condition from being folded in later without a cue and a
199+
remedy of its own.
200+
"""
201+
monkeypatch.setattr(card_context, "_declared_fw_version", lambda: "0.8.1")
202+
for stale in (True, False):
203+
v = _versions_block([_dev("a", "0.7.0")], {"version": "0.8.1", "stale": stale})
204+
assert v["restart_needed"] is v["server_stale"] is stale

0 commit comments

Comments
 (0)