From 9b13c0e21cff3a46188172cc14560f6a69ab649a Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 27 Feb 2026 10:59:56 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Tier2=20backup=20pauses=20when=20destin?= =?UTF-8?q?ation=20drive=20is=20inactive=20(Inakt=C3=ADv)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 10 ++++++---- controller/README.md | 13 +++++++------ controller/internal/backup/crossdrive.go | 4 ++++ controller/internal/settings/settings.go | 14 ++++++++++++++ controller/internal/web/handlers.go | 13 +++++++++++++ controller/internal/web/templates/backups.html | 11 +++++++++++ 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f82eef3..e3669b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,18 @@ ## 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 - **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 -- **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 - **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 -- **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 +- **settings/settings.go**: New `IsStoragePathSchedulable(path)` method — returns true only if path belongs to a registered, active (Schedulable), non-disconnected, non-decommissioned storage +- **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) diff --git a/controller/README.md b/controller/README.md index 92a9137..69232ea 100644 --- a/controller/README.md +++ b/controller/README.md @@ -434,7 +434,7 @@ Unified per-app status table with expandable rows showing **per-tier** backup st | Dot color | Meaning | |-----------|---------| | 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 | 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/` 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: -- Yellow status dot with "2. mentés szünetel — cél meghajtó leválasztva" tooltip (not red) -- "Cél meghajtó leválasztva" warning badge on the Tier2 row +**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" tooltip (not red) +- Warning badge: "Cél meghajtó leválasztva" (disconnected/removed) or "Cél meghajtó inaktív" (deactivated) - Grayed-out last-run info and backup contents - Hidden "Futtatás most" button (prevents futile manual triggers) - "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. diff --git a/controller/internal/backup/crossdrive.go b/controller/internal/backup/crossdrive.go index c05f37a..040d294 100644 --- a/controller/internal/backup/crossdrive.go +++ b/controller/internal/backup/crossdrive.go @@ -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) 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 _ = r.sett.UpdateCrossDriveStatus(stackName, func(c *settings.CrossDriveBackup) { diff --git a/controller/internal/settings/settings.go b/controller/internal/settings/settings.go index 76f438b..c36f030 100644 --- a/controller/internal/settings/settings.go +++ b/controller/internal/settings/settings.go @@ -707,6 +707,20 @@ func (s *Settings) IsStoragePathKnown(path string) bool { 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. func (s *Settings) GetStoppedStacks(path string) []string { s.mu.RLock() diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index e9355b9..dc7109b 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -787,6 +787,8 @@ type AppBackupRow struct { DriveDisconnected bool // Tier2 destination drive is currently disconnected (backup paused, not failed) Tier2DestDisconnected bool + // Tier2 destination drive is inactive (Schedulable=false, backup paused) + Tier2DestInactive bool // Warnings accumulated for this app 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 { // Disconnected destination — treat as paused, not failed row.Status = "yellow" 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 { // Destination health check — can downgrade green to yellow/red if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil { diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html index d44bbc4..0d0b2dd 100644 --- a/controller/internal/web/templates/backups.html +++ b/controller/internal/web/templates/backups.html @@ -306,6 +306,17 @@ + {{else if and .Tier2Configured .Tier2DestInactive}} + rsync + → {{.Tier2Dest}} + Cél meghajtó inaktív + {{if .Tier2LastRun}} + Utolsó: {{.Tier2LastRun}} + {{end}} + {{.BackupContents}} + {{else if .Tier2Configured}} rsync → {{.Tier2Dest}}