v0.283.0: apps go off-site by themselves (decision 50) with a size warning; a Stop holds during a backup (R-721); page slips (R-724/R-725)
gates / gates (push) Successful in 25s

Red-proofs RP31-RP38. MinAgent 0.131.0 (unchanged).

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-30 10:31:55 +02:00
parent 1cfb1244d6
commit 6be6c53e29
38 changed files with 3097 additions and 46 deletions
+38 -2
View File
@@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"gitea.dooplex.hu/admin/felhom-controller/internal/backupwindow"
"gitea.dooplex.hu/admin/felhom-controller/internal/dockerexec"
"log"
"net/http"
@@ -1013,6 +1014,31 @@ func (s *Server) backupsOffboxData(data map[string]interface{}, lang string) {
}
}
data["OffboxBlockedSet"] = blocked
// Decision 50 (v0.283.0): the one-press offer for apps installed before apps went off-site by default,
// shown until answered; and the size card when the selected apps will not fit the quota.
if s.backupMgr != nil && s.backupMgr.OffboxConfigured() && !s.settings.OffboxOfferDismissedValue() {
if off := s.appsWithoutOffbox(); len(off) > 0 {
data["OffboxOfferCount"] = len(off)
}
}
if s.backupMgr != nil && offboxTgt != nil && offboxTgt.QuotaGB > 0 && s.backupMgr.OffboxConfigured() {
if f := s.backupMgr.OffsiteFitForPage(); !f.At.IsZero() && !f.Fits {
data["OffsiteFitTotal"] = appbackup.HumanizeBytes(f.TotalBytes)
data["OffsiteFitQuota"] = offboxTgt.QuotaGB
var top []string
for i, a := range f.Apps {
if i == 3 || a.Bytes == 0 {
break
}
name := a.Stack
if st, ok := s.stackMgr.GetStack(a.Stack); ok && st.Meta.DisplayName != "" {
name = st.Meta.DisplayName
}
top = append(top, name+" ("+appbackup.HumanizeBytes(a.Bytes)+")")
}
data["OffsiteFitTop"] = strings.Join(top, ", ")
}
}
}
// offboxStaleWarningMarker is the substring the zero-toggled offbox run wrote into LastWarning before
@@ -2244,8 +2270,14 @@ func (s *Server) systemPageData() map[string]interface{} {
data["GitRepoURL"] = s.cfg.Git.RepoURL
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
data["BackupEnabled"] = s.cfg.Backup.Enabled
data["DBDumpSchedule"] = s.cfg.Backup.DBDumpSchedule
data["ResticSchedule"] = s.cfg.Backup.ResticSchedule
// R-724 (v0.283.0): the schedule the box really runs — the household's backup window and its derived
// legs, the same numbers the backups page shows — not the controller.yaml seed (measured 2026-09-29:
// Beállítások said „02:30 / 03:00" while the backups page said 02:30 / 03:30 / 04:15).
{
db, tier2, offbox := backupwindow.LegTimes(s.effectiveBackupWindow())
data["DBDumpSchedule"] = db
data["ResticSchedule"] = tier2 + " / " + offbox
}
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
data["HubEnabled"] = s.cfg.Hub.Enabled
@@ -2264,7 +2296,11 @@ func (s *Server) systemPageData() map[string]interface{} {
if status.LastCheck != nil {
data["UpdateAvailable"] = status.LastCheck.UpdateAvailable
data["LatestVersion"] = status.LastCheck.LatestVersion
// R-724: the box's local time, not a raw RFC3339 stamp.
data["LastCheckTime"] = status.LastCheck.CheckedAt
if t, err := time.Parse(time.RFC3339, status.LastCheck.CheckedAt); err == nil {
data["LastCheckTime"] = t.In(getTimezone()).Format("2006-01-02 15:04")
}
data["LastCheckError"] = status.LastCheck.Error
}
if status.LastState != nil {