diff --git a/controller/internal/backup/crossdrive.go b/controller/internal/backup/crossdrive.go index e2aabd9..901b122 100644 --- a/controller/internal/backup/crossdrive.go +++ b/controller/internal/backup/crossdrive.go @@ -90,25 +90,15 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e r.mu.Unlock() }() - // Check if source or destination drive is disconnected + // Check if source or destination drive is disconnected — skip silently (not an error) srcDrive := r.stackProvider.GetStackHDDPath(stackName) if srcDrive != "" && r.sett.IsDisconnected(srcDrive) { - if r.debug { - r.logger.Printf("[DEBUG] RunAppBackup: source drive disconnected for %s: %s", stackName, srcDrive) - } - r.mu.Lock() - r.running[stackName] = false - r.mu.Unlock() - return fmt.Errorf("source drive disconnected: %s", srcDrive) + r.logger.Printf("[WARN] [backup] Cross-drive backup skipped for %s: source drive disconnected (%s)", stackName, srcDrive) + return nil } if r.sett.IsDisconnected(cfg.DestinationPath) { - if r.debug { - r.logger.Printf("[DEBUG] RunAppBackup: destination drive disconnected for %s: %s", stackName, cfg.DestinationPath) - } - r.mu.Lock() - r.running[stackName] = false - r.mu.Unlock() - return fmt.Errorf("destination drive disconnected: %s", cfg.DestinationPath) + r.logger.Printf("[WARN] [backup] Cross-drive backup skipped for %s: destination drive disconnected (%s)", stackName, cfg.DestinationPath) + return nil } // Mark as running in settings diff --git a/controller/internal/system/mounts_linux.go b/controller/internal/system/mounts_linux.go index 369bb7b..26d8953 100644 --- a/controller/internal/system/mounts_linux.go +++ b/controller/internal/system/mounts_linux.go @@ -248,6 +248,10 @@ func stripPartition(base string) string { // diskModel reads the disk model from /sys/block//device/model. func diskModel(device string) string { + // Strip bind-mount subdir suffix (e.g., "/dev/sdb1[/felhom_data]" → "/dev/sdb1") + if idx := strings.IndexByte(device, '['); idx >= 0 { + device = device[:idx] + } disk := stripPartition(filepath.Base(device)) modelPath := "/sys/block/" + disk + "/device/model" data, err := os.ReadFile(modelPath) @@ -321,8 +325,13 @@ func ProbeStoragePath(path string) ProbeResult { // IsUSBDevice checks if a block device is connected via USB. // devicePath should be like "/dev/sdb" or "/dev/sdb1". +// Also handles bind-mount suffixes from findmnt (e.g., "/dev/sdb1[/felhom_data]"). // Checks the sysfs symlink for the disk — if the path contains "/usb", it's a USB device. func IsUSBDevice(devicePath string) bool { + // Strip bind-mount subdir suffix (e.g., "/dev/sdb1[/felhom_data]" → "/dev/sdb1") + if idx := strings.IndexByte(devicePath, '['); idx >= 0 { + devicePath = devicePath[:idx] + } disk := stripPartition(filepath.Base(devicePath)) if disk == "" { return false diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index 1aa1c05..a5846db 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -785,6 +785,8 @@ type AppBackupRow struct { // Drive disconnected — app's home drive is currently disconnected DriveDisconnected bool + // Tier2 destination drive is currently disconnected (backup paused, not failed) + Tier2DestDisconnected bool // Warnings accumulated for this app Warnings []string @@ -926,8 +928,22 @@ func (s *Server) buildAppBackupRows( // Tier2 configured but never run — stay yellow } - // Destination health check — can downgrade green to yellow/red - if cfg.DestinationPath != "" && s.crossDriveRunner != nil { + // Check if Tier2 destination drive is disconnected + if cfg.DestinationPath != "" { + for dp := range disconnectedPaths { + if cfg.DestinationPath == dp || strings.HasPrefix(cfg.DestinationPath, dp+"/") { + row.Tier2DestDisconnected = true + break + } + } + } + + if row.Tier2DestDisconnected { + // Disconnected destination — treat as paused, not failed + row.Status = "yellow" + row.StatusText = "2. mentés szünetel — cél meghajtó leválasztva" + } else if cfg.DestinationPath != "" && s.crossDriveRunner != nil { + // Destination health check — can downgrade green to yellow/red 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" diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html index 1497a68..d44bbc4 100644 --- a/controller/internal/web/templates/backups.html +++ b/controller/internal/web/templates/backups.html @@ -295,7 +295,18 @@
2. mentés - {{if .Tier2Configured}} + {{if and .Tier2Configured .Tier2DestDisconnected}} + rsync + → {{.Tier2Dest}} + Cél meghajtó leválasztva + {{if .Tier2LastRun}} + Utolsó: {{.Tier2LastRun}} + {{end}} + {{.BackupContents}} + + {{else if .Tier2Configured}} rsync → {{.Tier2Dest}} {{.Tier2Schedule}}