v0.173.0 — R-77: endpoint-drift detection, samba protected-set gate, channel log honesty

Source: felhom.eu/documentation/audits/DIAG-agent-channel-2026-07-26.md

bootstrap.DetectEndpointDrift names a controller.yaml vs bootstrap.json
local_api.endpoint divergence -- one ERROR carrying BOTH values and BOTH paths,
its own event type local_api_endpoint_drift, and its own Hungarian banner shown
ABOVE the channel banner because drift is the cause and "agent unreachable" the
symptom. It writes NOTHING: reconciling from bootstrap.json would clobber a
correct controller.yaml on any half-provisioned or hand-repaired guest, so the
authority ruling is deferred to R-78. Fail-safe silent on absent/unparseable/
incomplete bootstrap and on an empty endpoint (ensureLocalAPI's fill-if-missing
path is untouched). Fingerprint compared as a BOOLEAN only; token never
compared, logged or exposed.

EffectiveProtected now gates samba on Enabled && UserSet, mirroring BOTH of
reconcileSambaAt's early returns, and the doc comment is corrected in the same
change -- it claimed "detection and deployment agree in both directions" while
citing only !smb.Enabled, an assertion that went false when !smb.UserSet was
added. Not over-suppressed: sharing on WITH a password and a dead container
still alarms.

Channel log: the debounce placeholder is stateUnconfirmed (rendered "unseeded")
instead of "up", so a born-down channel no longer logs "up->down" and orUnseeded
stops being dead code. Logging only -- the placeholder is still matched in the
re-arm condition, so F2 born-down alerting is byte-for-byte unchanged and all
nine pre-existing channelhealth tests pass.

Tests 951 -> 959, all green. Red-proofs A (both directions), E and F.
MinAgent unchanged; felhom-agent untouched.
This commit is contained in:
2026-07-26 09:13:52 +02:00
parent c7a3a90782
commit 9056f01fae
11 changed files with 648 additions and 19 deletions
+33 -1
View File
@@ -42,6 +42,10 @@ type AlertManager struct {
// out-of-band, state-based, self-clearing model as agentChannelAlert: passing an empty slice when
// every deployed app is running clears the banner with no manual dismissal.
deadAppAlerts []Alert
// endpointDriftAlert (R-77) is set/cleared at startup by the local_api drift check. Separate from
// agentChannelAlert on purpose: drift is usually the CAUSE and "agent unreachable" the SYMPTOM,
// and during the 2026-07-25 outage only the symptom was visible.
endpointDriftAlert *Alert
}
// NewAlertManager creates a new AlertManager.
@@ -77,6 +81,28 @@ func (am *AlertManager) SetAgentChannelAlert(down bool, msg string) {
}
}
// SetEndpointDriftAlert sets (drift=true) or clears the local_api endpoint-drift banner (R-77).
//
// It is deliberately a SEPARATE alert from SetAgentChannelAlert: during the 2026-07-25 outage the
// generic "agent unreachable" banner was the ONLY signal, and it looked like a dead agent. The two
// can also be true at once — a drifted endpoint usually CAUSES the channel to be down — so folding
// them together would hide the actionable one behind the symptom.
func (am *AlertManager) SetEndpointDriftAlert(drift bool, msg string) {
am.mu.Lock()
defer am.mu.Unlock()
if !drift {
am.endpointDriftAlert = nil
return
}
am.endpointDriftAlert = &Alert{
ID: "local-api-endpoint-drift",
Level: "error",
Message: msg,
Link: "/settings",
LinkText: "Beállítások",
}
}
// DeadApp is a deployed app the health loop found not-running (fix-3). State is the container-state
// string for the display (e.g. "stopped"/"exited").
type DeadApp struct {
@@ -244,7 +270,7 @@ func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert {
am.mu.RLock()
defer am.mu.RUnlock()
if len(am.alerts) == 0 && am.agentChannelAlert == nil && len(am.deadAppAlerts) == 0 {
if len(am.alerts) == 0 && am.agentChannelAlert == nil && am.endpointDriftAlert == nil && len(am.deadAppAlerts) == 0 {
return nil
}
@@ -254,6 +280,12 @@ func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert {
}
var result []Alert
// Endpoint drift first: it is the actionable CAUSE, and the channel-down banner below is usually
// just its symptom. Showing the symptom above the cause is what made the 2026-07-25 outage read
// as an infrastructure blip for 17.5 h.
if am.endpointDriftAlert != nil && !exclude[am.endpointDriftAlert.ID] {
result = append(result, *am.endpointDriftAlert)
}
// 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)