From f19c6fb0c95add7a358a452d636591d91e32bee8 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 27 Feb 2026 09:59:29 +0100 Subject: [PATCH] fix: USB badge detection for bind-mounted drives + graceful Tier2 backup on disconnected destinations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IsUSBDevice/diskModel: strip findmnt bind-mount suffix [/subdir] before parsing device path (fixes USB badge not showing for attach-wizard drives) - crossdrive.go: skip disconnected src/dest drives with WARN log instead of returning error (prevents noisy error status in settings.json) - handlers.go: detect Tier2 destination disconnection, set yellow status dot instead of red, skip ValidateDestination for disconnected paths - backups.html: new template branch showing "Cél meghajtó leválasztva" badge with grayed-out info and hidden "Futtatás most" button Co-Authored-By: Claude Opus 4.6 --- controller/internal/backup/crossdrive.go | 20 +++++-------------- controller/internal/system/mounts_linux.go | 9 +++++++++ controller/internal/web/handlers.go | 20 +++++++++++++++++-- .../internal/web/templates/backups.html | 13 +++++++++++- 4 files changed, 44 insertions(+), 18 deletions(-) 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}}