channelhealth: controller->agent channel health-check (periodic probe + classified operator alert) v0.90.0

New internal/channelhealth Checker: ~60s probe via the PRODUCTION memoized client
(Server.ProbeAgentChannel, GET /storage), classifies failures (spike Q1 map), debounces transient
reasons (N>=2; construction error latches distinctly), seeds first obs, alerts operator+dashboard on
transition. Notifier.NotifyAgentChannelDown/Recovered (English, operator-only), AlertManager dashboard
banner (Hungarian). No agent/hub change. Spike-proven.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
This commit is contained in:
2026-06-29 20:28:02 +02:00
parent 77bccf1212
commit a277b18981
7 changed files with 531 additions and 6 deletions
@@ -1,6 +1,7 @@
package web
import (
"context"
"encoding/json"
"errors"
"net/http"
@@ -59,6 +60,22 @@ func (s *Server) agentClient() (*agentapi.Client, error) {
return s.agentCli, s.agentCliErr
}
// ProbeAgentChannel runs one controller→agent channel health probe using the PRODUCTION memoized
// client (NOT a fresh one — spike SPIKE-controller-agent-channel-health-2026-06-29: it self-heals,
// reflects exactly what the disk UI sees, and avoids the per-call transport leak the singleton fixed).
// It returns whether the failure was a CONSTRUCTION error (agentClient() couldn't build — a latching
// config fault, distinct from a runtime channel failure) and the error (nil = channel up). The probe
// is GET /storage (cheap, read-only — the same call probeLocalAPI uses at startup). This is the
// channelhealth.Probe seam.
func (s *Server) ProbeAgentChannel(ctx context.Context) (constructionErr bool, err error) {
client, cerr := s.agentClient()
if cerr != nil {
return true, cerr
}
_, serr := client.Storage(ctx)
return false, serr
}
// writeDiskJSON writes the standard {ok,data,error} envelope used by the disk API.
func writeDiskJSON(w http.ResponseWriter, status int, ok bool, errMsg string, data interface{}) {
w.Header().Set("Content-Type", "application/json")