controller: F7 atomic volume dumps + F6 no-single-copy + F5 stale-primary sweep (WIP, pre-build)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
2026-07-12 09:00:16 +02:00
parent b389e73fff
commit 68b3a3932e
13 changed files with 569 additions and 57 deletions
+11 -5
View File
@@ -206,19 +206,25 @@ func (m *Manager) RunAllTier2() {
}
var n int
for _, stack := range m.stackProvider.ListDeployedStacks() {
if m.stackProvider.GetStackHDDPath(stack.Name) == "" {
continue // not an HDD app — its data is on the rootfs, covered by PBS
}
// F6 (CAMPAIGN-3): volume-only apps (no HDD_PATH, backups on sys_drive) previously got NO
// tier-2 copy — a single controller-level copy on one device. They now flow through too: their
// recovery unit (which holds the db/volume dumps) gets a cross-drive second copy like any HDD
// app. Apps with no recovery unit yet (infra/never-backed-up) are a cheap no-op inside RunTier2
// (the `os.Stat(unitDir)` guard), so this doesn't spuriously copy protected stacks.
if m.settings != nil && (m.settings.IsDisconnected(m.GetAppDrivePath(stack.Name)) ||
m.settings.IsDecommissioned(m.GetAppDrivePath(stack.Name))) {
continue
}
if err := m.RunTier2(stack.Name); err != nil {
runOne := m.perAppTier2
if runOne == nil {
runOne = m.RunTier2
}
if err := runOne(stack.Name); err != nil {
m.logger.Printf("[WARN] [backup] Tier 2 failed for %s: %v", stack.Name, err)
}
n++
}
m.logger.Printf("[INFO] [backup] Tier 2 run complete: %d HDD app(s) processed", n)
m.logger.Printf("[INFO] [backup] Tier 2 run complete: %d app(s) processed (incl. volume-only — F6)", n)
}
// --- per-app config-panel view (drives the Tier-2 "Beállítás" page) ---