controller v0.272.0: the backup page says when a whole-box backup does not fit (R-685); R-671, R-670, R-677
gates / gates (push) Successful in 26s

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-25 11:19:27 +02:00
parent 71accca427
commit 44ae4dea70
21 changed files with 376 additions and 10 deletions
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"regexp"
"strings"
"time"
@@ -136,6 +137,28 @@ type guestTierView struct {
Current bool // newest success is inside the tier's window
FailedAfter bool // the last attempt failed and is newer than the newest success
FailedAt time.Time
// NoSpaceLine (v0.272.0, R-685 page half) is the plain sentence under a failed tier when the agent
// SKIPPED the backup because it cannot fit (agent v0.134.0+), in the reader's language.
NoSpaceLine string
}
// agentNoSpacePrefix is the agent's `backup.BackupSkipNoSpacePrefix` (felhom-agent v0.134.0,
// internal/backup/runner.go) — the stable start of a tier attempt's Error when the space preflight refused.
// A wire contract between two repos: change it in both or in neither.
const agentNoSpacePrefix = "skipped: not enough space: "
// agentNoSpaceRe reads the agent's sentence: "<target> has X GiB free; … needs about Z GiB (…)".
var agentNoSpaceRe = regexp.MustCompile(`has ([0-9.]+ GiB) free;.*needs about ([0-9.]+ GiB)`)
// noSpaceLine renders the household's sentence for a space skip, "" when the attempt is not one.
func noSpaceLine(errText string, msg func(key string, a ...interface{}) string) string {
if !strings.HasPrefix(errText, agentNoSpacePrefix) {
return ""
}
if m := agentNoSpaceRe.FindStringSubmatch(errText); m != nil {
return msg("backup.tier.no_space", m[2], m[1])
}
return msg("backup.tier.no_space_unknown")
}
// tierWindow is how old a tier's newest success may be and still count as current: its cadence plus
@@ -174,6 +197,7 @@ func buildTierViews(v *guestBackupView, tiers []agentapi.TierBackupState, cadenc
if a := t.LastAttempt; a != nil && !a.Success {
if ts, err := time.Parse(time.RFC3339, a.StartedAt); err == nil && (!tv.HasSuccess || ts.After(tv.SuccessAt)) {
tv.FailedAfter, tv.FailedAt = true, ts
tv.NoSpaceLine = noSpaceLine(a.Error, msg)
}
}
if !tv.NotSetUp && !tv.Current {