controller v0.50.0: slice 10 P4 — dual-role drives + backup-aware wipe warning

4A: user-data drives are backup-target-eligible (not role-locked) — surfaced in
the drive purpose note. 4B: handleStorageImpact returns backup_copies (apps whose
cross-drive backups live on the drive, via backupCopiesOnPath); the wipe/eject
modal warns they'd be destroyed (stays customer-confirmable — copies redundant).
Cross-drive backup engine remains out of scope. Test: TestBackupCopiesOnPath.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:00:27 +02:00
parent 2a353572f7
commit 4913130514
4 changed files with 92 additions and 4 deletions
+39 -1
View File
@@ -6,12 +6,15 @@ import (
"errors"
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
)
@@ -307,7 +310,42 @@ func (s *Server) handleStorageImpact(w http.ResponseWriter, r *http.Request) {
if apps == nil {
apps = []string{}
}
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"where": where, "apps": apps})
// P4 (4B): a user-data drive is ALSO backup-target-eligible — it may hold cross-drive backup copies
// of OTHER drives' app data. A wipe destroys those copies too, so name them in the confirmation.
// (The copies are redundant — the originals live on the source drive — so the wipe stays customer-
// confirmable, NOT operator-signature; the warning just makes the loss explicit.)
backupCopies := backupCopiesOnPath(where)
if backupCopies == nil {
backupCopies = []string{}
}
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{
"where": where, "apps": apps, "backup_copies": backupCopies,
})
}
// backupCopiesOnPath lists the apps whose CROSS-DRIVE (secondary) backup copies are stored on the
// drive mounted at `where` (slice 10 P4) — the felhom-data/backups/secondary/<app> dirs. A wipe of
// this drive removes these copies. Best-effort filesystem scan; empty until the cross-drive backup
// ENGINE (a follow-on slice) actually writes here. Shared/aggregate dirs (restic repo, _infra) are
// not apps and are skipped.
func backupCopiesOnPath(where string) []string {
secondary := filepath.Join(where, appbackup.FelhomDataDir, "backups", "secondary")
entries, err := os.ReadDir(secondary)
if err != nil {
return nil // no secondary backups here (or the path isn't readable) — nothing to warn about
}
var apps []string
for _, e := range entries {
if !e.IsDir() {
continue
}
name := e.Name()
if name == "restic" || name == "_infra" { // shared repo / infra, not a per-app copy
continue
}
apps = append(apps, name)
}
return apps
}
// handleStorageWipe is the customer-confirmed wipe of a USER-DATA drive: it unmounts (eject —