fix: USB badge detection for bind-mounted drives + graceful Tier2 backup on disconnected destinations
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -248,6 +248,10 @@ func stripPartition(base string) string {
|
||||
|
||||
// diskModel reads the disk model from /sys/block/<dev>/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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -295,7 +295,18 @@
|
||||
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
|
||||
<div class="backup-layer-row">
|
||||
<span class="tier-label">2. mentés</span>
|
||||
{{if .Tier2Configured}}
|
||||
{{if and .Tier2Configured .Tier2DestDisconnected}}
|
||||
<span class="layer-method" style="opacity:.6">rsync</span>
|
||||
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
|
||||
<span class="badge badge-warn" style="font-size:.7rem">Cél meghajtó leválasztva</span>
|
||||
{{if .Tier2LastRun}}
|
||||
<span class="layer-last" style="opacity:.6">Utolsó: {{.Tier2LastRun}}</span>
|
||||
{{end}}
|
||||
<span class="tier-contents" style="opacity:.6">{{.BackupContents}}</span>
|
||||
<div class="layer-actions">
|
||||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
|
||||
</div>
|
||||
{{else if .Tier2Configured}}
|
||||
<span class="layer-method">rsync</span>
|
||||
<span class="layer-dest">→ {{.Tier2Dest}}</span>
|
||||
<span class="layer-schedule">{{.Tier2Schedule}}</span>
|
||||
|
||||
Reference in New Issue
Block a user