diff --git a/CHANGELOG.md b/CHANGELOG.md index 971227c..192150f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## Changelog +### What was just completed (2026-02-17 session 37) +- **v0.12.0 — Backup Page Overhaul — Unified App Backup Status & Bug Fixes:** + - **Bug Fix 1: Duplicate unconfigured apps** — `GetFullStatus()` now returns a deep copy of the cached status. `CrossDriveSummary`, `UnconfiguredApps`, and `CrossDriveWarnings` slices are always nil in the returned copy so the handler builds them fresh on every page load. Previously the handler appended to the cached slices, causing 3× duplication on 3 page loads. + - **Bug Fix 2: Misleading "drive disconnected" error** — Replaced the binary `IsMountPoint || !IsWritable` check with tiered `CheckBackupDestination()` validation (new in `internal/system/mounts_linux.go` and stub in `mounts_other.go`). Tiers: path doesn't exist (critical/blocked), not writable (critical/blocked), same block device as `/` (warning/allowed with note about system drive), disk >95% full (critical/blocked), disk >90% (warning/allowed). `isSameBlockDevice()` replaces `IsMountPoint()` for source/dest same-device detection. Used in both `deployHandler()` and `backupsHandler()` for display, and in `crossdrive.go` logic via `CheckBackupDestination()`. + - **Bug Fix 3: Dead BackupEnabled toggle** — Removed `settingsAppBackupHandler()` from handlers.go and its `POST /settings/app-backup` route from server.go. The toggle wrote to settings.json but nothing read it to skip apps. UI nightly backup section in deploy.html now shows an informational note instead of the toggle. + - **Architecture: Unified per-app backup rows** — New `AppBackupRow` struct and `buildAppBackupRows()` in handlers.go. Replaces old "Alkalmazás adatok" + "Másolatok másik meghajtóra" sections with a single expandable row per app showing all 3 backup layers (DB, Docker volumes, user data). Status dot: green=fully covered, yellow=warning (failed run, system drive, disk full), red=HDD data without cross-drive configured, auto=no user data. Expandable JS toggle with ▶/▼ icon. + - **Architecture: Sequential backup chaining** — Removed independent `cross-drive-daily` (03:30) and `cross-drive-weekly` (04:30) scheduler jobs. Cross-drive backups now run immediately after the restic backup completes (daily jobs every night; weekly jobs on Sunday). This ensures DB dump → restic → cross-drive happen in the same window for file/DB consistency on restore. + - **Architecture: Deploy page schedule dropdown** — Removed "Csak kézi indítás" option (schedule="manual"). Two options remain: "Naponta (az éjszakai mentés után)" and "Hetente, vasárnap (az éjszakai mentés után)". Weekly option shows informational note about DB consistency implications. Existing "manual" configs treated as "weekly" in the dropdown. + - **CSS added:** `.app-backup-row`, `.app-backup-row-header`, `.app-backup-row-name`, `.app-backup-row-meta`, `.app-backup-row-detail`, `.status-dot` (green/yellow/red/auto), `.backup-layers`, `.backup-layer-row`, `.layer-label`, `.layer-badge`, `.layer-na`, `.layer-method`, `.layer-dest`, `.layer-schedule`, `.layer-last`, `.layer-unconfigured`, `.layer-actions`, `.layer-warnings`, `.backup-layer-warning`, `.btn-xs`, `.text-ok`, `.text-error`. + - **Files modified (9):** `internal/backup/backup.go`, `internal/system/mounts_linux.go`, `internal/system/mounts_other.go`, `internal/web/handlers.go`, `internal/web/server.go`, `internal/web/templates/backups.html`, `internal/web/templates/deploy.html`, `internal/web/templates/style.css`, `cmd/controller/main.go` + ### What was just completed (2026-02-17 session 36) - **v0.11.9 — UI Polish Fixes for deploy/settings backup section:** - **Fix 1: Spacing** — `.deploy-cross-drive` `margin-bottom` increased from `1rem` to `1.5rem` for consistent spacing before deploy form. diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 63ec111..92c8bb3 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -192,6 +192,18 @@ func main() { if err != nil { notifier.NotifyBackupFailed("Biztonsági mentés sikertelen", err.Error()) } + // Phase 3: Chain cross-drive backups immediately after restic (regardless of restic success) + // Daily jobs run every night; weekly jobs only on Sunday + if crossDriveRunner != nil { + if cdErr := crossDriveRunner.RunAllScheduled(ctx, "daily"); cdErr != nil { + logger.Printf("[WARN] Cross-drive daily backup had errors: %v", cdErr) + } + if time.Now().Weekday() == time.Sunday { + if cdErr := crossDriveRunner.RunAllScheduled(ctx, "weekly"); cdErr != nil { + logger.Printf("[WARN] Cross-drive weekly backup had errors: %v", cdErr) + } + } + } return err }) @@ -216,18 +228,6 @@ func main() { }) } - // Cross-drive backup — daily at 03:30 (after main backup at 03:00) - sched.Daily("cross-drive-daily", "03:30", func(ctx context.Context) error { - return crossDriveRunner.RunAllScheduled(ctx, "daily") - }) - // Cross-drive weekly — Sunday 04:30 (after integrity check at 04:00) - sched.Daily("cross-drive-weekly", "04:30", func(ctx context.Context) error { - if time.Now().Weekday() != time.Sunday { - return nil - } - return crossDriveRunner.RunAllScheduled(ctx, "weekly") - }) - // Metrics prune — daily at 04:00 if metricsStore != nil { sched.Daily("metrics-prune", "04:00", func(ctx context.Context) error { diff --git a/controller/internal/backup/backup.go b/controller/internal/backup/backup.go index 571ad64..1191b73 100644 --- a/controller/internal/backup/backup.go +++ b/controller/internal/backup/backup.go @@ -585,29 +585,43 @@ func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) { // GetFullStatus returns the cached backup status for page rendering. // Returns instantly — no subprocess calls. +// Returns a deep copy so callers can safely append to slice fields without +// polluting the cache (which would cause duplicate entries on repeated calls). func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus { m.mu.Lock() defer m.mu.Unlock() if m.cachedStatus != nil { + // Deep copy — callers (backupsHandler) append to CrossDriveSummary, + // UnconfiguredApps, and CrossDriveWarnings. If we returned the cache + // pointer directly, every page load would accumulate more entries. + status := *m.cachedStatus + status.AppDataInfo = make([]AppBackupInfo, len(m.cachedStatus.AppDataInfo)) + copy(status.AppDataInfo, m.cachedStatus.AppDataInfo) + // These three slices are assembled by the handler from AppDataInfo + settings; + // they must always start empty so the handler builds them fresh. + status.CrossDriveSummary = nil + status.UnconfiguredApps = nil + status.CrossDriveWarnings = nil + // Update dynamic fields that don't need subprocess calls - m.cachedStatus.Running = m.running - m.cachedStatus.NextDBDump = nextDBDump - m.cachedStatus.NextBackup = nextBackup - m.cachedStatus.LastDBDump = m.lastDBDump - m.cachedStatus.LastBackup = m.lastBackup + status.Running = m.running + status.NextDBDump = nextDBDump + status.NextBackup = nextBackup + status.LastDBDump = m.lastDBDump + status.LastBackup = m.lastBackup // Update snapshot history - m.cachedStatus.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory)) - copy(m.cachedStatus.SnapshotHistory, m.snapshotHistory) + status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory)) + copy(status.SnapshotHistory, m.snapshotHistory) // Reverse so newest first - for i, j := 0, len(m.cachedStatus.SnapshotHistory)-1; i < j; i, j = i+1, j-1 { - m.cachedStatus.SnapshotHistory[i], m.cachedStatus.SnapshotHistory[j] = m.cachedStatus.SnapshotHistory[j], m.cachedStatus.SnapshotHistory[i] + for i, j := 0, len(status.SnapshotHistory)-1; i < j; i, j = i+1, j-1 { + status.SnapshotHistory[i], status.SnapshotHistory[j] = status.SnapshotHistory[j], status.SnapshotHistory[i] } // Synthesize LastBackup from snapshot history if not in memory (e.g., after restart) - if m.cachedStatus.LastBackup == nil && len(m.cachedStatus.SnapshotHistory) > 0 { - latest := m.cachedStatus.SnapshotHistory[0] // already reversed, newest first - m.cachedStatus.LastBackup = &BackupStatus{ + if status.LastBackup == nil && len(status.SnapshotHistory) > 0 { + latest := status.SnapshotHistory[0] // already reversed, newest first + status.LastBackup = &BackupStatus{ LastRun: latest.Time, Success: latest.Success, Snapshot: &SnapshotResult{ @@ -617,10 +631,10 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta } // Synthesize LastDBDump from DumpFiles on disk if not in memory - if m.cachedStatus.LastDBDump == nil && len(m.cachedStatus.DumpFiles) > 0 { + if status.LastDBDump == nil && len(status.DumpFiles) > 0 { var results []DumpResult var latestTime time.Time - for _, f := range m.cachedStatus.DumpFiles { + for _, f := range status.DumpFiles { results = append(results, DumpResult{ DB: DiscoveredDB{StackName: f.StackName, DBType: f.DBType, ContainerName: f.StackName}, FilePath: f.FileName, @@ -630,14 +644,14 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta latestTime = f.ModTime } } - m.cachedStatus.LastDBDump = &DBDumpStatus{ + status.LastDBDump = &DBDumpStatus{ LastRun: latestTime, Results: results, Success: true, } } - return m.cachedStatus + return &status } // No cache yet — return a minimal status (first page load before cache is populated) diff --git a/controller/internal/system/mounts_linux.go b/controller/internal/system/mounts_linux.go index 1222622..fbcefd1 100644 --- a/controller/internal/system/mounts_linux.go +++ b/controller/internal/system/mounts_linux.go @@ -118,6 +118,85 @@ func GetFSInfo(path string) *FSInfo { return info } +// DestinationHealth holds the result of a tiered backup destination check. +type DestinationHealth struct { + Exists bool + Writable bool + MountPoint bool // true if path is on a different device from its parent + SystemDrive bool // true if path is on the same device as / + UsedPercent float64 // disk usage percentage (0 if unknown) + FreeGB float64 + Warning string // human-readable warning message in Hungarian (empty = ok) + Blocked bool // if true, backup must not run + Severity string // "ok", "warning", "critical" +} + +// CheckBackupDestination performs tiered validation of a cross-drive backup destination. +// Returns a DestinationHealth describing any issues found. +func CheckBackupDestination(path string) DestinationHealth { + h := DestinationHealth{Severity: "ok"} + + // Tier 1: path must exist + if _, err := os.Stat(path); os.IsNotExist(err) { + h.Warning = "A cél tárhely (" + path + ") nem létezik!" + h.Blocked = true + h.Severity = "critical" + return h + } + h.Exists = true + + // Tier 2: path must be writable + if !IsWritable(path) { + h.Warning = "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat." + h.Blocked = true + h.Severity = "critical" + return h + } + h.Writable = true + + // Tier 3: detect if source and destination are on the same block device + // (stronger than IsMountPoint — catches e.g. bind mounts within same device) + if isSameBlockDevice(path, "/") { + h.SystemDrive = true + // This is a warning, not a block — user data still protected against software errors + h.Warning = "A cél tárhely (" + path + ") a rendszermeghajtón van. " + + "Meghajtóhiba esetén az eredeti adat és a mentés is elveszhet. " + + "Külső meghajtó használata javasolt." + h.Severity = "warning" + // Don't return early — also check disk usage + } else { + h.MountPoint = true + } + + // Tier 4: disk usage checks + if di := GetDiskUsage(path); di != nil { + h.UsedPercent = di.UsedPercent + h.FreeGB = di.AvailGB + if di.UsedPercent >= 95 { + h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent) + h.Blocked = true + h.Severity = "critical" + } else if di.UsedPercent >= 90 && h.Severity == "ok" { + h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent) + h.Severity = "warning" + } + } + + return h +} + +// isSameBlockDevice returns true if pathA and pathB are on the same block device. +func isSameBlockDevice(pathA, pathB string) bool { + var statA, statB syscall.Stat_t + if err := syscall.Stat(pathA, &statA); err != nil { + return false + } + if err := syscall.Stat(pathB, &statB); err != nil { + return false + } + return statA.Dev == statB.Dev +} + // diskModel reads the disk model from /sys/block//device/model. func diskModel(device string) string { // /dev/sda1 → sda, /dev/nvme0n1p1 → nvme0n1 diff --git a/controller/internal/system/mounts_other.go b/controller/internal/system/mounts_other.go index b0c7243..fa61dbf 100644 --- a/controller/internal/system/mounts_other.go +++ b/controller/internal/system/mounts_other.go @@ -57,3 +57,26 @@ type FSInfo struct { // GetFSInfo returns nil on non-Linux. func GetFSInfo(_ string) *FSInfo { return nil } + +// DestinationHealth holds the result of a tiered backup destination check. +type DestinationHealth struct { + Exists bool + Writable bool + MountPoint bool + SystemDrive bool + UsedPercent float64 + FreeGB float64 + Warning string + Blocked bool + Severity string +} + +// CheckBackupDestination always returns ok on non-Linux (assume healthy for dev/testing). +func CheckBackupDestination(path string) DestinationHealth { + return DestinationHealth{ + Exists: true, + Writable: true, + MountPoint: true, + Severity: "ok", + } +} diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index dbca212..55d5e90 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -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 { diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index 09efc93..2fb404d 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -110,8 +110,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.settingsStorageSchedulableHandler(w, r) case path == "/settings/storage/label" && r.Method == http.MethodPost: s.settingsStorageLabelHandler(w, r) - case path == "/settings/app-backup" && r.Method == http.MethodPost: - s.settingsAppBackupHandler(w, r) case strings.HasPrefix(path, "/settings/cross-backup/") && r.Method == http.MethodPost: name := strings.TrimPrefix(path, "/settings/cross-backup/") s.settingsCrossBackupHandler(w, r, name) diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html index 07f1262..d44f0cb 100644 --- a/controller/internal/web/templates/backups.html +++ b/controller/internal/web/templates/backups.html @@ -233,84 +233,109 @@ {{end}} - + {{if .Backup.AppDataInfo}}
-

