v0.231.0: the off-site proof gets a by-hand trigger, like its integrity sibling (R-87)
gates / gates (push) Successful in 12s
gates / gates (push) Successful in 12s
Without it the only way to see the job work is to wait for 05:30, which makes live validation and any future diagnosis a next-day exercise. Same function as the scheduled job - no second code path. ONE deliberate difference from the integrity button: due-ness is NOT bypassed. There, forcing means "check the store again", which is always answerable. Here due-ness IS the target selection - an app is due when its newest snapshot has not been proved - so ignoring it would mean inventing a second way to choose an app, exactly what having one function prevents. When nothing is due the button says so, honestly. Every other guard intact, including the single-writer flag: a hand-run during a backup SKIPS exactly as the scheduled one would. POST /api/debug/backup/offsite-proof, button beside "Restic integritas" on the debug page. debug_route_gate pairs the two, so a button with no dispatch (R-400's shape) cannot ship.
This commit is contained in:
@@ -1634,6 +1634,12 @@ func main() {
|
||||
defer cancel()
|
||||
return runOffsiteIntegrityCheck(ctx, backupMgr, notifier, logger, force)
|
||||
}
|
||||
// R-87: the proof button's caller. Same function as the scheduled job — no second path.
|
||||
dc.RunOffsiteProof = func() backup.ProofResult {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
return runOffsiteProof(ctx, backupMgr, notifier, logger)
|
||||
}
|
||||
dc.HubConnectivityTest = func() (int, int64, error) {
|
||||
start := time.Now()
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appexport"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/report"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
@@ -39,6 +39,11 @@ type DebugCallbacks struct {
|
||||
// `force` is the ONLY difference between the two callers. It skips the due-ness question and
|
||||
// nothing else — every guard, above all the single-writer flag, applies identically.
|
||||
RunIntegrityCheck func(force bool) backup.IntegrityResult
|
||||
// RunOffsiteProof (R-87) runs the nightly off-site content proof SYNCHRONOUSLY and returns what it
|
||||
// found. Same function as the scheduled job — there is no second code path, for the reason
|
||||
// runOffsiteProof's own comment gives: a hand-run that could drift from the scheduled one is how
|
||||
// the button ends up proving something the nightly job does not.
|
||||
RunOffsiteProof func() backup.ProofResult
|
||||
}
|
||||
|
||||
// debugPageHandler renders the debug dashboard page.
|
||||
@@ -76,6 +81,11 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// own right rather than disappearing inside this change.
|
||||
case subpath == "backup/integrity" && r.Method == http.MethodPost:
|
||||
s.debugRunIntegrityCheck(w, r)
|
||||
// R-87 — the off-site content proof, by hand. It exists for the same reason the integrity button
|
||||
// does: without it the only way to see this job work is to wait for 05:30, which makes both live
|
||||
// validation and any future diagnosis a next-day exercise.
|
||||
case subpath == "backup/offsite-proof" && r.Method == http.MethodPost:
|
||||
s.debugRunOffsiteProof(w, r)
|
||||
|
||||
// Section 5: Hub & connectivity
|
||||
case subpath == "hub/push" && r.Method == http.MethodPost:
|
||||
@@ -469,6 +479,55 @@ func crossDriveTargets(all []stacks.Stack, isHDDApp func(name string) bool) []st
|
||||
return names
|
||||
}
|
||||
|
||||
// debugRunOffsiteProof runs the nightly off-site content proof by hand (R-87).
|
||||
//
|
||||
// SYNCHRONOUS, like the integrity button beside it and for the same reason: the operator pressed this
|
||||
// to learn an ANSWER. One app's unit measured 2.3-4.0 s on demo-hp, so there is nothing to wait for.
|
||||
//
|
||||
// DUE-NESS IS NOT BYPASSED, and that is the ONE place this differs from the integrity button. There,
|
||||
// forcing means "check the store again", which is always answerable. Here, due-ness IS the target
|
||||
// selection — an app is due when its newest snapshot has not been proved — so ignoring it would mean
|
||||
// inventing a different way to choose an app, i.e. a second code path, which is exactly what having
|
||||
// one function is meant to prevent. When nothing is due the button says so, honestly.
|
||||
//
|
||||
// Every other guard is intact, including the single-writer flag: a hand-run during a backup SKIPS
|
||||
// exactly as the scheduled one would, because "the operator asked for it" is precisely the reasoning
|
||||
// that would reintroduce the hazard.
|
||||
func (s *Server) debugRunOffsiteProof(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.RunOffsiteProof == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
res := s.debugCallbacks.RunOffsiteProof()
|
||||
data := map[string]interface{}{
|
||||
"stack": res.Stack,
|
||||
"snapshot": res.SnapshotID,
|
||||
"verdict": res.Verdict(),
|
||||
"reason": string(res.Judgement.Reason),
|
||||
"missing": res.Judgement.Missing,
|
||||
"duration_ms": res.Duration.Milliseconds(),
|
||||
"skipped": res.Skipped,
|
||||
"skip_reason": res.SkipReason,
|
||||
"no_snapshot": res.NoSnapshot,
|
||||
}
|
||||
switch {
|
||||
case res.Skipped:
|
||||
writeDebugJSON(w, http.StatusOK, true, "Kihagyva: "+res.SkipReason, data)
|
||||
case res.NoSnapshot:
|
||||
writeDebugJSON(w, http.StatusOK, true, "Nincs esedékes alkalmazás — minden legújabb mentés már ellenőrizve", data)
|
||||
case res.Err != nil:
|
||||
// NOT a verdict about the backup: the restore did not finish, so nothing was concluded.
|
||||
data["error"] = res.Err.Error()
|
||||
writeDebugJSON(w, http.StatusOK, true, "A visszaállítás nem futott le — a mentésről semmi nem derült ki", data)
|
||||
case res.Judgement.Verdict == backup.UnitProofPass:
|
||||
writeDebugJSON(w, http.StatusOK, true, "A mentés tartalmazza az alkalmazás adatait", data)
|
||||
case res.Judgement.Verdict == backup.UnitProofCannotJudge:
|
||||
writeDebugJSON(w, http.StatusOK, true, "Nem megítélhető — a mentés nem hordoz elég információt", data)
|
||||
default:
|
||||
writeDebugJSON(w, http.StatusOK, false, "A mentés olvasható, de nem tartalmazza az alkalmazás adatait", data)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-integrity" data-label="Restic integritás" onclick="triggerAction('btn-integrity','/api/debug/backup/integrity','POST')">Restic integritás</button>
|
||||
<span class="debug-result" id="btn-integrity-result"></span>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-offsite-proof" data-label="Távoli mentés tartalma" onclick="triggerAction('btn-offsite-proof','/api/debug/backup/offsite-proof','POST')">Távoli mentés tartalma</button>
|
||||
<span class="debug-result" id="btn-offsite-proof-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user