v0.12.8: complete cross-drive backup + per-tier UI
- Cross-drive now copies DB dumps (_db/) and config (_config/) alongside user data - restic cross-drive includes config dir + full DB dump dir - UI: per-tier rows (1. mentés / 2. mentés) instead of per-layer (DB/Konfig/Data) - UI: BackupContents label shows what each tier protects (DB + Konfig + Adatok) - UI: rsync backups show browsable indicator (📁) - Cleanup: removed unused filterSnapshotsByPaths + pathCovers from router.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -497,42 +497,46 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
// AppBackupRow holds all backup information for one app, used by the backup page template.
|
||||
// AppBackupRow holds per-tier backup information for one app on the backup page.
|
||||
type AppBackupRow struct {
|
||||
StackName string
|
||||
DisplayName string
|
||||
Status string // "green", "yellow", "red", "auto"
|
||||
StatusText string // short Hungarian tooltip
|
||||
|
||||
// Storage info (HDD apps only)
|
||||
// App characteristics
|
||||
HasHDDData bool
|
||||
HasDB bool
|
||||
StorageLabel string
|
||||
HDDSizeHuman string
|
||||
|
||||
// Layer details (nil = layer not applicable)
|
||||
HasDB bool
|
||||
DBLastRun string // formatted time
|
||||
DBLastStatus string // "ok", "error", ""
|
||||
// What this app's backup contains (for display)
|
||||
// e.g., "DB + Konfiguráció + Adatok", "DB + Konfiguráció", "Konfiguráció"
|
||||
BackupContents string
|
||||
|
||||
VolumeLastRun string
|
||||
VolumeLastStatus string
|
||||
// Tier 1: Nightly backup (always exists)
|
||||
Tier1LastRun string // formatted time of last restic snapshot
|
||||
Tier1LastStatus string // "ok", "error", ""
|
||||
Tier1DBStatus string // "ok", "error", "" — separate DB dump status for warning
|
||||
|
||||
// Cross-drive / user data
|
||||
HasUserData bool
|
||||
UserDataConfigured bool
|
||||
UserDataMethod string // "rsync", "restic"
|
||||
UserDataDest string // destination label
|
||||
UserDataSchedule string // "Naponta", "Hetente"
|
||||
UserDataLastRun string
|
||||
UserDataLastStatus string // "ok", "error", "running", ""
|
||||
UserDataLastError string
|
||||
UserDataStatusBadge string // "Sikeres", "Hiba", "Fut...", "—"
|
||||
// Tier 2: Cross-drive backup (only for apps with HDD data)
|
||||
Tier2Configured bool
|
||||
Tier2Method string // "rsync", "restic"
|
||||
Tier2MethodLabel string // "rsync", "restic"
|
||||
Tier2Dest string // destination label
|
||||
Tier2Schedule string // "Naponta", "Hetente"
|
||||
Tier2LastRun string
|
||||
Tier2LastStatus string // "ok", "error", "running", ""
|
||||
Tier2LastError string
|
||||
Tier2StatusBadge string // "Sikeres", "Hiba", "Fut...", "—"
|
||||
Tier2SizeHuman string
|
||||
Tier2Browsable bool // true for rsync (plain files), false for restic
|
||||
|
||||
// Warnings accumulated for this app
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// buildAppBackupRows constructs one AppBackupRow per deployed app for the unified backup page.
|
||||
// buildAppBackupRows constructs one AppBackupRow per deployed app for the backup page.
|
||||
func (s *Server) buildAppBackupRows(
|
||||
status *backup.FullBackupStatus,
|
||||
crossConfigs map[string]*settings.CrossDriveBackup,
|
||||
@@ -540,137 +544,139 @@ func (s *Server) buildAppBackupRows(
|
||||
) []AppBackupRow {
|
||||
loc := getTimezone()
|
||||
|
||||
// Build a quick lookup: which stacks have a DB dump?
|
||||
// Build DB stack lookup
|
||||
dbStacks := make(map[string]bool)
|
||||
for _, db := range status.DiscoveredDBs {
|
||||
dbStacks[db.StackName] = true
|
||||
}
|
||||
// Also check dump files if no live discovered DBs
|
||||
for _, f := range status.DumpFiles {
|
||||
dbStacks[f.StackName] = true
|
||||
}
|
||||
|
||||
// Determine last restic run time for volume backup display
|
||||
volumeLastRun := ""
|
||||
volumeLastStatus := ""
|
||||
// Tier 1 timestamps (shared across all apps — single nightly job)
|
||||
tier1LastRun := ""
|
||||
tier1LastStatus := ""
|
||||
if status.LastBackup != nil {
|
||||
volumeLastRun = status.LastBackup.LastRun.In(loc).Format("01-02 15:04")
|
||||
tier1LastRun = status.LastBackup.LastRun.In(loc).Format("01-02 15:04")
|
||||
if status.LastBackup.Success {
|
||||
volumeLastStatus = "ok"
|
||||
tier1LastStatus = "ok"
|
||||
} else {
|
||||
volumeLastStatus = "error"
|
||||
tier1LastStatus = "error"
|
||||
}
|
||||
}
|
||||
|
||||
// DB dump last run
|
||||
dbLastRun := ""
|
||||
dbLastStatus := ""
|
||||
tier1DBStatus := ""
|
||||
if status.LastDBDump != nil {
|
||||
dbLastRun = status.LastDBDump.LastRun.In(loc).Format("01-02 15:04")
|
||||
if status.LastDBDump.Success {
|
||||
dbLastStatus = "ok"
|
||||
tier1DBStatus = "ok"
|
||||
} else {
|
||||
dbLastStatus = "error"
|
||||
tier1DBStatus = "error"
|
||||
}
|
||||
}
|
||||
|
||||
var rows []AppBackupRow
|
||||
for _, app := range status.AppDataInfo {
|
||||
hasDB := dbStacks[app.StackName] || app.HasDBDump
|
||||
|
||||
// Build backup contents label
|
||||
var parts []string
|
||||
if hasDB {
|
||||
parts = append(parts, "DB")
|
||||
}
|
||||
parts = append(parts, "Konfig")
|
||||
if app.HasHDDData {
|
||||
parts = append(parts, "Adatok")
|
||||
}
|
||||
contents := strings.Join(parts, " + ")
|
||||
|
||||
row := AppBackupRow{
|
||||
StackName: app.StackName,
|
||||
DisplayName: app.DisplayName,
|
||||
HasHDDData: app.HasHDDData,
|
||||
StorageLabel: app.StorageLabel,
|
||||
HDDSizeHuman: app.HDDSizeHuman,
|
||||
HasDB: dbStacks[app.StackName] || app.HasDBDump,
|
||||
DBLastRun: dbLastRun,
|
||||
DBLastStatus: dbLastStatus,
|
||||
VolumeLastRun: volumeLastRun,
|
||||
VolumeLastStatus: volumeLastStatus,
|
||||
StackName: app.StackName,
|
||||
DisplayName: app.DisplayName,
|
||||
HasHDDData: app.HasHDDData,
|
||||
HasDB: hasDB,
|
||||
StorageLabel: app.StorageLabel,
|
||||
HDDSizeHuman: app.HDDSizeHuman,
|
||||
BackupContents: contents,
|
||||
|
||||
Tier1LastRun: tier1LastRun,
|
||||
Tier1LastStatus: tier1LastStatus,
|
||||
Tier1DBStatus: tier1DBStatus,
|
||||
}
|
||||
|
||||
// Default status = green/auto
|
||||
// Default status = auto (no user data, just config)
|
||||
row.Status = "auto"
|
||||
row.StatusText = "Automatikus mentés"
|
||||
|
||||
if app.HasHDDData {
|
||||
row.HasUserData = true
|
||||
cfg, hasCfg := crossConfigs[app.StackName]
|
||||
|
||||
if !hasCfg || cfg == nil || !cfg.Enabled {
|
||||
// HDD data backed up via nightly restic (mandatory), but no second copy
|
||||
row.UserDataConfigured = false
|
||||
row.Tier2Configured = false
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Nincs második másolat (csak helyi mentés)"
|
||||
} else {
|
||||
row.UserDataConfigured = true
|
||||
row.UserDataMethod = cfg.Method
|
||||
row.UserDataDest = destLabels[cfg.DestinationPath]
|
||||
if row.UserDataDest == "" {
|
||||
row.UserDataDest = cfg.DestinationPath
|
||||
row.Tier2Configured = true
|
||||
row.Tier2Method = cfg.Method
|
||||
row.Tier2MethodLabel = cfg.Method // "rsync" or "restic"
|
||||
row.Tier2Browsable = cfg.Method == "rsync"
|
||||
row.Tier2Dest = destLabels[cfg.DestinationPath]
|
||||
if row.Tier2Dest == "" {
|
||||
row.Tier2Dest = cfg.DestinationPath
|
||||
}
|
||||
switch cfg.Schedule {
|
||||
case "daily":
|
||||
row.UserDataSchedule = "Naponta"
|
||||
row.Tier2Schedule = "Naponta"
|
||||
case "weekly":
|
||||
row.UserDataSchedule = "Hetente (vasárnap)"
|
||||
row.Tier2Schedule = "Hetente"
|
||||
default:
|
||||
row.UserDataSchedule = cfg.Schedule
|
||||
row.Tier2Schedule = cfg.Schedule
|
||||
}
|
||||
if cfg.LastRun != "" {
|
||||
if t, err := time.Parse(time.RFC3339, cfg.LastRun); err == nil {
|
||||
row.UserDataLastRun = t.In(loc).Format("01-02 15:04")
|
||||
row.Tier2LastRun = t.In(loc).Format("01-02 15:04")
|
||||
}
|
||||
}
|
||||
row.UserDataLastStatus = cfg.LastStatus
|
||||
row.UserDataLastError = cfg.LastError
|
||||
row.Tier2LastStatus = cfg.LastStatus
|
||||
row.Tier2LastError = cfg.LastError
|
||||
row.Tier2SizeHuman = cfg.LastSizeHuman
|
||||
switch cfg.LastStatus {
|
||||
case "ok":
|
||||
row.UserDataStatusBadge = "Sikeres"
|
||||
row.Tier2StatusBadge = "Sikeres"
|
||||
case "error":
|
||||
row.UserDataStatusBadge = "Hiba"
|
||||
case "running":
|
||||
row.UserDataStatusBadge = "Fut..."
|
||||
default:
|
||||
row.UserDataStatusBadge = "—"
|
||||
}
|
||||
|
||||
// Check destination health for status determination
|
||||
health := system.CheckBackupDestination(cfg.DestinationPath)
|
||||
if health.Blocked {
|
||||
row.Status = "red"
|
||||
row.StatusText = "Mentési cél nem elérhető"
|
||||
row.Warnings = append(row.Warnings, health.Warning)
|
||||
} else if health.Warning != "" {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Figyelmeztetés"
|
||||
row.Warnings = append(row.Warnings, health.Warning)
|
||||
} else if cfg.LastStatus == "error" {
|
||||
row.Tier2StatusBadge = "Hiba"
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Utolsó mentés sikertelen"
|
||||
} else {
|
||||
row.Status = "green"
|
||||
row.StatusText = "Mentés rendben"
|
||||
case "running":
|
||||
row.Tier2StatusBadge = "Fut..."
|
||||
default:
|
||||
row.Tier2StatusBadge = "—"
|
||||
}
|
||||
|
||||
// Destination health check
|
||||
if cfg.Enabled && cfg.DestinationPath != "" {
|
||||
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") {
|
||||
row.Status = "red"
|
||||
row.StatusText = "Mentési cél nem elérhető"
|
||||
} else {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Figyelmeztetés"
|
||||
}
|
||||
row.Warnings = append(row.Warnings, err.Error())
|
||||
} else if row.Status != "yellow" {
|
||||
row.Status = "green"
|
||||
row.StatusText = "Mentés rendben"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No HDD data — everything backed up automatically via restic
|
||||
if volumeLastStatus == "ok" {
|
||||
row.Status = "green"
|
||||
row.StatusText = "Mentés rendben"
|
||||
} else if volumeLastStatus == "error" {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Kötetek mentése sikertelen"
|
||||
} else {
|
||||
row.Status = "auto"
|
||||
row.StatusText = "Automatikus mentés"
|
||||
}
|
||||
}
|
||||
|
||||
// If DB dump failed for this app, degrade to yellow (if not already red)
|
||||
if row.HasDB && dbLastStatus == "error" && row.Status != "red" {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Adatbázis mentés sikertelen"
|
||||
// DB dump failure warning (affects Tier 1 quality)
|
||||
if hasDB && tier1DBStatus == "error" {
|
||||
if row.Status != "red" {
|
||||
row.Status = "yellow"
|
||||
row.StatusText = "Adatbázis mentés sikertelen"
|
||||
}
|
||||
}
|
||||
|
||||
rows = append(rows, row)
|
||||
|
||||
Reference in New Issue
Block a user