v0.12.0 — Backup page overhaul: unified app rows, bug fixes, sequential chaining
Bug fixes: - GetFullStatus() returns deep copy; CrossDriveSummary/UnconfiguredApps/CrossDriveWarnings are always nil in the copy so the handler builds them fresh (fixes duplicate-apps bug) - Replace binary IsMountPoint check with tiered CheckBackupDestination() — path-not-exist, not-writable, system-drive (warning), disk >90-95% full; shown as warning vs critical - Remove dead settingsAppBackupHandler / POST /settings/app-backup route (toggle wrote to settings.json but nothing consumed the flag) Architecture: - Unified per-app backup rows: new AppBackupRow struct + buildAppBackupRows() replaces the two old sections with expandable rows showing all 3 layers per app - Sequential backup chaining: cross-drive runs immediately after restic (removed independent cross-drive-daily/cross-drive-weekly scheduler jobs) - Deploy page: remove "Csak kézi indítás" schedule option; add weekly consistency note Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,19 +232,14 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
}
|
||||
data["BackupDestPaths"] = destPaths
|
||||
|
||||
// Destination health warning
|
||||
// Destination health warning (tiered validation)
|
||||
if crossCfg != nil && crossCfg.Enabled && crossCfg.DestinationPath != "" {
|
||||
if !system.IsMountPoint(crossCfg.DestinationPath) || !system.IsWritable(crossCfg.DestinationPath) {
|
||||
data["BackupDestWarning"] = fmt.Sprintf(
|
||||
"A cél tárhely (%s) nem elérhető! Ellenőrizd a meghajtó csatlakozását.",
|
||||
crossCfg.DestinationPath,
|
||||
)
|
||||
health := system.CheckBackupDestination(crossCfg.DestinationPath)
|
||||
if health.Warning != "" {
|
||||
data["BackupDestWarning"] = health.Warning
|
||||
data["BackupDestWarningSeverity"] = health.Severity
|
||||
}
|
||||
}
|
||||
|
||||
// Nightly backup toggle state
|
||||
appBackupEnabled := s.settings.IsAppBackupEnabled(name)
|
||||
data["AppBackupEnabled"] = appBackupEnabled
|
||||
}
|
||||
|
||||
// Memory info for deploy page (only for non-deployed apps)
|
||||
@@ -457,15 +452,38 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
fullStatus.CrossDriveSummary = append(fullStatus.CrossDriveSummary, item)
|
||||
|
||||
// Destination health warning
|
||||
// Destination health warning (tiered validation)
|
||||
if cfg.Enabled && cfg.DestinationPath != "" {
|
||||
if !system.IsMountPoint(cfg.DestinationPath) || !system.IsWritable(cfg.DestinationPath) {
|
||||
health := system.CheckBackupDestination(cfg.DestinationPath)
|
||||
if health.Warning != "" {
|
||||
prefix := "⚠️"
|
||||
if health.Severity == "critical" {
|
||||
prefix = "🔴"
|
||||
}
|
||||
fullStatus.CrossDriveWarnings = append(fullStatus.CrossDriveWarnings,
|
||||
fmt.Sprintf("⚠️ %s mentési célja (%s) nem elérhető!", app.DisplayName, cfg.DestinationPath))
|
||||
fmt.Sprintf("%s %s: %s", prefix, app.DisplayName, health.Warning))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build unified per-app backup rows for the new UI
|
||||
data["AppBackupRows"] = s.buildAppBackupRows(fullStatus, crossConfigs, destLabels)
|
||||
|
||||
// Top-level warning: no user data backed up at all
|
||||
hasAnyCrossDrive := false
|
||||
hasAnyHDDApp := false
|
||||
for _, app := range fullStatus.AppDataInfo {
|
||||
if app.HasHDDData {
|
||||
hasAnyHDDApp = true
|
||||
if cfg, ok := crossConfigs[app.StackName]; ok && cfg != nil && cfg.Enabled {
|
||||
hasAnyCrossDrive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasAnyHDDApp && !hasAnyCrossDrive {
|
||||
data["NoUserDataBackupWarning"] = true
|
||||
}
|
||||
|
||||
data["Backup"] = fullStatus
|
||||
|
||||
// Restic password for display
|
||||
@@ -479,38 +497,177 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsAppBackupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
// AppBackupRow holds all backup information for one app, used by the backup page template.
|
||||
type AppBackupRow struct {
|
||||
StackName string
|
||||
DisplayName string
|
||||
Status string // "green", "yellow", "red", "auto"
|
||||
StatusText string // short Hungarian tooltip
|
||||
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups", http.StatusFound)
|
||||
return
|
||||
// Storage info (HDD apps only)
|
||||
HasHDDData bool
|
||||
StorageLabel string
|
||||
HDDSizeHuman string
|
||||
|
||||
// Layer details (nil = layer not applicable)
|
||||
HasDB bool
|
||||
DBLastRun string // formatted time
|
||||
DBLastStatus string // "ok", "error", ""
|
||||
|
||||
VolumeLastRun string
|
||||
VolumeLastStatus string
|
||||
|
||||
// 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...", "—"
|
||||
|
||||
// Warnings accumulated for this app
|
||||
Warnings []string
|
||||
}
|
||||
|
||||
// buildAppBackupRows constructs one AppBackupRow per deployed app for the unified backup page.
|
||||
func (s *Server) buildAppBackupRows(
|
||||
status *backup.FullBackupStatus,
|
||||
crossConfigs map[string]*settings.CrossDriveBackup,
|
||||
destLabels map[string]string,
|
||||
) []AppBackupRow {
|
||||
loc, _ := time.LoadLocation("Europe/Budapest")
|
||||
|
||||
// Build a quick lookup: which stacks have a DB dump?
|
||||
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
|
||||
}
|
||||
|
||||
// Get current app data info to know which stacks have HDD data
|
||||
nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule)
|
||||
nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule)
|
||||
fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup)
|
||||
|
||||
prefs := make(map[string]bool)
|
||||
for _, app := range fullStatus.AppDataInfo {
|
||||
if app.HasHDDData {
|
||||
prefs[app.StackName] = r.FormValue("backup_"+app.StackName) == "on"
|
||||
// Determine last restic run time for volume backup display
|
||||
volumeLastRun := ""
|
||||
volumeLastStatus := ""
|
||||
if status.LastBackup != nil {
|
||||
volumeLastRun = status.LastBackup.LastRun.In(loc).Format("01-02 15:04")
|
||||
if status.LastBackup.Success {
|
||||
volumeLastStatus = "ok"
|
||||
} else {
|
||||
volumeLastStatus = "error"
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.settings.SetAppBackupBulk(prefs); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to save app backup prefs: %v", err)
|
||||
http.Redirect(w, r, "/backups?flash_error=Hiba+a+ment%C3%A9skor", http.StatusFound)
|
||||
return
|
||||
// DB dump last run
|
||||
dbLastRun := ""
|
||||
dbLastStatus := ""
|
||||
if status.LastDBDump != nil {
|
||||
dbLastRun = status.LastDBDump.LastRun.In(loc).Format("01-02 15:04")
|
||||
if status.LastDBDump.Success {
|
||||
dbLastStatus = "ok"
|
||||
} else {
|
||||
dbLastStatus = "error"
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] App backup preferences updated: %v", prefs)
|
||||
var rows []AppBackupRow
|
||||
for _, app := range status.AppDataInfo {
|
||||
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,
|
||||
}
|
||||
|
||||
// Trigger cache refresh so the page shows updated data
|
||||
go s.backupMgr.RefreshCache(nextDBDump, nextBackup)
|
||||
// Default status = green/auto
|
||||
row.Status = "auto"
|
||||
row.StatusText = "Automatikus mentés"
|
||||
|
||||
http.Redirect(w, r, "/backups?flash=Alkalmaz%C3%A1s+ment%C3%A9si+be%C3%A1ll%C3%ADt%C3%A1sok+mentve.", http.StatusFound)
|
||||
if app.HasHDDData {
|
||||
row.HasUserData = true
|
||||
cfg, hasCfg := crossConfigs[app.StackName]
|
||||
|
||||
if !hasCfg || cfg == nil || !cfg.Enabled {
|
||||
// HDD data but no cross-drive configured → RED
|
||||
row.UserDataConfigured = false
|
||||
row.Status = "red"
|
||||
row.StatusText = "Felhasználói adatokról nincs mentés"
|
||||
} else {
|
||||
row.UserDataConfigured = true
|
||||
row.UserDataMethod = cfg.Method
|
||||
row.UserDataDest = destLabels[cfg.DestinationPath]
|
||||
if row.UserDataDest == "" {
|
||||
row.UserDataDest = cfg.DestinationPath
|
||||
}
|
||||
switch cfg.Schedule {
|
||||
case "daily":
|
||||
row.UserDataSchedule = "Naponta"
|
||||
case "weekly":
|
||||
row.UserDataSchedule = "Hetente (vasárnap)"
|
||||
default:
|
||||
row.UserDataSchedule = 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.UserDataLastStatus = cfg.LastStatus
|
||||
row.UserDataLastError = cfg.LastError
|
||||
switch cfg.LastStatus {
|
||||
case "ok":
|
||||
row.UserDataStatusBadge = "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.Status = "yellow"
|
||||
row.StatusText = "Utolsó mentés sikertelen"
|
||||
} else {
|
||||
row.Status = "green"
|
||||
row.StatusText = "Mentés rendben"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No HDD data — fully automatic
|
||||
row.Status = "auto"
|
||||
row.StatusText = "Automatikus mentés (nincs felhasználói adat)"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// settingsCrossBackupHandler handles POST /settings/cross-backup/{name}
|
||||
@@ -532,7 +689,7 @@ func (s *Server) settingsCrossBackupHandler(w http.ResponseWriter, r *http.Reque
|
||||
if method != "rsync" && method != "restic" {
|
||||
method = "rsync"
|
||||
}
|
||||
if schedule != "daily" && schedule != "weekly" && schedule != "manual" {
|
||||
if schedule != "daily" && schedule != "weekly" {
|
||||
schedule = "daily"
|
||||
}
|
||||
} else if existing != nil {
|
||||
|
||||
Reference in New Issue
Block a user