v0.342.0 (unreleased): the upload and CPU watch — an app that sends or burns far more than its own numbers explain is one operator-only alarm; the line learnt from the off-site push; qBittorrent's default upload limit (never over the household's)
gates / gates (push) Successful in 1m17s

internal/upwatch: exact per-container counters from the Docker Engine API, per-app judging (upload >= max(5 Mbit/s,
25% of the line) for 15 min; CPU >= max(150%, 50% x cores) for 30 min), explainers qBittorrent / Transmission / Plex,
app_unexplained_upload / app_unexplained_cpu + _cleared (hub 0.169.0). The off-site push is sampled once a second
for the line (a lower bound). qBittorrent limit = 50% of the line, the household's limit always wins. 20 red-proofs.
Spike + design: felhom.eu audits/SPIKE-upload-watch-2026-10-23.md, architecture/08 section 9. Not released.
This commit is contained in:
2026-10-23 21:26:09 +02:00
parent 277e99839d
commit a9c1997fbc
35 changed files with 2438 additions and 0 deletions
+28
View File
@@ -14,6 +14,7 @@ import (
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/upwatch"
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
)
@@ -882,6 +883,33 @@ func (n *Notifier) NotifyAppRepairedAfterCrash(d AppRepairDetails) {
}
}
// NotifyUnexplained sends the upload and CPU watch's events (internal/upwatch; felhom.eu `08` §9). OPERATOR-ONLY on the
// hub (notify.operatorOnlyEvents there, registered in the same commit as the types): the household can do nothing
// about a hacked app but wait for support (the R-388 rule). Edge-triggered by the watch: one event when the condition
// is judged, one `_cleared` (info, recorded) when it stops.
func (n *Notifier) NotifyUnexplained(eventType, _ string, d upwatch.Details) {
// The severity is fixed per type (alarm = warning, clear = info) — never the caller's word, so the hub's vocabulary
// contract (TestR329_EveryEmittedSeverityIsInTheHubVocabulary) can read every value from this function.
switch eventType {
case "app_unexplained_upload":
if d.AppMbps >= 0 {
n.pushEventMsg(eventType, "warning", "event.app_unexplained_upload", d, d.App, d.RateMbps, d.Minutes, d.AppMbps)
} else {
n.pushEventMsg(eventType, "warning", "event.app_unexplained_upload_noapp", d, d.App, d.RateMbps, d.Minutes)
}
case "app_unexplained_upload_cleared":
n.pushEventMsg(eventType, "info", "event.app_unexplained_upload_cleared", d, d.App)
case "app_unexplained_cpu":
n.pushEventMsg(eventType, "warning", "event.app_unexplained_cpu", d, d.App, d.CPUPercent, d.Minutes)
case "app_unexplained_cpu_cleared":
n.pushEventMsg(eventType, "info", "event.app_unexplained_cpu_cleared", d, d.App)
default:
if n.logger != nil {
n.logger.Printf("[ERROR] NotifyUnexplained: unknown event type %q — not sent", eventType)
}
}
}
type OOMStormDetails struct {
App string `json:"app"`
StackName string `json:"stack_name"`