hub v0.116.0: every new customer starts WITH the off-site copy (operator ruling)
gates / gates (push) Successful in 19s
gates / gates (push) Successful in 19s
Off-site is ON by default for a new customer — shared, 100 GB soft quota prefilled, the checkbox kept so an operator can opt a customer out. The reason is this repo's own [FACT]: the whole-guest tiers do not carry the data drive and a Tier-1 unit has no file leg, so with this unticked a one-drive box keeps NO copy of the household's own files. Measured on a fresh box the same day. The quota is prefilled because the fill warning only fires when quota_gb > 0. Also registers controller v0.244.0's app_deploy_started / app_deploy_failed in both allowedEventTypes and customerMessages, per the rule that the two move together. Red-proofed: dropping the default fails the new render test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -2070,6 +2070,17 @@ var allowedEventTypes = map[string]bool{
|
||||
"health_critical": true,
|
||||
"health_recovered": true,
|
||||
"app_deployed": true,
|
||||
// R-536 (controller v0.244.0). `app_deployed` used to fire beside the 202 that merely ACCEPTED a
|
||||
// deploy, so an install interrupted five seconds later stood on the timeline as a completed one
|
||||
// — measured 2026-09-16 on the drill box with mealie, which ended `not_deployed`. The accept-time
|
||||
// fact is worth keeping, so it becomes its own type, and the failure gets one too rather than
|
||||
// being silence. `app_deployed` now means the compose up succeeded and the durable state was
|
||||
// written.
|
||||
//
|
||||
// Both are customer-tier like `app_deployed` itself (NOT in operatorOnlyEvents): a household that
|
||||
// pressed „Telepítés" is the party who wants to know it did not finish.
|
||||
"app_deploy_started": true,
|
||||
"app_deploy_failed": true,
|
||||
"app_removed": true,
|
||||
"app_start_failed": true, // controller fix-3 (CAMPAIGN-3): a deployed app is not running
|
||||
"disaster_recovery_started": true,
|
||||
|
||||
@@ -139,6 +139,11 @@ var customerMessages = map[string]string{
|
||||
"app_deployed": "Alkalmazás telepítve.",
|
||||
"app_removed": "Alkalmazás eltávolítva.",
|
||||
"app_start_failed": "Egy telepített alkalmazás nem fut — ellenőrizze a rendszermonitort.",
|
||||
// R-536 (controller v0.244.0): the pair that makes „telepítve" mean it. The started event is the
|
||||
// acceptance `app_deployed` used to assert beside the 202; the failed one is what an interrupted
|
||||
// install used to be — silence. A new type must enter allowedEventTypes AND this map together.
|
||||
"app_deploy_started": "Alkalmazás telepítése elindult.",
|
||||
"app_deploy_failed": "Egy alkalmazás telepítése nem fejeződött be.",
|
||||
|
||||
// Disaster recovery events
|
||||
"disaster_recovery_started": "Katasztrófa helyreállítás elindítva.",
|
||||
|
||||
@@ -651,7 +651,30 @@ func (s *Server) configFormData(r *http.Request, isNew bool, cfg *store.Customer
|
||||
// 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{DRTier: true}, nil, "")
|
||||
// Operator ruling 2026-09-16: EVERY new customer starts with the off-site copy switched on —
|
||||
// shared (a sub-account on the pool box), 100 GB soft quota. Opting a customer out is the
|
||||
// per-customer exception, exactly as DRTier has been since 2026-07-12.
|
||||
//
|
||||
// Why it is a default rather than a nicety: the whole-guest tiers do not carry the customer's
|
||||
// data drive (07-backup-architecture §6, "[FACT] What the whole-guest tiers do NOT carry") and a
|
||||
// Tier-1 unit holds no file leg, so on a one-drive box with this unticked the household's own
|
||||
// files are in NO backup. That was measured on a fresh box on 2026-09-16: five photos deleted,
|
||||
// restored from the box's own backup, and none of them opened.
|
||||
//
|
||||
// The quota is PREFILLED rather than left empty on purpose: `monitor/offsite.go` only warns about
|
||||
// a filling store when quota_gb > 0, so an empty field means no fill warning ever fires.
|
||||
//
|
||||
// Every key the template touches is present, including box_type: the form compares it with `eq`,
|
||||
// and a missing key renders as an untyped nil that makes `eq` fail the whole page.
|
||||
overrides := map[string]interface{}{
|
||||
"offsite": map[string]interface{}{
|
||||
"enabled": true,
|
||||
"type": "shared",
|
||||
"quota_gb": 100,
|
||||
"box_type": "",
|
||||
},
|
||||
}
|
||||
s.renderConfigForm(w, r, true, &store.CustomerConfig{DRTier: true}, overrides, "")
|
||||
}
|
||||
|
||||
// handleConfigCreate processes the form submission to create a new config.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Off-site-by-default (operator ruling 2026-09-16) — the same shape as DR-tier-by-default, and for a
|
||||
// sharper reason: with the box's off-site copy unticked, a one-drive box keeps NO copy of the
|
||||
// household's own files. The whole-guest tiers do not carry the data drive (07-backup-architecture
|
||||
// §6) and a Tier-1 unit holds no file leg, so the files are covered by Tier 2 or Tier 3 and by
|
||||
// nothing else. On 2026-09-16 that was measured, not argued: five photos deleted on a fresh box,
|
||||
// restored from the box's own backup, and none of them opened.
|
||||
//
|
||||
// Red-proof partner: drop the overrides from handleConfigNewForm → this test fails at the checkbox.
|
||||
func TestOffsite_NewCustomerFormDefaultsOn(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
req := httptest.NewRequest("GET", "/configs/new", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
s.handleConfigNewForm(rr, req)
|
||||
|
||||
// The render itself is half the assertion: the form compares `box_type` with `eq`, and a default
|
||||
// that supplies some keys but not that one makes `eq` fail and takes the whole page with it.
|
||||
if rr.Code != 200 || rr.Body.Len() == 0 {
|
||||
t.Fatalf("the new-customer form did not render: code=%d len=%d", rr.Code, rr.Body.Len())
|
||||
}
|
||||
out := rr.Body.String()
|
||||
if strings.Contains(out, "incompatible types") || strings.Contains(out, "executing \"config_form") {
|
||||
t.Fatalf("the form rendered a template error:\n%s", out[max0(len(out)-400):])
|
||||
}
|
||||
|
||||
i := strings.Index(out, `name="offsite_enabled"`)
|
||||
if i < 0 {
|
||||
t.Fatal("the new-customer form has no off-site checkbox at all")
|
||||
}
|
||||
if !strings.Contains(out[i:min0(i+200, len(out))], "checked") {
|
||||
t.Fatal("the new-customer form does not default the off-site copy ON — a fresh one-drive box would keep no copy of the customer's files")
|
||||
}
|
||||
// The quota is prefilled on purpose: the fill warning in monitor/offsite.go only fires when
|
||||
// quota_gb > 0, so an empty field means the operator is never told the store is filling up.
|
||||
if !strings.Contains(out, `value="100"`) {
|
||||
t.Fatal("the off-site soft quota must be prefilled (100 GB) — an empty quota silences the fill warning")
|
||||
}
|
||||
}
|
||||
|
||||
func max0(n int) int {
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func min0(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user