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
+14
View File
@@ -27,6 +27,7 @@ import (
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
"gitea.dooplex.hu/admin/felhom-controller/internal/upwatch"
"golang.org/x/crypto/bcrypt"
)
@@ -804,6 +805,13 @@ func (s *Server) appCommonData(w http.ResponseWriter, r *http.Request, slug stri
if lost, at := lostFilesNote(found.Deployed, found.AppConfig); len(lost) > 0 {
data["LostFiles"], data["LostBootAt"] = lost, at
}
// The upload watch (internal/upwatch): qBittorrent's limit, while the box owns it — one line the household can act on
// in qBittorrent itself. A limit the household set is theirs and is not described here.
if found.Meta.Slug == "qbittorrent" && found.Deployed && s.upwatchView != nil {
if v := s.upwatchView(); v.QBLimitBps > 0 {
data["QBLimitNote"] = s.msgLang(s.langFor(r), "upwatch.qb_limit_note", upwatch.MbpsText(v.QBLimitBps))
}
}
// v0.280.0 (decision 46): the setup gate's card, while the gate stands.
data["SetupGateClosed"] = found.Deployed && found.AppConfig != nil && found.AppConfig.SetupGate.Closed()
data["SetupGateHasProbe"] = found.Meta.SetupDoneProbe != nil && found.Meta.SetupDoneProbe.URL != ""
@@ -2349,6 +2357,12 @@ func (s *Server) settingsBaseData(page, title string) map[string]interface{} {
func (s *Server) systemPageData() map[string]interface{} {
data := s.settingsBaseData("settings", "Beállítások")
data["TitleKey"] = "page.title.settings" // i18n: the Hungarian title above is what hu renders
// The upload watch's measured line (a lower bound, learnt from the nightly off-site push); "" = still learning.
if s.upwatchView != nil {
if v := s.upwatchView(); v.LineKnown {
data["UplinkLine"] = upwatch.MbpsText(int64(v.LineMbps * 1e6 / 8))
}
}
data["GitRepoURL"] = s.cfg.Git.RepoURL
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
data["BackupEnabled"] = s.cfg.Backup.Enabled