v0.155.0 — the restore wizard read the wrong "is something running" flag

Fixes a defect shipped in v0.154.0, found by the operator on the first live
click-through of the new wizard.

backup.Manager carries TWO running booleans. `running` (read by IsRunning) is the
concurrency single-flight, acquired inside the background goroutine — and
RestoreOffboxScratch never acquires it at all. `opRunning` (read by RestoreStatus) is the
display flag, set synchronously by BeginRestoreOp in the handler.

The wizard sourced OpRunning from IsRunning(), so for „Ellenőrzés" and the full-restore
preparation — its two most-used and longest actions, both streaming from restic — the
execution step was unreachable: the page offered all three intents with live buttons
while a restore was running, and the progress banner contradicted the phase strip on the
same screen. Pressing anything there would have been refused by the handler, which is the
exact "offering a control guaranteed to fail" dishonesty R-48 exists to remove.

Fix: restoreOpInFlight(st) behind a documented seam, fed by a SINGLE RestoreStatus() read
per render so the strip, the suppression decision and the running-op name cannot diverge.

Why the tests missed it: the Scenario-E table proved deriveWizardStep behaves correctly
GIVEN OpRunning=true, but nothing proved the handler ever computes true — hollow at
exactly that seam. TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag now drives a
real Manager through BeginRestoreOp and asserts the render suppresses every form.
Red-proofed against the v0.154.0 shape.

Also: „Eredmény" was a dead label. The strip's highlight is now its own derived Phase,
separate from Step — a finished restore returns to the intent step (everything available
again) while the strip reads „Eredmény" and an outcome card shows the result. Bounded by
restoreResultWindow (10 min) so a stale result cannot look fresh, and bound to the app so
a finished bookstack restore does not light immich's page with bookstack's message. The
card survives a reload; the redirect flash does not.

No new agent coupling — MinAgent stays 0.90.0.
This commit is contained in:
2026-07-21 09:15:56 +02:00
parent 70cb21b058
commit 9d1b4983f5
7 changed files with 320 additions and 21 deletions
+38
View File
@@ -1,5 +1,43 @@
## Changelog
### v0.155.0 — the restore wizard read the wrong "is something running" flag (2026-07-21)
**No new agent coupling — MinAgent stays 0.90.0.** Fixes a defect shipped in v0.154.0 and found by
the operator on the first live click-through, plus the dead phase-strip label from the same release.
**The bug.** `backup.Manager` carries two different booleans and v0.154.0 read the wrong one:
| flag | read by | set by | covers the verification restore? |
|---|---|---|---|
| `running` | `IsRunning()` | `acquireRunning()`, **inside** the goroutine | **no — `RestoreOffboxScratch` never acquires it at all** |
| `opRunning` | `RestoreStatus()` | `BeginRestoreOp()`, in the handler, synchronously | yes, all four offsite actions |
The wizard sourced `OpRunning` from `IsRunning()`. For „Ellenőrzés" and the full-restore preparation
— the wizard's two most-used actions, and the long ones, since they stream from restic — that flag is
false for the *entire* operation. So the execution step was unreachable: the page kept offering all
three intents with live buttons while a restore was downloading, and the progress banner (which polls
the op status) contradicted the phase strip on the same screen. Any button pressed there would have
been refused by the handler — which is exactly the "offering a control guaranteed to fail" dishonesty
R-48 exists to remove.
**The fix** is one line of behaviour behind a named seam: `restoreOpInFlight(st)` takes the
`RestoreOpStatus` the handler already reads once, and its doc comment states which flag is which and
why. The handler now takes a single `RestoreStatus()` read, so the strip, the suppression decision
and the running-op name can no longer disagree with each other.
**Why the v0.154.0 tests missed it.** The Scenario-E table proved `deriveWizardStep` behaves
correctly *given* `OpRunning=true`; nothing proved the handler ever computes `true`. Hollow at exactly
that seam. `TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag` now drives a real `Manager`
through `BeginRestoreOp` and asserts the wizard suppresses every form — red-proofed against the
v0.154.0 shape.
**„Eredmény" is now reachable.** The fourth phase label never lit up in v0.154.0. The strip's
highlight is now its own derived value (`Phase`), separate from `Step`: a finished restore is back on
the intent step — everything is available again — while the strip rightly reads „Eredmény" and an
outcome card shows the result. Bounded by `restoreResultWindow` (10 min) so a stale result cannot
claim to be fresh, and bound to the app, so a finished bookstack restore does not light up immich's
page with bookstack's message. The card survives a reload, which the redirect flash does not.
### v0.154.0 — one restore entry per app, and the intent is a described choice (2026-07-21)
Closes **R-48**. **No new agent coupling — MinAgent stays 0.90.0.** This is a UI-layer change: