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:
@@ -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()
|
||||
|
||||
|
||||
@@ -276,11 +276,13 @@ func TestSortDisksForView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// P4 (4B): a drive's cross-drive backup copies (felhom-data/backups/secondary/<app>) are listed so the
|
||||
// wipe confirmation can warn they'd be destroyed. Shared repo / infra dirs and files are skipped.
|
||||
// P4 (4B): a drive's cross-drive backup copies (backups/secondary/<app>) are listed so the wipe
|
||||
// confirmation can warn they'd be destroyed. Shared repo / infra dirs and files are skipped.
|
||||
// Layout is Model-A in-guest: the drive mount IS the felhom-data namespace root (no felhom-data
|
||||
// subdir), matching NamespaceRoot(where, true) and where Tier 2 (Phase 3) writes its copies.
|
||||
func TestBackupCopiesOnPath(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sec := filepath.Join(root, "felhom-data", "backups", "secondary")
|
||||
sec := filepath.Join(root, "backups", "secondary")
|
||||
for _, d := range []string{"immich", "nextcloud", "restic", "_infra"} {
|
||||
if err := os.MkdirAll(filepath.Join(sec, d), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -358,8 +358,10 @@
|
||||
</div>
|
||||
{{else}}
|
||||
<span class="layer-auto-ok">✓ 1. mentés auto</span>
|
||||
<span class="layer-unconfigured">⚠ Nincs 2. másolat</span>
|
||||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
|
||||
<span class="layer-unconfigured">⚠ Nincs 2. (off-drive) másolat</span>
|
||||
{{if .Tier2LastError}}
|
||||
<span class="layer-reason" style="opacity:.85" title="A 2. mentés automatikus — külön beállítás nem kell">{{.Tier2LastError}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- Tier 3: Remote backup (future) -->
|
||||
|
||||
Reference in New Issue
Block a user