v0.228.0 — the off-site check reads the data; the debug page stops lying (R-399 + R-400)
gates / gates (push) Successful in 12s

R-399: monitoring.integrity.read_data_subset defaults to 100%. A pack damaged
without changing its size made plain `restic check` report "no errors were found"
on demo-hp 2026-08-30; every read-data form caught it. Cost on that 134 MB store:
35.0s structure vs 39.2s at 100%. "off" (any case) is the off token; empty means
not-configured, therefore the default; a malformed value falls back to the DEFAULT,
never to structure. A completed check over 5 minutes logs a WARN naming the
duration, the depth and R-401 — operator log only, no hub event, no depth change.
The depth is now recorded with the verdict (LastIntegrityDepth; empty = NOT
RECORDED, never "structure").

R-400: 24 debug-page references, 17 dispatched, 7 dead — three of which fetched on
page LOAD, so those panels were permanently blank. backup/crossdrive implemented;
backup/infra, hub/infra-push, dr/infra-status, storage/watchdog-status and both
storage/simulate-* deleted with their panels and JavaScript.
scripts/debug_route_gate.py fails in both directions and is registered after the
seven were resolved. 18 referenced, 18 dispatched, none orphaned.

Corrections: the dead-field warning in report/types.go said the controller runs no
integrity check and the notifiers are called from nowhere — both false since
v0.227.0. controller.yaml.example gains its missing integrity: block.
integrityCheckTimeout's "ships OFF" comment rewritten.
This commit is contained in:
2026-08-31 10:24:29 +02:00
parent 300d7e87d7
commit 3c49dc8ea4
23 changed files with 1144 additions and 175 deletions
+68
View File
@@ -65,6 +65,11 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
// Section 3: Backup testing (app-data only; disk-tier moved to host agent)
case subpath == "backup/dbdump" && r.Method == http.MethodPost:
s.debugTriggerDBDump(w, r)
// R-400 — the „Csak cross-drive" button has posted here since it was added and nothing answered.
// The capability was never missing: Manager.RunTier2 is live and the app config page already calls
// it. Only the debug route was absent, so this is IMPLEMENTED rather than deleted.
case subpath == "backup/crossdrive" && r.Method == http.MethodPost:
s.debugRunCrossDrive(w, r)
// R-397 — the button at debug.html:83 has posted here since it was added and NOTHING answered.
// Verified 2026-08-30: this dispatch had no such case, so pressing „Restic integritás" did
// nothing at all. Seventh instance of built-but-never-wired in this project; filed as R-400 in its
@@ -404,6 +409,66 @@ func (s *Server) debugTriggerDBDump(w http.ResponseWriter, r *http.Request) {
writeDebugJSON(w, http.StatusOK, true, "DB dump elindítva", nil)
}
// debugRunCrossDrive runs the Tier-2 (cross-drive) copy for every deployed HDD app (R-400).
//
// ASYNCHRONOUS, following debugTriggerDBDump above rather than the integrity button below: a Tier-2
// sweep across every app copies real data and is bounded by disk speed, not by a timeout, so holding
// the operator's request open for it would time the browser out and teach nothing. The integrity
// button is synchronous because the operator pressed it to learn an ANSWER; this one is pressed to
// make something happen, and the log is where its outcome belongs.
//
// It reports WHICH apps it started for, not just „elindítva". A sweep that quietly matched zero apps
// and a sweep that matched eight are different facts, and a message that cannot tell them apart is
// how a button reads as working while doing nothing — the class this whole row exists to close.
func (s *Server) debugRunCrossDrive(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil {
writeDebugJSON(w, http.StatusBadRequest, false, "Backup manager nincs konfigurálva", nil)
return
}
names := crossDriveTargets(s.stackMgr.GetStacks(), func(name string) bool {
return s.backupMgr.Tier2Info(name).IsHDDApp
})
if len(names) == 0 {
writeDebugJSON(w, http.StatusOK, true, "Nincs olyan telepített alkalmazás, amelynek 2. mentése futtatható lenne",
map[string]interface{}{"apps": names, "count": 0})
return
}
go func(apps []string) {
for _, name := range apps {
if err := s.backupMgr.RunTier2(name); err != nil {
s.logger.Printf("[WARN] [web] debug cross-drive run for %s failed: %v", name, err)
continue
}
s.logger.Printf("[INFO] [web] debug cross-drive run for %s completed", name)
}
}(names)
writeDebugJSON(w, http.StatusOK, true,
fmt.Sprintf("Cross-drive mentés elindítva %d alkalmazásra", len(names)),
map[string]interface{}{"apps": names, "count": len(names)})
}
// crossDriveTargets picks the apps a Tier-2 sweep should run for.
//
// A NAMED FUNCTION rather than an inline loop so both of its answers are assertable. The empty answer
// and the non-empty answer are the two outcomes a dead button and a working one are told apart by, and
// building a deployed HDD-backed stack inside a web test is not practical — so the selection is proven
// here and the dispatch is proven at the route.
func crossDriveTargets(all []stacks.Stack, isHDDApp func(name string) bool) []string {
names := make([]string, 0)
for _, st := range all {
if !st.Deployed {
continue
}
// Tier 2 is an off-DRIVE copy: an app with no HDD path has no second drive to copy to, and
// RunTier2 would return "no source drive" for every one of them.
if !isHDDApp(st.Name) {
continue
}
names = append(names, st.Name)
}
return names
}
// debugRunIntegrityCheck runs the off-site integrity check by hand (R-359/R-397).
//
// SYNCHRONOUS on purpose, unlike the DB-dump button beside it: the operator pressed this to learn an
@@ -427,6 +492,9 @@ func (s *Server) debugRunIntegrityCheck(w http.ResponseWriter, r *http.Request)
"unreachable": res.Unreachable,
"duration_ms": res.Duration.Milliseconds(),
"read_data_subset": res.ReadDataSubset,
// R-399: the depth as it is RECORDED, so the JSON says "structure" rather than an empty string
// a reader has to interpret. read_data_subset above stays raw for anyone parsing the argv.
"depth": backup.IntegrityDepthCode(res.ReadDataSubset),
}
switch {
case res.Skipped: