controller v0.271.0: automatic app updates — the update leg after the off-site copy, the backup gate waits, the switch (09 6.4 part 7; R-680, R-678, R-643)
gates / gates (push) Successful in 24s

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:
2026-09-24 22:07:24 +02:00
parent 1495ca43fb
commit 9cf13a3add
33 changed files with 1672 additions and 14 deletions
@@ -726,6 +726,21 @@ func (m *Manager) HoldAfterFailedUpdateWhole(ctx context.Context, stackName stri
return false, nil
}
// FreshWholeCopy answers decision 13's `files_may_change` mark for the automatic update leg (v0.271.0):
// is there a copy on this box, younger than maxAge, that brings the app back WHOLE — the SAME truth
// table the hold uses (WholeOnTier, decisions 25 and 26), so the leg and the hold cannot disagree about
// what "whole" means. The string says why, for the leg's log.
func (m *Manager) FreshWholeCopy(ctx context.Context, stackName string, maxAge time.Duration, now time.Time) (bool, string) {
best, found, seen := m.HoldCopies(ctx, stackName)
if !found {
return false, fmt.Sprintf("no copy on this box brings it back whole (%d copies seen; drive files declared: %v)", len(seen), m.HasDriveFileLegs(stackName))
}
if age := now.Sub(best.At); age > maxAge {
return false, fmt.Sprintf("the newest whole copy (tier %d, %s) is %s old, limit %s", best.Tier, best.At.UTC().Format(time.RFC3339), age.Round(time.Minute), maxAge)
}
return true, fmt.Sprintf("tier %d copy from %s", best.Tier, best.At.UTC().Format(time.RFC3339))
}
// HoldNoWholeCopy reports whether the app's hold names no copy (R-659) — the page then offers no
// restore button for it.
func (m *Manager) HoldNoWholeCopy(stackName string) bool {