feat: Tier2 backup pauses when destination drive is inactive (Inaktív)
Deactivated drives (Schedulable=false) now treated like disconnected for Tier2 backups. New IsStoragePathSchedulable() checks active+connected+not decommissioned. UI shows yellow "Cél meghajtó inaktív" badge, scheduler skips silently with WARN log. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+6
-4
@@ -1,16 +1,18 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
### v0.32.5 — USB badge fix + graceful Tier2 backup on disconnected destinations (2026-02-27)
|
### v0.32.5 — USB badge fix + graceful Tier2 backup on disconnected/inactive/removed destinations (2026-02-27)
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
- **system/mounts_linux.go**: `IsUSBDevice()` and `diskModel()` now strip findmnt bind-mount suffix (`[/subdir]`) before parsing device path — fixes USB badge and disk model not showing for drives mounted via the attach wizard
|
- **system/mounts_linux.go**: `IsUSBDevice()` and `diskModel()` now strip findmnt bind-mount suffix (`[/subdir]`) before parsing device path — fixes USB badge and disk model not showing for drives mounted via the attach wizard
|
||||||
- **backup/crossdrive.go**: Disconnected source/destination drives now silently skip with WARN log instead of returning error — prevents noisy error aggregation in `RunAllScheduled()` and false "failed" counts
|
- **backup/crossdrive.go**: Disconnected source/destination drives now silently skip with WARN log instead of returning error — prevents noisy error aggregation in `RunAllScheduled()` and false "failed" counts
|
||||||
- **web/handlers.go + backup/crossdrive.go**: Tier2 destination check now also covers drives **removed** from storage (not just marked disconnected) — `IsStoragePathKnown()` detects when destination path is no longer in any registered storage, UI shows yellow "Cél meghajtó leválasztva" and scheduler skips silently
|
- **web/handlers.go + backup/crossdrive.go**: Tier2 destination check now covers drives **removed** from storage (not just marked disconnected) — `IsStoragePathKnown()` detects when destination path is no longer in any registered storage, UI shows yellow "Cél meghajtó leválasztva" and scheduler skips silently
|
||||||
|
- **web/handlers.go + backup/crossdrive.go**: Tier2 destination check now also covers **inactive** (Schedulable=false) drives — `IsStoragePathSchedulable()` detects when destination drive is deactivated, UI shows yellow "Cél meghajtó inaktív" and scheduler skips silently
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
- **settings/settings.go**: New `IsStoragePathKnown(path)` method — returns whether a path belongs to any registered storage (connected, disconnected, or decommissioned); paths removed entirely return false
|
- **settings/settings.go**: New `IsStoragePathKnown(path)` method — returns whether a path belongs to any registered storage (connected, disconnected, or decommissioned); paths removed entirely return false
|
||||||
- **web/handlers.go**: New `Tier2DestDisconnected` field on `AppBackupRow` — detects when a Tier2 backup destination drive is disconnected or removed, sets yellow status dot ("2. mentés szünetel") instead of red
|
- **settings/settings.go**: New `IsStoragePathSchedulable(path)` method — returns true only if path belongs to a registered, active (Schedulable), non-disconnected, non-decommissioned storage
|
||||||
- **web/templates/backups.html**: New template branch for disconnected Tier2 destinations — shows "Cél meghajtó leválasztva" warning badge with grayed-out info, hides "Futtatás most" button
|
- **web/handlers.go**: New `Tier2DestDisconnected` and `Tier2DestInactive` fields on `AppBackupRow` — detect when Tier2 destination is disconnected/removed/inactive, sets yellow status dot instead of green/red
|
||||||
|
- **web/templates/backups.html**: New template branches for disconnected ("Cél meghajtó leválasztva") and inactive ("Cél meghajtó inaktív") Tier2 destinations — grayed-out info, warning badge, no "Futtatás most" button
|
||||||
|
|
||||||
### v0.32.4 — Controller telemetry: include controller in hub app telemetry (2026-02-27)
|
### v0.32.4 — Controller telemetry: include controller in hub app telemetry (2026-02-27)
|
||||||
|
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ Unified per-app status table with expandable rows showing **per-tier** backup st
|
|||||||
| Dot color | Meaning |
|
| Dot color | Meaning |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| Green | 2+ tiers configured with successful backups + destination healthy |
|
| Green | 2+ tiers configured with successful backups + destination healthy |
|
||||||
| Yellow | Only 1 tier, or Tier 2 failing, or Tier 2 configured but never run |
|
| Yellow | Only 1 tier, or Tier 2 failing, or Tier 2 configured but never run, or destination disconnected/inactive |
|
||||||
| Red | Tier 2 destination blocked or inaccessible |
|
| Red | Tier 2 destination blocked or inaccessible |
|
||||||
|
|
||||||
Every app starts as yellow (1 tier only). Green requires Tier 2 configured with successful backup.
|
Every app starts as yellow (1 tier only). Green requires Tier 2 configured with successful backup.
|
||||||
@@ -570,15 +570,16 @@ Continuously monitors registered storage paths for disconnection/reconnection (p
|
|||||||
|
|
||||||
**USB detection** (`system.IsUSBDevice`): Reads `/host/sys/block/<disk>` symlink — if target path contains `/usb`, it's a USB device. The `removable` sysfs flag is unreliable for USB HDDs (returns 0). USB drives show an orange "USB" badge on their storage card alongside Aktív/Alapértelmezett badges (v0.27.2). Handles findmnt bind-mount suffix stripping (`/dev/sdb1[/subdir]` → `/dev/sdb1`) for attach-wizard drives (v0.32.5).
|
**USB detection** (`system.IsUSBDevice`): Reads `/host/sys/block/<disk>` symlink — if target path contains `/usb`, it's a USB device. The `removable` sysfs flag is unreliable for USB HDDs (returns 0). USB drives show an orange "USB" badge on their storage card alongside Aktív/Alapértelmezett badges (v0.27.2). Handles findmnt bind-mount suffix stripping (`/dev/sdb1[/subdir]` → `/dev/sdb1`) for attach-wizard drives (v0.32.5).
|
||||||
|
|
||||||
**Backup guards**: Nightly DB dumps, restic snapshots, and cross-drive backups all skip disconnected drives with WARN log (not treated as failures). Cross-drive `RunAppBackup()` returns nil (not error) for disconnected source/destination — prevents noisy error aggregation in scheduled runs (v0.32.5).
|
**Backup guards**: Nightly DB dumps, restic snapshots, and cross-drive backups all skip disconnected, removed, and inactive drives with WARN log (not treated as failures). Cross-drive `RunAppBackup()` returns nil (not error) for unavailable destinations — prevents noisy error aggregation in scheduled runs (v0.32.5).
|
||||||
|
|
||||||
**Tier2 destination disconnected (v0.32.5)**: When a Tier2 backup destination drive is disconnected, the backup page shows:
|
**Tier2 destination unavailable (v0.32.5)**: When a Tier2 backup destination drive is disconnected, removed from storage, or deactivated (Inaktív), the backup page shows:
|
||||||
- Yellow status dot with "2. mentés szünetel — cél meghajtó leválasztva" tooltip (not red)
|
- Yellow status dot with "2. mentés szünetel" tooltip (not red)
|
||||||
- "Cél meghajtó leválasztva" warning badge on the Tier2 row
|
- Warning badge: "Cél meghajtó leválasztva" (disconnected/removed) or "Cél meghajtó inaktív" (deactivated)
|
||||||
- Grayed-out last-run info and backup contents
|
- Grayed-out last-run info and backup contents
|
||||||
- Hidden "Futtatás most" button (prevents futile manual triggers)
|
- Hidden "Futtatás most" button (prevents futile manual triggers)
|
||||||
- "Beállítás" link preserved for reconfiguration
|
- "Beállítás" link preserved for reconfiguration
|
||||||
- Tier2 config persists through disconnect/reconnect — backups auto-resume when drive returns
|
- Tier2 config persists — backups auto-resume when drive returns/reactivates
|
||||||
|
- Detection: `IsStoragePathKnown()` catches removed paths, `IsStoragePathSchedulable()` catches inactive/disconnected/decommissioned
|
||||||
|
|
||||||
**UI integration**: Disconnected drives show with hatched red bars on dashboard, monitoring, and backup pages. Per-app backup rows show "Meghajtó leválasztva" badge. Health check emits warnings for disconnected paths.
|
**UI integration**: Disconnected drives show with hatched red bars on dashboard, monitoring, and backup pages. Per-app backup rows show "Meghajtó leválasztva" badge. Health check emits warnings for disconnected paths.
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
|
|||||||
r.logger.Printf("[WARN] [backup] Cross-drive backup skipped for %s: destination not a registered storage (%s)", stackName, cfg.DestinationPath)
|
r.logger.Printf("[WARN] [backup] Cross-drive backup skipped for %s: destination not a registered storage (%s)", stackName, cfg.DestinationPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if !r.sett.IsStoragePathSchedulable(cfg.DestinationPath) {
|
||||||
|
r.logger.Printf("[WARN] [backup] Cross-drive backup skipped for %s: destination drive inactive (%s)", stackName, cfg.DestinationPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as running in settings
|
// Mark as running in settings
|
||||||
_ = r.sett.UpdateCrossDriveStatus(stackName, func(c *settings.CrossDriveBackup) {
|
_ = r.sett.UpdateCrossDriveStatus(stackName, func(c *settings.CrossDriveBackup) {
|
||||||
|
|||||||
@@ -707,6 +707,20 @@ func (s *Settings) IsStoragePathKnown(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStoragePathSchedulable returns whether a path belongs to a registered,
|
||||||
|
// schedulable (active) storage path. Returns false if the path is unknown,
|
||||||
|
// disconnected, decommissioned, or inactive.
|
||||||
|
func (s *Settings) IsStoragePathSchedulable(path string) bool {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
for _, sp := range s.StoragePaths {
|
||||||
|
if path == sp.Path || strings.HasPrefix(path, sp.Path+"/") {
|
||||||
|
return sp.Schedulable && !sp.Disconnected && !sp.Decommissioned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GetStoppedStacks returns the list of stacks that were auto-stopped for a storage path.
|
// GetStoppedStacks returns the list of stacks that were auto-stopped for a storage path.
|
||||||
func (s *Settings) GetStoppedStacks(path string) []string {
|
func (s *Settings) GetStoppedStacks(path string) []string {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
|
|||||||
@@ -787,6 +787,8 @@ type AppBackupRow struct {
|
|||||||
DriveDisconnected bool
|
DriveDisconnected bool
|
||||||
// Tier2 destination drive is currently disconnected (backup paused, not failed)
|
// Tier2 destination drive is currently disconnected (backup paused, not failed)
|
||||||
Tier2DestDisconnected bool
|
Tier2DestDisconnected bool
|
||||||
|
// Tier2 destination drive is inactive (Schedulable=false, backup paused)
|
||||||
|
Tier2DestInactive bool
|
||||||
|
|
||||||
// Warnings accumulated for this app
|
// Warnings accumulated for this app
|
||||||
Warnings []string
|
Warnings []string
|
||||||
@@ -945,10 +947,21 @@ func (s *Server) buildAppBackupRows(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if Tier2 destination drive is inactive (not schedulable)
|
||||||
|
if cfg.DestinationPath != "" && !row.Tier2DestDisconnected {
|
||||||
|
if !s.settings.IsStoragePathSchedulable(cfg.DestinationPath) {
|
||||||
|
row.Tier2DestInactive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if row.Tier2DestDisconnected {
|
if row.Tier2DestDisconnected {
|
||||||
// Disconnected destination — treat as paused, not failed
|
// Disconnected destination — treat as paused, not failed
|
||||||
row.Status = "yellow"
|
row.Status = "yellow"
|
||||||
row.StatusText = "2. mentés szünetel — cél meghajtó leválasztva"
|
row.StatusText = "2. mentés szünetel — cél meghajtó leválasztva"
|
||||||
|
} else if row.Tier2DestInactive {
|
||||||
|
// Inactive destination — treat as paused
|
||||||
|
row.Status = "yellow"
|
||||||
|
row.StatusText = "2. mentés szünetel — cél meghajtó inaktív"
|
||||||
} else if cfg.DestinationPath != "" && s.crossDriveRunner != nil {
|
} else if cfg.DestinationPath != "" && s.crossDriveRunner != nil {
|
||||||
// Destination health check — can downgrade green to yellow/red
|
// Destination health check — can downgrade green to yellow/red
|
||||||
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
|
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
|
||||||
|
|||||||
@@ -306,6 +306,17 @@
|
|||||||
<div class="layer-actions">
|
<div class="layer-actions">
|
||||||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
|
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{else if and .Tier2Configured .Tier2DestInactive}}
|
||||||
|
<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ó inaktív</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}}
|
{{else if .Tier2Configured}}
|
||||||
<span class="layer-method">rsync</span>
|
<span class="layer-method">rsync</span>
|
||||||
<span class="layer-dest">→ {{.Tier2Dest}}</span>
|
<span class="layer-dest">→ {{.Tier2Dest}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user