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:
2026-02-17 17:56:28 +01:00
parent e002d712cf
commit 1de244646b
10 changed files with 637 additions and 154 deletions
@@ -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/<dev>/device/model.
func diskModel(device string) string {
// /dev/sda1 → sda, /dev/nvme0n1p1 → nvme0n1
@@ -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",
}
}