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
+28 -2
View File
@@ -33,6 +33,10 @@ type AlertManager struct {
alerts []Alert
logger *log.Logger
hubPushStatusFn func() HubPushStatusData
// agentChannelAlert is set/cleared by the channel-health checker (out-of-band from Refresh, which
// is health-report-driven). nil = channel up. It is prepended in GetAlerts so a dead controller→
// agent link (disk/storage UI broken) is always visible regardless of the health-report cycle.
agentChannelAlert *Alert
}
// NewAlertManager creates a new AlertManager.
@@ -49,6 +53,25 @@ func (am *AlertManager) SetHubPushStatus(fn func() HubPushStatusData) {
am.mu.Unlock()
}
// SetAgentChannelAlert sets (down=true) or clears (down=false) the controller→agent channel-down
// dashboard banner. Called by the channel-health checker each probe (idempotent). msg is the short
// Hungarian display line.
func (am *AlertManager) SetAgentChannelAlert(down bool, msg string) {
am.mu.Lock()
defer am.mu.Unlock()
if !down {
am.agentChannelAlert = nil
return
}
am.agentChannelAlert = &Alert{
ID: "agent-channel-down",
Level: "error",
Message: msg,
Link: "/settings",
LinkText: "Beállítások",
}
}
// Refresh regenerates alerts from the latest health check report and config state.
// Called after each health check cycle (every 5 minutes) and on storage state changes.
func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config, backupMgr *backup.Manager, updateAvailable bool, latestVersion string, storagePaths ...[]settings.StoragePath) {
@@ -159,7 +182,7 @@ func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert {
am.mu.RLock()
defer am.mu.RUnlock()
if len(am.alerts) == 0 {
if len(am.alerts) == 0 && am.agentChannelAlert == nil {
return nil
}
@@ -169,6 +192,10 @@ func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert {
}
var result []Alert
// Channel-down is prepended (highest priority — the agent link being dead breaks disk/storage UI).
if am.agentChannelAlert != nil && !exclude[am.agentChannelAlert.ID] {
result = append(result, *am.agentChannelAlert)
}
for _, a := range am.alerts {
if exclude[a.ID] {
continue
@@ -239,4 +266,3 @@ func countLevel(alerts []Alert, level string) int {
}
return n
}