v0.147.3 — 4c follow-up 3: the run does not end with the last app

Third real run, third thing only a live run could show. The per-app legs
finished in ~15 seconds; the remaining 40 of the 57-second run was the shares leg
and forget --prune, during which the card sat frozen on "calibre-web — 8 / 8
fájl". The same frozen-looking silence 4c exists to remove, relocated to the end.

Progress now carries a phase. The post-app stages announce themselves and the
app-scoped counters are cleared when a phase starts, so the last app's finished
numbers are never shown against work that is no longer about that app. Starting
the next app clears the phase again. Pinned by a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
This commit is contained in:
2026-07-19 09:54:18 +02:00
parent 111369dd10
commit 63a22e5911
5 changed files with 88 additions and 2 deletions
@@ -52,6 +52,11 @@ type OffboxProgress struct {
// long it has been going — "working on X, 42s" is a completely different message from "0%".
CurrentFile string `json:"current_file"`
ElapsedSec int64 `json:"elapsed_sec"`
// Phase names the part of the run in progress. A run is NOT just the per-app loop: after the last
// app come the shares leg and `forget --prune`, which on the demo box took 40 of a 57-second run.
// Without this the card froze on the last app's finished counters for that whole tail — the same
// silence the slice exists to remove, just relocated. "" = per-app backup.
Phase string `json:"phase"`
}
// resticStatusLine is the subset of restic's `--json` status object we consume. restic emits several
@@ -150,6 +155,7 @@ func (p *offboxProgressState) setApp(app string) {
// A new app resets the byte counters: restic's percentages are per-invocation, and carrying
// the previous app's 100% into the next app's start would show a bar that jumps backwards.
p.cur.CurrentApp = app
p.cur.Phase = ""
p.cur.Percent, p.cur.BytesDone, p.cur.TotalBytes = 0, 0, 0
p.cur.FilesDone, p.cur.TotalFiles = 0, 0
p.cur.CurrentFile, p.cur.ElapsedSec = "", 0
@@ -158,6 +164,27 @@ func (p *offboxProgressState) setApp(app string) {
p.mu.Unlock()
}
// setPhase marks a non-per-app stage of the run and clears the app-scoped counters, so the card
// stops showing the last app's finished numbers against work that is no longer about that app.
func (p *offboxProgressState) setPhase(phase string) {
p.mu.Lock()
if p.live {
p.cur.Phase = phase
p.cur.CurrentApp = ""
p.cur.Percent, p.cur.BytesDone, p.cur.TotalBytes = 0, 0, 0
p.cur.FilesDone, p.cur.TotalFiles = 0, 0
p.cur.CurrentFile, p.cur.ElapsedSec = "", 0
p.cur.DoneHuman, p.cur.TotalHuman = "", ""
}
p.mu.Unlock()
}
// OffboxPhaseShares / OffboxPhaseRetention are the post-app-loop stages.
const (
OffboxPhaseShares = "shares"
OffboxPhaseRetention = "retention"
)
func (p *offboxProgressState) update(r resticProgress) {
p.mu.Lock()
if p.live {