feat: infra backup retention + version picker

Hub: GFS retention (7d/4w/3m, ~14 versions) in new infra_backup_versions
table. Recovery endpoint supports ?version=ID. New /versions API endpoint.
Dashboard shows backup history.

Controller: local drive backups rotated into history/ (last 5 versions).
Setup wizard shows version picker for Hub restores when multiple versions
exist. Scan results enriched with app names, disk count, history badge.
Local restore supports historical versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 14:47:40 +01:00
parent 8f49bcc4cc
commit c0cdd95e56
9 changed files with 540 additions and 80 deletions
+62 -29
View File
@@ -17,15 +17,20 @@ import (
// DriveBackup represents a found infra backup on a drive.
type DriveBackup struct {
Device string `json:"device"`
Label string `json:"label"`
MountPoint string `json:"mount_point"`
CustomerID string `json:"customer_id"`
Timestamp string `json:"timestamp"`
CtrlVersion string `json:"controller_version"`
IntegrityOK bool `json:"integrity_ok"`
Error string `json:"error,omitempty"`
WasTempMounted bool `json:"-"`
Device string `json:"device"`
Label string `json:"label"`
MountPoint string `json:"mount_point"`
CustomerID string `json:"customer_id"`
Timestamp string `json:"timestamp"`
CtrlVersion string `json:"controller_version"`
IntegrityOK bool `json:"integrity_ok"`
Error string `json:"error,omitempty"`
StackCount int `json:"stack_count"`
StackNames []string `json:"stack_names,omitempty"`
DiskCount int `json:"disk_count"`
IsHistory bool `json:"is_history"`
HistoryFile string `json:"history_file,omitempty"`
WasTempMounted bool `json:"-"`
}
// lsblkOutput represents the JSON output of lsblk.
@@ -114,10 +119,8 @@ func ScanDrivesForInfraBackups(logger *log.Logger, debug bool) ([]DriveBackup, e
continue
}
result := scanPartition(part, mountedFS, logger)
if result != nil {
results = append(results, *result)
}
partResults := scanPartition(part, mountedFS, logger)
results = append(results, partResults...)
}
logger.Printf("[INFO] Setup: drive scan complete — found %d backup(s)", countValid(results))
@@ -137,7 +140,7 @@ func CleanupTempMounts(results []DriveBackup, logger *log.Logger) {
}
}
func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Logger) *DriveBackup {
func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Logger) []DriveBackup {
label := ""
if part.Label != nil {
label = *part.Label
@@ -187,10 +190,12 @@ func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Lo
return nil
}
// Found backup — read and validate
_, meta, err := backup.ReadLocalInfraBackup(mountPoint)
var results []DriveBackup
result := &DriveBackup{
// Read current backup
backupData, meta, err := backup.ReadLocalInfraBackup(mountPoint)
current := DriveBackup{
Device: part.Path,
Label: label,
MountPoint: mountPoint,
@@ -198,24 +203,52 @@ func scanPartition(part lsblkDevice, mountedFS map[string]string, logger *log.Lo
}
if err != nil {
result.IntegrityOK = false
result.Error = err.Error()
current.IntegrityOK = false
current.Error = err.Error()
if meta != nil {
result.CustomerID = meta.CustomerID
result.Timestamp = meta.Timestamp
result.CtrlVersion = meta.ControllerVersion
current.CustomerID = meta.CustomerID
current.Timestamp = meta.Timestamp
current.CtrlVersion = meta.ControllerVersion
}
} else {
result.IntegrityOK = true
result.CustomerID = meta.CustomerID
result.Timestamp = meta.Timestamp
result.CtrlVersion = meta.ControllerVersion
current.IntegrityOK = true
current.CustomerID = meta.CustomerID
current.Timestamp = meta.Timestamp
current.CtrlVersion = meta.ControllerVersion
backup.ParseBackupCounts(backupData, &current.StackCount, &current.StackNames, &current.DiskCount)
}
logger.Printf("[INFO] Setup: found infra backup on %s (%s) — customer=%s, integrity=%v",
part.Path, label, result.CustomerID, result.IntegrityOK)
results = append(results, current)
return result
logger.Printf("[INFO] Setup: found infra backup on %s (%s) — customer=%s, integrity=%v",
part.Path, label, current.CustomerID, current.IntegrityOK)
// Also scan history directory for older versions
history := backup.ReadLocalInfraHistory(mountPoint)
for _, hv := range history {
hResult := DriveBackup{
Device: part.Path,
Label: label,
MountPoint: mountPoint,
CustomerID: hv.CustomerID,
Timestamp: hv.Timestamp,
CtrlVersion: hv.ControllerVersion,
IntegrityOK: hv.IntegrityOK,
Error: hv.Error,
StackCount: hv.StackCount,
StackNames: hv.StackNames,
DiskCount: hv.DiskCount,
IsHistory: true,
HistoryFile: hv.HistoryFile,
}
results = append(results, hResult)
}
if len(history) > 0 {
logger.Printf("[INFO] Setup: found %d historical backup version(s) on %s", len(history), part.Path)
}
return results
}
func readMountedFilesystems() map[string]string {