controller v0.269.0: whole restore from the second drive; crash loops stopped; exact image digests; steps judged by their own .felhom.yml (decisions 26-28, R-661 R-666 R-667 R-668 R-664 R-665 R-662, 09 6.4 part 6)
gates / gates (push) Successful in 27s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-24 12:18:39 +02:00
parent 7c3b3a9694
commit 3c6b49b31c
141 changed files with 3401 additions and 237 deletions
+17
View File
@@ -607,6 +607,14 @@ func (r *Router) actionStack(w http.ResponseWriter, req *http.Request, action, n
// R-439 (slice 4): `update` is in this list. It was not until v0.237.0, so a held app could be
// updated — the one action most likely to make a held app's data worse. Pinned by
// TestR439_UpdateOfAHeldAppIsRefused.
// v0.269.0 (`09` §3 decision 28): an app the BOX stopped for a crash loop / OOM storm is started
// again by the household's Start — the hold is lifted and the app gets one more try. Restart and
// Update stay refused while it holds; any other hold kind is untouched.
if action == "start" && r.backupMgr != nil && r.backupMgr.HoldKind(name) == settings.HoldReasonUnhealthyStop {
if r.backupMgr.LiftUnhealthyStop(name) {
r.logger.Printf("[INFO] [api] %s: the unhealthy stop is LIFTED by Start — one more try (decision 28)", name)
}
}
if action == "start" || action == "restart" || action == "update" {
if held, why := r.restoreHoldFor(name); held {
// `reason` (v0.261.0) — THIS LINE FIRES BEFORE UpdatePreflight, so without it a held app
@@ -862,6 +870,7 @@ func (r *Router) getStackHDDData(w http.ResponseWriter, req *http.Request, name
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: r.errText(req, err)})
return
}
resp.KeepDataOnly = r.stackMgr.RemoveKeepsDataOnly(name) // decision 27: the dialog offers keep-data only
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: resp})
}
@@ -908,6 +917,14 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
}
r.dbg("removeStack: name=%s removeHDDData=%v removeBackups=%v", name, body.RemoveHDDData, body.RemoveBackups)
// v0.269.0 (`09` §3 decision 27, R-666): while a held app's page says support is informed, only
// "remove the app, keep my data" is allowed. Refused BEFORE anything is removed (live-probes rule).
if (body.RemoveHDDData || body.RemoveBackups) && r.stackMgr.RemoveKeepsDataOnly(name) {
r.logger.Printf("[WARN] [api] Remove of %s with data REFUSED: the app is held with no whole copy (support is informed) — only keep-data is allowed (decision 27)", name)
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.errText(req, stacks.ErrRemoveKeepDataWhileSupport)})
return
}
// 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.
//