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
+2
View File
@@ -106,6 +106,8 @@ type Manager struct {
// offbox (Part B): the restic-SFTP exec seam (nil → real restic) + the failure→operator-alert hook.
offboxRunner offboxRunner
offboxNotify func(dur time.Duration, snapshots int, err error)
// offboxPushObserver (the upload watch) measures the push; nil = none.
offboxPushObserver func() func()
// offboxStreamRunner + offboxProgress: the MANUAL run's live progress (v0.147.0, 4c). The stream
// seam scans restic's `--json` stdout line-by-line; the state is what the page polls. Both are
// inert on the nightly path — the sink is installed only for the duration of a manual run.
+14
View File
@@ -52,6 +52,11 @@ func defaultOffboxRunner(ctx context.Context, env []string, args ...string) ([]b
// SetOffboxRunner overrides the restic exec (tests). SetOffboxNotify wires the failure→operator alert.
func (m *Manager) SetOffboxRunner(r offboxRunner) { m.offboxRunner = r }
// SetOffboxPushObserver hands the off-site push a measurement: fn is called as the push starts and returns the
// function that ends it (the upload watch's line sampler). Nil = none.
func (m *Manager) SetOffboxPushObserver(fn func() func()) { m.offboxPushObserver = fn }
func (m *Manager) SetOffboxNotify(fn func(dur time.Duration, snapshots int, err error)) {
m.offboxNotify = fn
}
@@ -1127,7 +1132,16 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
} else {
m.logger.Printf("[INFO] [offbox] pre-push dump leg completed in %s — snapshot pair is coherent", time.Since(dumpStart).Round(time.Millisecond))
}
// The upload watch learns the household's upload line from this push (internal/upwatch/line.go): a
// one-second sample of this container's own sent bytes, from here to the end of the push only.
var endPush func()
if m.offboxPushObserver != nil {
endPush = m.offboxPushObserver()
}
runResult, runErr = m.runOffboxInternal(ctx, apps, base, env, t)
if endPush != nil {
endPush()
}
backedUp = runResult.backedUp
missing = runResult.missing
endStamp()