hub v0.51.0: DR-tier-by-default — per-customer dr_tier flag (default ON, legacy backfill from reality), cascade stages, WG-registration auto-provision hook, offsite-requires-DR guard (F-6 policy), host-page capability chips (inactive=neutral)

Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
2026-07-12 20:37:00 +02:00
parent 007946faf4
commit 448a68237a
18 changed files with 824 additions and 87 deletions
+25 -3
View File
@@ -473,13 +473,15 @@ func (s *Server) configFormData(r *http.Request, isNew bool, cfg *store.Customer
ActiveNav: "configs",
Error: errMsg,
CSRFField: s.csrfField(r),
PBSDR: s.pbsDRViewFor(cfg.CustomerID),
PBSDR: s.pbsDRViewFor(cfg.CustomerID, cfg.DRTier),
}
}
// handleConfigNewForm shows the form to create a new customer config.
// handleConfigNewForm shows the form to create a new customer config. DRTier starts ON —
// DR-tier-by-default is the new-customer default (operator decision 2026-07-12 #2); opting out
// is the per-customer exception.
func (s *Server) handleConfigNewForm(w http.ResponseWriter, r *http.Request) {
s.renderConfigForm(w, r, true, &store.CustomerConfig{}, nil, "")
s.renderConfigForm(w, r, true, &store.CustomerConfig{DRTier: true}, nil, "")
}
// handleConfigCreate processes the form submission to create a new config.
@@ -534,6 +536,9 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
RetrievalPassword: retrievalPassword,
APIKey: apiKey,
ConfigJSON: configJSON,
// v0.51.0: the DR-tier flag (the form checkbox defaults ON for new customers). Set
// BEFORE applyOffsite — offsite provisioning is refused without the DR tier (F-6).
DRTier: formBool(r, "dr_tier"),
}
// Offsite provisioning (fail-closed): a provisioning error must NOT save a half-enabled config.
@@ -588,6 +593,9 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
cfg.CustomerName = strings.TrimSpace(r.FormValue("customer_name"))
cfg.Domain = strings.TrimSpace(r.FormValue("domain"))
cfg.Email = strings.TrimSpace(r.FormValue("email"))
// v0.51.0: the DR-tier flag — set BEFORE applyOffsite (offsite requires the tier) and
// applyPBSDR (which converges the descriptor toward it).
cfg.DRTier = formBool(r, "dr_tier")
// Server-side twin of the form's required attributes (v0.48.0 — B3). The error re-render is
// the STANDALONE page and carries the SUBMITTED overrides, so nothing the operator typed is
@@ -1040,6 +1048,13 @@ func (s *Server) applyOffsite(ctx context.Context, r *http.Request, cfg *store.C
if v := r.FormValue("offsite_enabled"); v != "on" && v != "true" {
return nil // not enabled → disabled by omission
}
// v0.51.0 (decision 3, drill F-6 closed by policy): offsite app backup REQUIRES the DR tier —
// fork-4 needs the escrow ceremony, and the ceremony hard-requires the PBS key K. Without the
// tier, provisioning would run straight into the F-6 dead end (EscrowState pending forever).
// The guard reads cfg.DRTier, which the handlers set from the form BEFORE calling here.
if !cfg.DRTier {
return fmt.Errorf("Offsite backup requires the DR tier — enable it first (the escrow ceremony depends on the PBS key)")
}
if s.offsite == nil {
return fmt.Errorf("offsite provisioning is not configured on this hub (no Hetzner token)")
}
@@ -1110,6 +1125,13 @@ func buildConfigJSON(r *http.Request) string {
return string(data)
}
// formBool reads a checkbox-style form value ("on"/"true" → true; anything else, incl. absent,
// → false).
func formBool(r *http.Request, name string) bool {
v := r.FormValue(name)
return v == "on" || v == "true"
}
// handleGeoDisable removes all [felhom-geo] WAF rules from Cloudflare for a customer. The Cloudflare
// WAF rules ARE the geo enforcement, so removing them disables geo-restriction. This is a hub→Cloudflare
// call (NOT into the box) and stays. The old secondary inbound notify to the controller