v0.55.0: Phase 3 — auto off-drive Tier 2 (rootfs-headroom guard)

Tier 2 rsync-mirrors each HDD app's recovery unit + appdata to a DIFFERENT physical
disk (the only off-drive protection bind-mounted userdata can get; PBS can't reach it).
Auto-enabled, auto-target: prefer another registered drive (different physical disk via
system.SamePhysicalDevice), else the internal SSD for SMALL units only — with a
size-aware headroom guard that REFUSES rather than fill the ~8G guest rootfs, recording
an honest "needs 2nd HDD" status. Status persisted via the surviving CrossDriveBackup;
"2. mentés" UI card now populated. Daily tier2-backup job + POST /api/backup/tier2.

- backup/tier2.go (engine+selection+headroom), tier2_test.go (headroom arithmetic)
- system.SamePhysicalDevice (linux Stat_t.Dev + stub)
- handlers.go Tier2 UI population + tier2DestLabel; backups.html honest no-target reason
- fixed stale TestBackupCopiesOnPath (old felhom-data layout -> in-guest layout)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 13:24:49 +02:00
parent d8fe8f5ead
commit d2071430ea
12 changed files with 446 additions and 5 deletions
+37
View File
@@ -686,11 +686,48 @@ func (s *Server) buildAppBackupRows(status *backup.FullBackupStatus) []AppBackup
row.StatusText = "Adatbázis mentés sikertelen"
}
// Tier 2 (off-drive copy) status, from the config the Tier 2 runner persists.
if cd := s.settings.GetCrossDriveConfig(app.StackName); cd != nil {
if cd.LastStatus == "no_target" {
// Auto Tier 2 found no off-drive target — surface the honest reason (no silent gap).
row.Tier2Configured = false
row.Tier2StatusBadge = "Nincs 2. meghajtó"
row.Tier2LastError = cd.LastError
} else if cd.Enabled {
row.Tier2Configured = true
row.Tier2Dest = tier2DestLabel(cd.DestinationPath, s.cfg.Paths.SystemDataPath)
row.Tier2Schedule = "Naponta"
row.Tier2LastRun = cd.LastRun
row.Tier2LastStatus = cd.LastStatus
row.Tier2LastError = cd.LastError
row.Tier2SizeHuman = cd.LastSizeHuman
switch cd.LastStatus {
case "ok":
row.Tier2StatusBadge = "Sikeres"
case "error":
row.Tier2StatusBadge = "Hiba"
case "running":
row.Tier2StatusBadge = "Fut..."
default:
row.Tier2StatusBadge = "—"
}
}
}
rows = append(rows, row)
}
return rows
}
// tier2DestLabel renders a friendly destination label for the "2. mentés" card. A destination under
// the system-data path is the internal SSD (DB/config only); otherwise it's an external drive.
func tier2DestLabel(destPath, systemDataPath string) string {
if systemDataPath != "" && strings.HasPrefix(destPath, systemDataPath) {
return "belső SSD (csak DB/konfiguráció)"
}
return filepath.Base(strings.TrimSuffix(destPath, "/"+backup.FelhomDataDir))
}
func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()