v0.243.0: FileBrowser generated admin password (R-513); per-tier whole-guest backup truth (R-517); skip absent-storage tiers (R-518); OOM-killed worker visible (R-514)
gates / gates (push) Successful in 14s

MinAgent: 0.131.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-15 10:12:08 +02:00
parent 406755fa8f
commit 843b319f35
23 changed files with 1095 additions and 8 deletions
@@ -113,6 +113,85 @@ type guestBackupView struct {
RestoreTestedAt time.Time
CanTrigger bool // a backup trigger (quiesce loop) is wired
// Tiers (R-517, agent >= v0.131.0) is the per-tier truth. Empty on an older agent — the tile then
// renders the single latest record as before.
Tiers []guestTierView
}
// guestTierView is one whole-guest tier as the customer sees it. A failed attempt is shown UNDER the
// newest success, never instead of it ("presence is not success").
type guestTierView struct {
Target string
Label string // Hungarian tier name
IsPBS bool
NotSetUp bool // the tier's storage does not exist on the host → „nincs beállítva"
HasSuccess bool
SuccessAt time.Time
SizeKnown bool // false when the success was read back from storage after a restart
SizeBytes int64
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
}
// tierWindow is how old a tier's newest success may be and still count as current: its cadence plus
// half again (a scheduled run lands inside the nightly window, not on the exact second). A tier with
// no advertised cadence uses a day.
func tierWindow(cadenceSecs int64) time.Duration {
if cadenceSecs <= 0 {
cadenceSecs = 24 * 3600
}
return time.Duration(cadenceSecs) * time.Second * 3 / 2
}
// buildTierViews turns the agent's per-tier state into page rows and derives the three legacy tile
// fields from SUCCESSES only: HasBackup/Success/Size/Target/StartedAt from the primary tier's newest
// success; Due when any set-up tier has no current success; Offsite only on a current PBS success.
func buildTierViews(v *guestBackupView, tiers []agentapi.TierBackupState, cadence map[string]int64, now time.Time) {
v.Tiers = nil
v.Due, v.Offsite, v.HasBackup = false, false, false
var newestOK time.Time
for _, t := range tiers {
tv := guestTierView{Target: t.Target, IsPBS: strings.Contains(strings.ToLower(t.Target), "pbs")}
if tv.IsPBS {
tv.Label = "Biztonsági szerver – külön hardver (PBS)"
} else {
tv.Label = "Helyi tároló (" + t.Target + ")"
}
tv.NotSetUp = t.Storage == "absent"
if t.LastSuccess != nil {
if ts, err := time.Parse(time.RFC3339, t.LastSuccess.StartedAt); err == nil {
tv.HasSuccess, tv.SuccessAt = true, ts
tv.SizeKnown = t.LastSuccessSource != "storage"
tv.SizeBytes = t.LastSuccess.SizeBytes
tv.Current = now.Sub(ts) <= tierWindow(cadence[t.Target])
}
}
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
}
}
if !tv.NotSetUp && !tv.Current {
v.Due = true
}
if tv.IsPBS && tv.Current && !tv.NotSetUp {
v.Offsite = true
}
if tv.HasSuccess && (t.Primary || !v.HasBackup) && (t.Primary || tv.SuccessAt.After(newestOK)) {
v.HasBackup, v.Success = true, true
v.StartedAt, v.SizeBytes, v.Target = tv.SuccessAt, tv.SizeBytes, tv.Label
newestOK = tv.SuccessAt
}
v.Tiers = append(v.Tiers, tv)
}
if v.Due {
v.DueReason = "tier_not_current"
}
if v.HasBackup {
v.AgeHours = int64(now.Sub(v.StartedAt).Hours())
}
}
// loadGuestBackup fetches the agent's whole-guest backup view (best-effort). Returns a view with
@@ -153,6 +232,17 @@ func (s *Server) loadGuestBackup(ctx context.Context) *guestBackupView {
v.AgeHours = *due.AgeSecs / 3600
}
}
// R-517: an agent that speaks per tier replaces the single-record fields with per-tier truth.
// Done AFTER the legacy fill so an older agent keeps exactly the old rendering.
if len(st.Tiers) > 0 {
cadence := map[string]int64{}
if tr, terr := client.BackupTiers(ctx); terr == nil {
for _, t := range tr.Tiers {
cadence[t.Target] = t.CadenceSeconds
}
}
buildTierViews(v, st.Tiers, cadence, time.Now())
}
// Restore-test (the "verified restorable" trust signal; nil until one runs).
if rt, rerr := client.RestoreTestStatus(ctx); rerr == nil && rt != nil {
v.HasRestoreTest = true