controller: fix-3 dead-app alerting + fix-6 ring cap/spill/spam (WIP, pre-build)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
2026-07-12 10:07:47 +02:00
parent a6da64da15
commit d8f6069b46
10 changed files with 581 additions and 10 deletions
+15 -4
View File
@@ -37,6 +37,16 @@ const (
StateOrphaned ContainerState = "orphaned"
)
// IsDownState reports whether a container state means a DEPLOYED app is not running and won't recover
// on its own (fix-3, CAMPAIGN-3). Only `stopped` and `exited` qualify — a Docker "created"/"dead"
// container (a failed-at-boot app, the F11 case) resolves to `stopped`. Deliberately NOT `starting`
// / `unhealthy` (running, with their own health handling), `restarting` (self-recovering),
// `deploying` (mid-deploy), `paused` (a deliberate user action), or `unknown` (ambiguous — fail-open,
// never manufacture a dead-app alert from an inconclusive read).
func IsDownState(s ContainerState) bool {
return s == StateStopped || s == StateExited
}
// ContainerInfo holds status info about a single container within a stack.
type ContainerInfo struct {
Name string `json:"name"`
@@ -444,9 +454,10 @@ func (m *Manager) refreshStatusLocked() error {
totalContainers++
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
}
// fix-6: refreshStatusLocked runs every 10s (the status-refresh job) — its per-cycle enumeration
// lines are TRACE (dropped from the debug ring) so they don't eat the post-incident window. A real
// state change is logged elsewhere at INFO; a docker error returns up the stack.
m.logger.Printf("[TRACE] [stacks] refreshStatusLocked: docker ps returned %d containers across %d projects", totalContainers, len(projectContainers))
m.logger.Printf("[INFO] [stacks] Status refresh: %d containers across %d stacks", totalContainers, len(m.stacks))
@@ -473,7 +484,7 @@ func (m *Manager) refreshStatusLocked() error {
}
if m.isDebug() {
m.logger.Printf("[DEBUG] [stacks] refreshStatusLocked: stack %q → state=%s containers=%d", name, stack.State, len(stack.Containers))
m.logger.Printf("[TRACE] [stacks] refreshStatusLocked: stack %q → state=%s containers=%d", name, stack.State, len(stack.Containers))
}
stack.LastUpdated = time.Now()