Alkalmazás adatok

-

Az alkalmazások felhasználói adatainak mentési állapota. Beállítás az alkalmazás oldalán.

-
- {{range .Backup.AppDataInfo}} -
-
- {{.DisplayName}} -
- {{if .HasHDDData}} - {{if .StorageLabel}}{{.StorageLabel}}{{end}} - {{if .BackupEnabled}} - {{.HDDSizeHuman}} - Aktív - {{else}} - Inaktív +

Alkalmazások mentési állapota

+ + {{if .NoUserDataBackupWarning}} +
+ Felhasználói adatokról nincs biztonsági mentés.
+ A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg csak egy példányban léteznek. + Külső meghajtó csatlakoztatásával biztonsági másolat készíthető a 3-2-1 szabály szerint. + Meghajtó beállítása → +
+ {{end}} + + {{range .AppBackupRows}} +
+
+ + {{.DisplayName}} +
+ {{if .HasHDDData}} + {{if .StorageLabel}}{{.StorageLabel}}{{end}} + {{.HDDSizeHuman}} + {{else}} + Auto + {{end}} +
+ +
+ - {{end}} -
-
-{{end}} - - -{{if or .Backup.CrossDriveSummary .Backup.UnconfiguredApps}} -
-

Másolatok másik meghajtóra

-

Alkalmazás adatok biztonsági másolata külső meghajtóra (3-2-1 szabály).

- - {{if .Backup.CrossDriveWarnings}} -
- {{range .Backup.CrossDriveWarnings}} -
{{.}}
- {{end}}
{{end}} {{if .Backup.CrossDriveSummary}} -
- {{range .Backup.CrossDriveSummary}} -
-
- {{.DisplayName}} -
- {{.MethodLabel}} - {{if .DestLabel}}→ {{.DestLabel}} - {{else if .DestPath}}→ {{.DestPath}}{{end}} - {{if eq .LastStatus "ok"}}{{.LastRunShort}} - {{else if eq .LastStatus "error"}}Hiba - {{else if eq .LastStatus "running"}}Fut... - {{else}}{{.ScheduleLabel}}{{end}} - {{if .SizeHuman}}{{.SizeHuman}}{{end}} -
-
-
- {{end}} +
+
{{end}} - - {{if .Backup.UnconfiguredApps}} -
- {{len .Backup.UnconfiguredApps}} alkalmazáshoz nincs beállítva: - {{range .Backup.UnconfiguredApps}} - {{.DisplayName}} - {{end}} -
- {{end}} - -
- -
{{end}} @@ -463,6 +488,40 @@ {{end}}