v0.246.0: an interrupted restore is told; the recovery-code reminder waits until the box can take it
gates / gates (push) Successful in 15s
gates / gates (push) Successful in 15s
MinAgent: 0.131.0 (unchanged). Requires hub v0.117.0 for restore_interrupted. R-550 (operator ruling: fix). A design reversed and recorded: the restore op-status was in memory by choice. Now restore-status.json in DataDir, written atomically at both ends of an op. At startup a record still marked running becomes a failed, interrupted result kept per app until that app's next restore, shown on /backups/restore and the off-site wizard, and raised once as restore_interrupted. Cooldowns stay in memory. R-546. The R-543 reminder bar consults the agent's own preflight ok (every blocking item, not a copy of pbs_storage_id), cached 60 s, probed only while paused. /backup/escrow shows a waiting card that polls and reloads instead of red crosses and English diagnostics. POST /api/escrow/start refuses 409 before staging or starting - the direct path chaos night used. Unknown readiness keeps the bar. Red-proofs (each seen failing): restore record across restart; main() calls both startup functions; startup helper with loading skipped; restore page card; bar held back; waiting card; start refusal. go build/vet/test ./... green, 28 packages; controller_gates --fast all OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-546 — is the box READY to run the escrow ceremony?
|
||||
//
|
||||
// Measured 2026-09-16/17 (chaos night Phase 0): after a fresh bind the box has no PBS storage for ~17
|
||||
// minutes. The agent's preflight is red, the ceremony refuses, and the R-543 reminder bar (v0.245.0)
|
||||
// was on every page urging the household into it. The escrow page itself already hid its start form
|
||||
// behind a red checklist — but it said nothing about WAITING, and a direct POST /api/escrow/start
|
||||
// (the path chaos night used) ran the ceremony and returned the agent's raw "-storage" stderr.
|
||||
//
|
||||
// THE DEFINITION IS THE AGENT'S OWN `ok`: every blocking preflight item (pbs_storage_id, dr_tier,
|
||||
// age_binary, hub_upload, sudo_grant; staged_secret is informational). Not a controller-side copy of
|
||||
// one of those items — the escrow.pbs_storage_id reading of R-546 was the SYMPTOM that box showed, and
|
||||
// a copy of one item is a second definition that drifts when the agent grows a sixth.
|
||||
//
|
||||
// Three answers, and UNKNOWN keeps today's behaviour (the bar shows): an unreachable agent must not
|
||||
// silence a reminder that R-543 made loud on purpose.
|
||||
//
|
||||
// Cost: the bar hangs off every page render, so the answer is cached for escrowReadyTTL, and a probe
|
||||
// is only ever made while the box is paused (escrowPaused gates it) — a finished box never asks.
|
||||
|
||||
const (
|
||||
escrowReadyTTL = 60 * time.Second
|
||||
escrowReadyTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
type escrowReadinessCache struct {
|
||||
mu sync.Mutex
|
||||
ready bool
|
||||
known bool
|
||||
checkedAt time.Time
|
||||
lastState string // for transition-only INFO logging
|
||||
}
|
||||
|
||||
// escrowReadiness returns (ready, known). fresh=true skips the cache (the escrow page and the start
|
||||
// API, where one request justifies one probe); fresh=false is the bar's cached read.
|
||||
func (s *Server) escrowReadiness(ctx context.Context, fresh bool) (ready, known bool) {
|
||||
c := &s.escrowReady
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !fresh && !c.checkedAt.IsZero() && time.Since(c.checkedAt) < escrowReadyTTL {
|
||||
return c.ready, c.known
|
||||
}
|
||||
pctx, cancel := context.WithTimeout(ctx, escrowReadyTimeout)
|
||||
defer cancel()
|
||||
ready, known, why := s.probeEscrowReadiness(pctx)
|
||||
c.ready, c.known, c.checkedAt = ready, known, time.Now()
|
||||
|
||||
state := "unknown"
|
||||
if known && ready {
|
||||
state = "ready"
|
||||
} else if known {
|
||||
state = "not-ready"
|
||||
}
|
||||
if state != c.lastState {
|
||||
switch state {
|
||||
case "ready":
|
||||
s.logger.Printf("[INFO] [web] escrow readiness: READY (agent preflight ok) — the recovery-code reminder is shown")
|
||||
case "not-ready":
|
||||
s.logger.Printf("[INFO] [web] escrow readiness: NOT READY (%s) — reminder held back; the escrow page says the box is still preparing", why)
|
||||
default:
|
||||
s.logger.Printf("[INFO] [web] escrow readiness: UNKNOWN (%s) — reminder shown (fail loud)", why)
|
||||
}
|
||||
c.lastState = state
|
||||
} else if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] escrow readiness: %s (%s)", state, why)
|
||||
}
|
||||
return ready, known
|
||||
}
|
||||
|
||||
// probeEscrowReadiness asks the agent. Never an error to the caller: failure is "unknown".
|
||||
func (s *Server) probeEscrowReadiness(ctx context.Context) (ready, known bool, why string) {
|
||||
agent, err := s.escrowAgentConn()
|
||||
if err != nil {
|
||||
return false, false, "agent unavailable: " + err.Error()
|
||||
}
|
||||
pf, err := agent.EscrowPreflight(ctx)
|
||||
if err != nil {
|
||||
return false, false, "preflight failed: " + err.Error()
|
||||
}
|
||||
if pf.OK {
|
||||
return true, true, "preflight ok"
|
||||
}
|
||||
var failing []string
|
||||
for _, it := range pf.Items {
|
||||
if !it.OK && it.ID != "staged_secret" {
|
||||
failing = append(failing, it.ID)
|
||||
}
|
||||
}
|
||||
sort.Strings(failing)
|
||||
return false, true, "failing: " + strings.Join(failing, ", ")
|
||||
}
|
||||
|
||||
// escrowNotReadyMessage is the one Hungarian sentence for "wait" — the page card and the API refusal
|
||||
// say the same thing.
|
||||
const escrowNotReadyMessage = "A doboz még készül — a távoli mentés kulcsát pár perc múlva tudod létrehozni. Ez az oldal magától frissül."
|
||||
Reference in New Issue
Block a user