hub v0.137.0 source + burn-down round 2 in felhom.eu: R-277 R-581 R-600 R-544 R-855 R-134 R-92 R-292 R-599 R-725 R-728 (hub), R-819 R-857 R-555 R-364 R-587 (gates/tools), R-571 R-129 R-124-runbook (docs); 28 rows closed incl. catalog + agent v0.147.0 rows, R-350 merged into R-132, R-888 opened, R-887 mechanism (249 -> 222)
gates / gates (push) Successful in 2m3s

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-10-05 19:43:37 +02:00
parent e0d884565f
commit 557629d2bf
65 changed files with 2680 additions and 122 deletions
+37 -2
View File
@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strings"
@@ -895,7 +896,7 @@ func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID
}
if status := s.hostStatus(host.LastReportAt); status == "ok" {
s.logger.Printf("[WARN] host delete refused: %s is online", hostID)
http.Error(w, "Host is ONLINE — deletion is refused (a live agent would receive 401s permanently).", http.StatusConflict)
http.Error(w, "Host is ONLINE — deletion is refused (a live agent would receive 401s permanently)."+s.deletionOpensAt(host.LastReportAt, time.Now()), http.StatusConflict)
return
}
if confirm := strings.TrimSpace(r.FormValue("confirm_host_id")); confirm != hostID {
@@ -904,6 +905,8 @@ func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID
return
}
deleteEscrow := r.FormValue("delete_escrow") == "1"
// R-544: read whether an escrow exists BEFORE the delete, so the log can say what happened to it.
escrowBefore, escErr := s.store.GetHostEscrow(hostID)
if err := s.store.DeleteHost(hostID, deleteEscrow); err != nil {
if errors.Is(err, store.ErrHostEscrowPresent) {
s.logger.Printf("[WARN] host delete refused: %s has key escrow (acknowledgement missing)", hostID)
@@ -914,7 +917,7 @@ func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
s.logger.Printf("[INFO] host deleted: %s (escrow deleted: %v)", hostID, deleteEscrow)
s.logger.Printf("[INFO] host deleted: %s (%s)", hostID, hostDeleteEscrowEffect(escrowBefore != nil, escErr))
// R-509: the customer record stays and now waits for a box → send the connect link.
if host.CustomerID != "" {
s.autoMintSelfBindIfWaiting(host.CustomerID, "host delete")
@@ -922,6 +925,38 @@ func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID
http.Redirect(w, r, "/hosts", http.StatusSeeOther)
}
// hostDeleteEscrowEffect states what a host delete did to the key escrow, in operator words (R-544).
// A host delete NEVER destroys escrow: the store demotes it to retained custody, and only the customer
// delete purges it. The old line printed the form flag ("escrow deleted: true"), which read as a
// household's last key being destroyed.
func hostDeleteEscrowEffect(hadEscrow bool, lookupErr error) string {
switch {
case lookupErr != nil:
return "escrow state unknown before the delete; any escrow is demoted to retained custody, never destroyed"
case hadEscrow:
return "escrow custody demoted to retained (host delete); the customer delete is the only purge"
default:
return "no key escrow held"
}
}
// deletionOpensAt is the tail of an ONLINE refusal (R-599): "online" is a report-staleness window, not a
// liveness probe, so a box that no longer exists still reads online until its last report is older than
// the CONFIGURED stale threshold. Say when that was and when the refusal ends, so a teardown waits
// instead of concluding the delete is broken. Empty when there is no last report.
func (s *Server) deletionOpensAt(lastReport *time.Time, now time.Time) string {
if lastReport == nil {
return ""
}
age := now.Sub(*lastReport)
if age < 0 {
age = 0
}
opens := lastReport.Add(s.staleThreshold).UTC()
return fmt.Sprintf(" Its last report arrived %d min ago (%s UTC); deletion opens at %s UTC, once the host has been silent for the stale threshold (%s).",
int(age.Minutes()), lastReport.UTC().Format("15:04"), opens.Format("15:04"), s.staleThreshold)
}
// handleHostDetail renders the read-only per-host detail page (audit F-M1). GET only.
func (s *Server) handleHostDetail(w http.ResponseWriter, r *http.Request, hostID string) {
host, err := s.store.GetHost(hostID)