controller v0.264.0: the household is told when an update is undone or held, in its language
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
app_update_undone / app_update_held events (09 decision 15), on by default and seeded once on existing boxes; R-606 update sentences as key+args rendered per reader; R-646 startup applied-meta backfill for apps current with the catalog; R-620 a disabled notifier WARNs once per event type. Needs hub v0.120.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -52,6 +52,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
catalogsync "gitea.dooplex.hu/admin/felhom-controller/internal/sync"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/web"
|
||||
)
|
||||
|
||||
@@ -463,6 +464,9 @@ func main() {
|
||||
// in the same boot. It reads and writes FILES only — no container is started, stopped or touched.
|
||||
// An app it cannot pin confidently is left UNPINNED and keeps pre-v0.235.0 behaviour, loudly.
|
||||
stackMgr.AdoptPins()
|
||||
// v0.264.0 (R-646): record the pinned version's .felhom.yml for apps pinned before v0.263.2 that are
|
||||
// current with the catalog — the probe their first undo will judge them by. Files only.
|
||||
stackMgr.BackfillAppliedMeta()
|
||||
|
||||
// --- Start the catalog syncer, AFTER adoption ---
|
||||
// ORDERING IS LOAD-BEARING. Start() fires an immediate sync in a goroutine. Started at its
|
||||
@@ -600,6 +604,35 @@ func main() {
|
||||
quiesceLoop.SetTierNotifier(quiesceTierNotifier{n: notifier})
|
||||
}
|
||||
|
||||
// v0.264.0 (`09` §3 decision 15, §6.4 part 2): an undone or held app update is TOLD — one household
|
||||
// mail + an operator event each. The held sentence is the hold's OWN sentence, rendered by the backup
|
||||
// side in whichever language is asked for, so the mail says exactly what the page says. Pinned by
|
||||
// TestUpdateEventSinkIsWiredAtStartup — a seam built and never wired is this project's commonest
|
||||
// defect, and this one would fail silently: no event, no mail, no error.
|
||||
stackMgr.SetUpdateEventSink(func(ev stacks.UpdateEvent) {
|
||||
switch ev.Kind {
|
||||
case stacks.UpdateEventUndone:
|
||||
notifier.NotifyAppUpdateUndone(ev.App, ev.From, ev.To, ev.At)
|
||||
case stacks.UpdateEventHeld:
|
||||
d := notify.AppUpdateDetails{App: ev.App, StackName: ev.App, From: ev.From, To: ev.To,
|
||||
At: ev.At.UTC().Format(time.RFC3339), CopyTier: ev.CopyTier}
|
||||
if !ev.CopyDate.IsZero() {
|
||||
d.CopyDate = ev.CopyDate.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if backupMgr != nil && ev.CopyTier > 0 {
|
||||
d.CopyHolds = backupMgr.UpdateCopyHolds(ev.App, ev.CopyTier)
|
||||
}
|
||||
notifier.NotifyAppUpdateHeld(d, func(lang string) string {
|
||||
if ev.HoldRecorded && backupMgr != nil {
|
||||
if held, why := backupMgr.RestoreHoldForLang(ev.App, lang); held {
|
||||
return why
|
||||
}
|
||||
}
|
||||
return util.Text(lang, "update.error.hold_unsaved")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// R-166 §2.4: report an interrupted app-data operation to the operator, HERE, because the
|
||||
// recovery itself had to run before the boot reconciler (line ~236) and the notifier does not
|
||||
// exist until this line. An interrupted operation means the controller died mid-backup and that
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// v0.264.0. The update-event sink and the R-646 backfill are both CALLED from main.go (an AST walk —
|
||||
// a comment naming them would not count). A sink built and never wired would fail silently: no event,
|
||||
// no mail, no error — this project's commonest defect, four recorded instances.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT.md): comment out the SetUpdateEventSink call in main.go — this fails.
|
||||
func TestUpdateEventSinkIsWiredAtStartup(t *testing.T) {
|
||||
lines, _, _ := slice4CallLines(t)
|
||||
if len(lines["SetUpdateEventSink"]) == 0 {
|
||||
t.Fatal("SetUpdateEventSink is never called — an undone or held update would tell nobody")
|
||||
}
|
||||
for _, callee := range []string{"NotifyAppUpdateUndone", "NotifyAppUpdateHeld", "RestoreHoldForLang"} {
|
||||
if len(lines[callee]) == 0 {
|
||||
t.Errorf("main.go must call %s inside the sink", callee)
|
||||
}
|
||||
}
|
||||
if len(lines["BackfillAppliedMeta"]) == 0 || len(lines["AdoptPins"]) == 0 || lines["BackfillAppliedMeta"][0] < lines["AdoptPins"][0] {
|
||||
t.Error("BackfillAppliedMeta (R-646) must be called, after AdoptPins")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user