v0.203.0: the box collects what the hub staged for it (R-218 consume half) + R-220's message
gates / gates (push) Successful in 10s

R-218's declaration half shipped in v0.201.0 and works. Its consume half never
existed. Reconcile ran exactly twice per process — at start-up and when the
recovery screen drives it — and BOTH fire before the hub has anything staged,
because the hub stages in RESPONSE to the declaration those runs precede.

Measured on the R-201 re-walk: unlock reconcile 11:43:07, hub staged 11:44:57
saying 'next cycle', a full report cycle ran 11:55:46, still unconsumed at
12:06. A guest command line applied it in 18 seconds — everything correct except
the trigger.

Bridge.RetryIfDeclared re-runs the SAME reconcile on a 5-minute tick, driven from
the box's own published declaration (OffboxReportStatus().State) — the very
statement the hub acts on, so the two cannot disagree.

Poll, not an ACK flag, decided on the promise: the no-target message says 'amint
megvannak' (no deadline) and the card says 'within a day'. Five minutes is inside
both by a wide margin and needs no hub change.

It stops by construction — a healthy box does no work and logs nothing — and the
settle gate is deliberately kept via ReconcileWhenSettled.

The marker was investigated and left alone: applied_marker lives in the guest's
DataDir, which a rebuild destroys, so it cannot suppress a legitimate re-run.

R-220's customer half: the refusal no longer tells the customer to choose from a
list that may be empty. It names the rebuild, points at the Meghajtók page, and
promises no outcome.

Red-proofs: remove the retry -> credential uncollected (the dead end reproduced);
drop the stop condition -> a healthy box hammers the hub; call Reconcile instead
of ReconcileWhenSettled -> settle gate bypassed; restore the old sentence -> the
impossible action returns.

28 packages ok, vet clean, all controller gates OK.
This commit is contained in:
2026-08-06 12:56:12 +02:00
parent 66d80efb9f
commit 9dc26459ea
6 changed files with 290 additions and 1 deletions
+49
View File
@@ -756,6 +756,55 @@ func main() {
return err
})
// ── R-218, THE CONSUME HALF (v0.203.0) ────────────────────────────────────────────────
//
// The declaration half shipped in v0.201.0 and works: a stranded box says
// `offsite.state=needs_credential`, and the hub's `offsiteheal` re-stages the one-time secret
// and logs *"the box re-consumes on its next cycle"*.
//
// **THERE WAS NO NEXT CYCLE.** `Reconcile` ran exactly twice in a process's life: once at
// start-up (the goroutine above, whose own comment says "retries on next config refresh/
// restart") and once when the recovery screen drives it (R-219). Both fire BEFORE the hub has
// anything staged, because the hub only stages in response to the declaration those runs
// precede.
//
// Measured on the R-201 re-walk, 2026-08-06: unlock reconcile 11:43:07 · hub staged 11:44:57
// saying "next cycle" · a full report cycle ran 11:55:46 · still unconsumed at 12:06. A guest
// command line moved it in **18 seconds**, which proves the credential, the target and the key
// were all fine and only the trigger was missing.
//
// So: re-run the SAME reconcile, on a tick, for exactly as long as the box itself says it needs
// a credential. The signal is the box's own `OffboxReportStatus().State` — the very statement
// the hub acts on, so the two can never disagree about whether a retry is wanted.
//
// WHY A POLL AND NOT AN ACK FLAG: the customer-facing text promises "amint megvannak" (no
// deadline) and the backups card promises "within a day"; a 5-minute tick is inside both by a
// wide margin, and it needs no hub change. If either promise ever tightens to minutes, revisit.
//
// IT STOPS BY CONSTRUCTION: the instant a target exists `OffboxReportStatus` stops returning
// the declared state, so a healthy box does no work and makes no noise. The settle gate is
// deliberately kept — `ReconcileWhenSettled` waits for floor knowledge exactly as at start-up.
sched.Every("offsite-credential-retry", 5*time.Minute, func(ctx context.Context) error {
if offsiteBridge == nil {
return nil // off-site not configured for this customer
}
attempted, err := offsiteBridge.RetryIfDeclared(ctx, func() bool {
st := backupMgr.OffboxReportStatus()
return st != nil && st.State == backup.OffsiteStateNeedsCredential
})
if !attempted {
return nil // a target exists (or we never needed one) — no work, and no log line
}
if err != nil {
// NOT a job failure: the box still declares, so the next tick tries again. Logged at
// WARN because a credential that never arrives is exactly what this exists to surface.
logger.Printf("[WARN] [offsite-apply] credential retry: %v (the box still declares a need; retrying)", err)
return nil
}
logger.Printf("[INFO] [offsite-apply] credential retry: the staged credential was collected and the tier applied")
return nil
})
// Cache refresh: every 5 minutes. Recompute the effective window each pass so the cached
// "next DB dump" follows a runtime window change (the UI save also refreshes immediately).
sched.Every("backup-cache", 5*time.Minute, func(ctx context.Context) error {