controller v0.242.0: a removed app is listed with its kept backup; five small ones (R-487 R-491 R-490 R-489 R-476 R-456)
gates / gates (push) Successful in 14s

R-487: the local backup lists are keyed on the drives, not on what is
deployed — a removed app whose unit was kept is listed with the restore
that reinstalls it, the picker answers for it, and the restore opens the
unit where it sits. R-491: a removal clears the app's update hold.
R-490: /api/system/info reaches the API router and reads the default
storage path. R-489: volumes_removed is the real before/after difference,
[] when none. R-476: a Tier-2 copy is dated by its data, not its manifest.
R-456: the boot-orphan rule is pinned. Every fix red-proofed.
This commit is contained in:
2026-09-13 22:50:18 +02:00
parent c1f62ddae8
commit d698ce343b
22 changed files with 838 additions and 13 deletions
+24 -2
View File
@@ -905,6 +905,17 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
r.logger.Printf("[WARN] [api] Failed to forget the backup preferences of %s: %v", name, err)
}
}
// R-491 (v0.242.0): an app held after a failed update that the customer then REMOVES is not held
// any more — the hold named a copy for a deploy that no longer exists, and every start gate reads
// the store, so a reinstall under the same name would begin refused. Measured on demo-hp
// 2026-09-13 (nextcloud). An R-379 restore hold is deliberately NOT cleared here.
if r.sett != nil {
if cleared, err := r.sett.ClearUpdateHold(name); err != nil {
r.logger.Printf("[WARN] [api] Failed to clear the update hold of %s: %v", name, err)
} else if cleared {
r.logger.Printf("[INFO] [api] remove %s: its update hold is cleared with it (R-491)", name)
}
}
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: resp, Message: "Stack " + name + " removed"})
@@ -978,7 +989,15 @@ func (r *Router) triggerSync(w http.ResponseWriter, _ *http.Request) {
}
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
info := system.GetInfo(r.cfg.Paths.HDDPath, r.cpuCollector)
// R-490 / R-465: the same default-storage-path fallback every other reader of the always-empty
// cfg.Paths.HDDPath has; without it hdd_configured was false on every box.
hddPath := r.cfg.Paths.HDDPath
if r.sett != nil {
if p := r.sett.GetDefaultStoragePath(); p != "" {
hddPath = p
}
}
info := system.GetInfo(hddPath, r.cpuCollector)
// F1: GetInfo now reports the guest RAM cap (from the Docker daemon) as TotalMemMB, but the guest-wide
// "used" is not observable from the container. Report the controller's accurate committed-app memory
// (sum of running apps' mem requests) as used — a meaningful "allocated of cap" figure for the UI.
@@ -993,7 +1012,10 @@ func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
info.MemPercent = float64(used) / float64(info.TotalMemMB) * 100
}
}
syncStatus := r.syncer.Status()
var syncStatus interface{}
if r.syncer != nil { // nil in tests; production always wires the syncer
syncStatus = r.syncer.Status()
}
data := map[string]interface{}{
"system": info,
"sync_status": syncStatus,