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
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user