diff --git a/CHANGELOG.md b/CHANGELOG.md index c88995b9..9ea8ef6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,12 @@ skipped it until the controller restarted. New seam `backup.SetUpdateHeldForgett **R-1348 — a failed put-back at start-up mails the operator.** `holdPutBackFailed` writes the D20 hold at `Recover`, before any notifier exists, so only the hold reporter's info and the hub's later reminder said anything. `AppStopGuard.SetHeldNotify` hands over what `Recover` held, once, and mails a later failure (a drive that returned) at once: the R-1340 sentence for -`MixedCauseInterruptedPutBack`, from the hold record. Tests `TestR1348_*`. +`MixedCauseInterruptedPutBack`, from the hold record. The mail's run name says „restore" for it, not „offsite-reconstitute" +(`heldRunKind`, seen on the live proof). Tests `TestR1348_*`. **Live proof 2026-11-03, scratch 9202, test image of c4b6844** (felhom.eu +`audits/security-2026-11-02/part3/`): a restore cut at „the restore copy is recorded", the volume held by a created-only +container, the controller started: hold `restore_mixed` / `interrupted_put_back`, the event `backup_run_failures` with the sentence in the sink. +**R-1347's live proof, same image:** an update with its undo copy cut off → hold `update_failed`; the household's restore → „the update hold is CLEARED", „its +update-held flag is forgotten"; six `kill -9` → the crash-loop watcher stops the app (`unhealthy_stop`), so it watches again. **R-1339 — no false „drive … not mounted".** A per-app folder under a mounted drive is never a mountpoint itself; nine false lines at every start on 9202. The folder still gets no bind and no skeleton of its own (the row's „not skipped" would write a diff --git a/controller/cmd/controller/holdmail.go b/controller/cmd/controller/holdmail.go index 170de1ca..4afd7e25 100644 --- a/controller/cmd/controller/holdmail.go +++ b/controller/cmd/controller/holdmail.go @@ -78,6 +78,16 @@ func heldStoppedOperatorText(h settings.RestoreHold) (msg, leg, reason string) { } } +// heldRunKind names the run in the mail's subject. A hold that does not say which restore wrote it is an off-site one, the +// pre-R-1340 rule, EXCEPT an interrupted put-back: its record names no kind, and its restore may be either, so „restore" (R-1348, +// seen live 2026-11-03: a local restore's mail read „offsite-reconstitute"). Pinned by TestR1348_HeldRunKind. +func heldRunKind(h settings.RestoreHold) string { + if h.RestoreKind == settings.RestoreKindLocal || h.MixedCause == settings.MixedCauseInterruptedPutBack { + return "restore" + } + return "offsite-reconstitute" +} + func mixedHoldText(h settings.RestoreHold, kind, tail string) (msg, leg, reason string) { stack := h.Stack leg = "restore-hold-mixed" diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 061ed123..80c0e0c1 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -1560,11 +1560,7 @@ func main() { holdNotify := func(stack string, replayErr, rollbackErr error) { h := heldHoldFromNotify(sett.GetRestoreHold, stack, replayErr, rollbackErr) msg, leg, reason := heldStoppedOperatorText(h) - runKind := "offsite-reconstitute" - if h.RestoreKind == settings.RestoreKindLocal { - runKind = "restore" - } - d := notify.BackupRunFailuresDetails{RunKind: runKind, Failed: 1, Attempted: 1} + d := notify.BackupRunFailuresDetails{RunKind: heldRunKind(h), Failed: 1, Attempted: 1} d.Apps = append(d.Apps, notify.RunFailureDetail{App: stack, Leg: leg, Reason: reason}) notifier.NotifyBackupRunFailures(msg, d) } diff --git a/controller/cmd/controller/r1347_r1348_wiring_test.go b/controller/cmd/controller/r1347_r1348_wiring_test.go index 1bc124c7..0af51222 100644 --- a/controller/cmd/controller/r1347_r1348_wiring_test.go +++ b/controller/cmd/controller/r1347_r1348_wiring_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/settings" +) // R-1347 and R-1348: each seam is CALLED from main.go (an AST walk) — a seam built and never wired is the project's // oldest shape. @@ -17,3 +21,21 @@ func TestR1348_HeldNotifyIsWiredAtStartup(t *testing.T) { t.Fatal("main.go never calls SetHeldNotify — a failed put-back at start-up would mail nobody") } } + +// The mail's run name for an interrupted put-back. RED (2026-11-03, live on 9202): the sink showed run_kind offsite-reconstitute for a +// local restore. +func TestR1348_HeldRunKind(t *testing.T) { + for _, c := range []struct { + h settings.RestoreHold + want string + }{ + {settings.RestoreHold{Reason: settings.HoldReasonRestoreMixed, MixedCause: settings.MixedCauseInterruptedPutBack}, "restore"}, + {settings.RestoreHold{RestoreKind: settings.RestoreKindLocal}, "restore"}, + {settings.RestoreHold{RestoreKind: settings.RestoreKindOffsite}, "offsite-reconstitute"}, + {settings.RestoreHold{Reason: settings.HoldReasonRestoreMixed}, "offsite-reconstitute"}, + } { + if got := heldRunKind(c.h); got != c.want { + t.Errorf("%+v: run kind %q, want %q", c.h, got, c.want) + } + } +}