docs: Q1c GREEN — reboot survival automatic since agent 0.84.0 (feature doc + audit §7 + CONTEXT + REPORT)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -2,9 +2,11 @@ package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
@@ -323,9 +325,90 @@ func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]in
|
||||
// v0.46.0 Diagnostics: pending log pulls + received/blocked bundles (72 h TTL).
|
||||
"LogBundles": s.hostLogBundleRows(host),
|
||||
"CSRFToken": s.getCSRFToken(r),
|
||||
// v0.47.0 stale host removal: the danger-zone card renders ONLY for non-online
|
||||
// hosts — an ONLINE host is never deletable (no override exists).
|
||||
"Deletable": status != "ok",
|
||||
}
|
||||
}
|
||||
|
||||
// handleHostDeleteImpact — GET /hosts/{id}/delete-impact (v0.47.0 stale host removal).
|
||||
// The confirm dialog's impact probe: counts/booleans ONLY (never a secret, blob, or key),
|
||||
// mirroring the global-floor impact endpoint's read-only-JSON pattern.
|
||||
func (s *Server) handleHostDeleteImpact(w http.ResponseWriter, r *http.Request, hostID string) {
|
||||
host, err := s.store.GetHost(hostID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] host delete-impact %s: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if host == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
a, err := s.store.CountHostArtifacts(hostID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] host delete-impact %s: artifacts: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
status := s.hostStatus(host.LastReportAt)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": status,
|
||||
"deletable": status != "ok",
|
||||
"guests": a.Guests,
|
||||
"reports": a.Reports,
|
||||
"log_bundles": a.LogBundles,
|
||||
"escrow_present": a.EscrowPresent,
|
||||
"wg_peer_bound": a.WGPeerBound,
|
||||
"pbs_secret_present": a.PBSSecretPresent,
|
||||
"recovery_present": a.RecoveryPresent,
|
||||
})
|
||||
}
|
||||
|
||||
// handleHostDelete — POST /hosts/{id}/delete (v0.47.0 stale host removal). Gates, in order:
|
||||
// - unknown host → 404
|
||||
// - ONLINE host → 409 unconditionally (host reports authenticate via GetHostByAPIKey;
|
||||
// deleting a live host permanently bricks its heartbeat channel — enroll is
|
||||
// passphrase-gated mint-once, so there is deliberately NO override)
|
||||
// - confirm_host_id mismatch → 400 (type-to-confirm)
|
||||
// - escrow present without delete_escrow=1 → 409 (store-enforced, fail-safe-to-refuse)
|
||||
func (s *Server) handleHostDelete(w http.ResponseWriter, r *http.Request, hostID string) {
|
||||
host, err := s.store.GetHost(hostID)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] host delete %s: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if host == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
if confirm := strings.TrimSpace(r.FormValue("confirm_host_id")); confirm != hostID {
|
||||
s.logger.Printf("[WARN] host delete refused: %s confirm mismatch", hostID)
|
||||
http.Error(w, "Confirmation does not match the host id — nothing deleted.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
deleteEscrow := true // RED-PROOF 2: escrow-ack check dropped
|
||||
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)
|
||||
http.Error(w, "This host has a key escrow (+ DR bundle). Tick the escrow acknowledgement to delete it too — nothing deleted.", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[ERROR] host delete %s: %v", hostID, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] host deleted: %s (escrow deleted: %v)", hostID, deleteEscrow)
|
||||
http.Redirect(w, r, "/hosts", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user