controller v0.67.0: intermediary-mount — HDD_PATH repoint + drive-absent gate + H1 routes

Drives are visible in-guest only at the STABLE /mnt/felhom-drives/<name>; the
registered path + HDD_PATH + FileBrowser source repoint there while agent calls
map back to raw /mnt/<name> (agentWhere). Enroll binds-under-parent before
register. Drive-absent GATE (planDriveGates + 30s driveGateLoop) stops/blocks
apps when a drive vanishes and auto-restarts on return; start-gate refuses start
when the drive is absent. H1 endpoints (disconnect/reconnect/restart-apps) routed
onto host-side ops. Non-hollow tests + companions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:45:13 +02:00
parent 2687843a83
commit 55c896624f
9 changed files with 467 additions and 17 deletions
+33
View File
@@ -414,6 +414,26 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
}
}
// startGatedByMissingDrive reports whether starting `name` must be BLOCKED because the drive its
// HDD_PATH points at is currently disconnected or decommissioned. Returns the storage path for the
// message. SSD-resident apps (no HDD_PATH) are never gated.
func (r *Router) startGatedByMissingDrive(name string) (bool, string) {
cfg := r.stackMgr.LoadAppConfigByName(name)
if cfg == nil {
return false, ""
}
hdd := cfg.Env["HDD_PATH"]
if hdd == "" {
return false, ""
}
for _, sp := range r.sett.GetStoragePaths() {
if sp.Path == hdd && (sp.Disconnected || sp.Decommissioned) {
return true, hdd
}
}
return false, ""
}
func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
r.logger.Printf("[INFO] [api] %s requested for stack: %s", action, name)
r.dbg("actionStack: action=%s name=%s", action, name)
@@ -424,6 +444,19 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
return
}
// Drive-absent gate: refuse to start an app whose data drive is currently disconnected/decommissioned
// (the intermediary-mount gate). Starting it would let it write to the empty fail-closed stable path
// or just crash-loop; block with a clear message until the drive returns (then the gate auto-restarts).
if action == "start" {
if gated, hdd := r.startGatedByMissingDrive(name); gated {
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Error: fmt.Sprintf("A(z) %s tárhely jelenleg nem elérhető — az alkalmazás nem indítható, amíg a meghajtó vissza nem csatlakozik.", hdd),
})
return
}
}
// Memory check before starting a stopped app
if action == "start" {
stackMemMB := r.stackMgr.StackMemoryMB(name)