hub v0.72.0 — R-70 + R-71c: offsite delivery-state detector, card, stuck event, R-39(a)-guarded self-heal restage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NKSN3gSg4TKVBBqkwW2djR
This commit is contained in:
2026-07-23 12:59:04 +02:00
parent c801cee647
commit 1133aade73
13 changed files with 1179 additions and 3 deletions
+12
View File
@@ -306,6 +306,11 @@ func main() {
// still renders, but saving with offsite enabled returns "not configured".
var offsiteBoxChecker *monitor.OffsiteBoxChecker
var pbsdrBoxChecker *monitor.PBSDRBoxChecker // R-5 v0.65.0: PBS-DR datastore fill (constructed with the tenantsync client below)
// R-71c: the delivery checker's self-heal invokes the SAME Re-issue path the operator button
// uses (webServer satisfies monitor.OffsiteReissuer). Wired ONLY when the provisioner exists —
// a heal that cannot actually restage must never run (it would emit a restaged event for a
// silent no-op: ReissueOffsiteForCustomer returns nil when offsite is unconfigured).
var offsiteHealReissuer monitor.OffsiteReissuer
if tok := os.Getenv("HETZNER_TOKEN"); tok != "" {
poolBoxID, _ := strconv.ParseInt(os.Getenv("HETZNER_POOL_BOX_ID"), 10, 64)
location := os.Getenv("HETZNER_LOCATION")
@@ -317,6 +322,7 @@ func main() {
API: client, Store: dataStore, Scanner: offsite.SSHHostKeyScanner{}, PoolBoxID: poolBoxID, Location: location, Logger: logger,
})
logger.Printf("[INFO] Offsite provisioning enabled (pool_box=%d, location=%s)", poolBoxID, location)
offsiteHealReissuer = webServer // R-71c heal armed (provisioner present)
// R-5 (v0.64.0): the pool-box aggregate checker shares the SAME client + pool box id (GET-only).
// It needs a valid box id to poll; without one, the aggregate stays unconfigured.
if poolBoxID != 0 {
@@ -544,6 +550,11 @@ func main() {
// (90/95% of quota_gb) + staleness (enabled+escrowed but no run >48h — the silently-stuck detector;
// run FAILURES already alert via backup_failed). Nil-safe on pre-v0.109 reports. Same sweep.
offsiteChecker := monitor.NewOffsiteChecker(dataStore, 0, dispatcher.ProcessEvent, logger)
// R-70 + R-71c: the delivery-state checker — surfaces the burned-credential shape as
// offsite_delivery_stuck (warning, 24h/customer) and self-heals it via the Re-issue path
// (offsite_credential_restaged, one restage/customer/24h, R-39(a)-guarded). Cooldowns are
// durable (events table), so a hub restart neither floods nor silently re-heals.
offsiteDeliveryChecker := monitor.NewOffsiteDeliveryChecker(dataStore, offsiteHealReissuer, dispatcher.ProcessEvent, logger)
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
@@ -561,6 +572,7 @@ func main() {
hostMgmtPlaneChecker.Check()
hostOOBChecker.Check()
offsiteChecker.Check()
offsiteDeliveryChecker.Check()
if offsiteBoxChecker != nil {
offsiteBoxChecker.Check() // R-5: restic pool-box aggregate (fetch-throttled internally)
}