controller v0.240.0: seven defects from the any-tier proof and the first nightly rotation
gates / gates (push) Successful in 13s

R-486 (P1): removing an app with its backups KEPT keeps its Tier-2 record,
so the second-drive restore is no longer refused over an intact mirror.
R-484: postgis/pgvector/timescaledb images are Postgres (logical dumps).
R-485: the backup card sizes the recovery unit and the mirror(s).
R-480: a held update's sentence leaves the card once the hold is lifted.
R-477: the update's off-site lookup is one snapshots call, no stats.
R-478: a copy older than this install's deploy does not count.
R-474: "delete backups" deletes the unit, the mirror(s) and the prefs.

Tests and red-proofs per row; evidence in felhom.eu
documentation/audits/v0240-2026-09-13/ and nightly-2026-09-13-adventurelog/.
This commit is contained in:
2026-09-13 19:26:50 +02:00
parent 0e3d831030
commit bdcbd50b42
20 changed files with 673 additions and 82 deletions
+24 -7
View File
@@ -817,11 +817,13 @@ func (r *Router) getStackBackupData(w http.ResponseWriter, _ *http.Request, name
// mount IS the namespace; SSD-only: <systemDataPath>/felhom-data). Passing the namespace root
// (not the raw drive) keeps GetStackBackupData's paths single-nested under Model A.
var nsRoot string
var mirrors []string
if r.backupMgr != nil {
nsRoot = r.backupMgr.AppNamespaceRoot(name)
mirrors = r.backupMgr.Tier2MirrorDirsForApp(name) // R-485: the real second-drive copy, not a dead `rsync` path
}
resp, err := r.stackMgr.GetStackBackupData(name, nsRoot)
resp, err := r.stackMgr.GetStackBackupData(name, nsRoot, mirrors)
if err != nil {
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: err.Error()})
return
@@ -850,12 +852,18 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
// Compute backup paths to remove if requested. Disk-tier (cross-drive rsync)
// backup has moved to the host agent; only the app-data DB-dump path is removed here.
var backupPaths []string
//
// R-474 (v0.240.0): "delete backups" deletes the app's WHOLE recovery unit (definition, db-dumps,
// volume tars) and its Tier-2 mirror, not only db-dumps. The mirror is on another drive, outside
// RemoveStack's per-app base, so the backup manager removes it — and its location is read NOW,
// before the cross-drive record is cleared below. Off-site snapshots are not touched by this path.
var backupPaths, mirrorDirs []string
if body.RemoveBackups && r.backupMgr != nil {
nsRoot := r.backupMgr.AppNamespaceRoot(name)
if nsRoot != "" {
backupPaths = append(backupPaths, backup.AppDBDumpPath(nsRoot, name))
backupPaths = append(backupPaths, backup.RecoveryUnitPath(nsRoot, name), backup.AppDBDumpPath(nsRoot, name))
}
mirrorDirs = r.backupMgr.Tier2MirrorDirsForApp(name)
}
resp, err := r.stackMgr.RemoveStack(name, body.RemoveHDDData, backupPaths)
@@ -882,10 +890,19 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
return
}
// Clean up cross-drive backup config for this stack
if r.sett != nil {
if err := r.sett.SetCrossDriveConfig(name, nil); err != nil {
r.logger.Printf("[WARN] [api] Failed to clean cross-drive config for %s: %v", name, err)
if body.RemoveBackups && r.backupMgr != nil && len(mirrorDirs) > 0 {
resp.BackupPathsRemoved = append(resp.BackupPathsRemoved, r.backupMgr.RemoveTier2Mirrors(name, mirrorDirs)...)
}
// R-486 (v0.240.0): the app's backup preferences — and with them the Tier-2 RECORD that
// tier2RecordedCopyDir needs — are forgotten ONLY when the customer asked for the backups to be
// deleted. Until v0.239.0 the cross-drive record was cleared on every removal, so an app removed
// with its backups kept could not be restored from its intact second-drive mirror (measured on
// demo-hp 2026-09-13: „nincs másodlagos fájlmásolat" over a 236 MB mirror). Pinned by
// TestR486_RemovalKeepsTheTier2RecordUnlessBackupsGo.
if r.sett != nil && body.RemoveBackups {
if err := r.sett.DeleteAppBackupPrefs(name); err != nil {
r.logger.Printf("[WARN] [api] Failed to forget the backup preferences of %s: %v", name, err)
}
}