0fe315b759
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
97 lines
4.5 KiB
Go
97 lines
4.5 KiB
Go
package web
|
|
|
|
import "net/http"
|
|
|
|
// The escrow reminder bar (R-543) — the household is ASKED for its recovery code.
|
|
//
|
|
// Off-site backup is ON by default (hub v0.116.0), and it does not RUN until the household has
|
|
// created its recovery code. That pause is a DESIGN, not a defect: the escrow is zero-knowledge, the
|
|
// household's code is the only key, and a run started without one would produce a copy nobody could
|
|
// ever open. Nothing here touches that mechanism.
|
|
//
|
|
// What was missing is that nothing ASKED. A fresh box sat at „Kulcsletétre vár" indefinitely while
|
|
// the backup page told the household their files were protected — measured on a fresh box
|
|
// 2026-09-16. A promise plus a pause nobody mentions is the same class of untruth as R-537 and R-538.
|
|
//
|
|
// This is the R-241 reminder bar, SECOND INSTANCE, on purpose: same session-cookie dismissal, same
|
|
// layout block shape, same "back at the next visit" behaviour. No second banner system was built.
|
|
const escrowBannerCookie = "felhom_escrow_banner"
|
|
|
|
// escrowPaused reads the same two facts tier3State reads (backup_page_state.go): the off-site tier is
|
|
// configured, and its escrow is not complete. Deliberately one predicate — a second copy would let
|
|
// the bar and the app rows tell the household two different stories about the same box.
|
|
func (s *Server) escrowPaused() bool {
|
|
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() || s.settings == nil {
|
|
return false
|
|
}
|
|
t := s.settings.GetOffboxTarget()
|
|
if t == nil {
|
|
return false
|
|
}
|
|
// The vocabulary is "" | "pending" | "escrowed" (agentapi). Anything that is not the finished
|
|
// state is a paused state — fail LOUD, so an unknown value reminds rather than goes quiet.
|
|
return t.EscrowState != "escrowed"
|
|
}
|
|
|
|
// hasAdminSession — the household is logged in right now.
|
|
//
|
|
// It mirrors RequireAuth's own check (auth.go), including the legacy-open box where no password is
|
|
// configured and every page is served. It exists because the bar hangs off executeTemplate, the
|
|
// render choke point for EVERY page: the login and claim pages bypass that function entirely, and
|
|
// this check is what additionally keeps a household reminder off the public guest share page, which
|
|
// carries a capability token and no admin session.
|
|
func (s *Server) hasAdminSession(r *http.Request) bool {
|
|
if !s.authEnabled() {
|
|
return true
|
|
}
|
|
c, err := r.Cookie(sessionCookieName)
|
|
return err == nil && s.isValidSession(c.Value)
|
|
}
|
|
|
|
// escrowBannerVisible — logged in, the tier is paused, and they have not clicked it away this visit.
|
|
func (s *Server) escrowBannerVisible(r *http.Request) bool {
|
|
if r == nil || !s.hasAdminSession(r) || !s.escrowPaused() {
|
|
return false
|
|
}
|
|
// R-546: while the agent says the ceremony cannot run yet, do not urge the household into it.
|
|
// Unknown readiness keeps the bar (fail loud).
|
|
if ready, known := s.escrowReadiness(r.Context(), false); known && !ready {
|
|
return false
|
|
}
|
|
if c, err := r.Cookie(escrowBannerCookie); err == nil && c.Value == "1" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// addEscrowBanner is called from executeTemplate for every authenticated page.
|
|
func (s *Server) addEscrowBanner(data map[string]interface{}, r *http.Request) {
|
|
if data == nil || !s.escrowBannerVisible(r) {
|
|
return
|
|
}
|
|
data["EscrowBanner"] = true
|
|
data["EscrowBannerBack"] = r.URL.Path
|
|
if data["CSRFField"] == nil {
|
|
data["CSRFField"] = s.csrfField(r)
|
|
}
|
|
// STATE, never customer data: which page, and the reason the bar is up. DEBUG because this fires
|
|
// on every page render and INFO is the operator's state-change level.
|
|
if s.isDebug() {
|
|
s.logger.Printf("[DEBUG] [web] escrow reminder: rendered on %s (offsite configured, escrow not complete)", r.URL.Path)
|
|
}
|
|
}
|
|
|
|
// escrowBannerDismissHandler records „Most nem" (POST /backup/escrow/banner/dismiss) — a browser
|
|
// SESSION cookie and nothing durable, exactly like R-241. The off-site tier is still paused whether
|
|
// or not anyone clicked, so the bar is back at the next visit and gone for good only when the escrow
|
|
// state becomes "escrowed".
|
|
func (s *Server) escrowBannerDismissHandler(w http.ResponseWriter, r *http.Request) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: escrowBannerCookie, Value: "1", Path: "/",
|
|
HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil,
|
|
// NO MaxAge and NO Expires — a session cookie, deliberately.
|
|
})
|
|
s.logger.Printf("[INFO] [web] escrow reminder: dismissed for this browser session; the off-site tier stays paused until the recovery code exists")
|
|
http.Redirect(w, r, redirectBackTo(r, "/launcher"), http.StatusFound)
|
|
}
|