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) }