v0.259.0 — the claim page and the backup warnings answer in the reader's language (R-596, R-598)
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
The 2026-09-20 English drill ended one screen short: the claim page was English and its answers were Hungarian, so a household who mistyped the code from their e-mail could not tell a typo from a dead code. Fourteen call sites carrying nine messages now go through s.msg; the backup page's two protection warnings — which are promises about whether the customer's files are safe — follow the same route. Hungarian is byte-identical, proved structurally by the go-parity gate against the frozen base capture and red-proofed on a single added full stop. data["Title"] was DEAD (claim.html is standalone; .Title is layout.html's) and is deleted rather than translated — a translated dead field is a permanent false signal about where the page's title comes from. Six existing copy-contract tests were kept, not weakened: each now resolves its key through the real bundle, so it still convicts on a reworded Hungarian sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -159,20 +159,29 @@ func baseName(p string) string {
|
||||
// HEALTHY RENDERS NOTHING. There is deliberately no "your backup is safe" banner: a working
|
||||
// configuration must look normal, or every customer's dashboard grows a permanent notice and the
|
||||
// warning stops meaning anything (E-2 Scenario E).
|
||||
//
|
||||
// R-598 — THESE ARE PROMISES ABOUT WHETHER THE CUSTOMER'S FILES ARE SAFE, and until v0.259.0 they
|
||||
// were composed in Go as finished Hungarian sentences and handed to the page as DATA. That is the
|
||||
// defect class R-573 and R-590 already cost: no template-parity fixture and no English-page test can
|
||||
// see a Hungarian sentence arriving as a struct field. So the constants below are now KEYS, and
|
||||
// degradedMessageFor returns the key; the one place that has a language resolves it.
|
||||
//
|
||||
// The Hungarian text moved to the bundle byte-for-byte (scripts/i18n_go_parity.py measures that
|
||||
// against the frozen base capture), so a Hungarian household reads exactly the same words.
|
||||
const (
|
||||
backupTargetDegradedText = "A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — " +
|
||||
"így hibás fájlok ellen véd, lemezhiba ellen nem. Csatlakoztass egy második meghajtót a teljes védelemhez."
|
||||
backupTargetOfferText = "Ezt a meghajtót kijelölheted a rendszermentés helyéül — így egy lemezhiba " +
|
||||
"után is vissza tudod állítani a rendszert."
|
||||
backupTargetDegradedKey = "backup.target.degraded"
|
||||
backupTargetOfferKey = "backup.target.offer"
|
||||
// backupTargetAbsentText (R-114) is the CONFIGURED-BUT-GONE state. It is VERBATIM the hub's
|
||||
// customerMessages["backup_target_absent"] (felhom.eu hub/internal/notify/templates.go:93) so the
|
||||
// banner a customer reads on the page and the email they receive say exactly the same thing — a
|
||||
// customer who is told two different stories about one drive trusts neither.
|
||||
//
|
||||
// DRIFT RISK, filed not fixed: this string now lives in two repos with nothing binding them. If
|
||||
// one is reworded the other silently disagrees.
|
||||
backupTargetAbsentText = "A rendszermentés meghajtója nem érhető el — amíg vissza nem " +
|
||||
"csatlakoztatod, a teljes rendszermentés nem készül el."
|
||||
// one is reworded the other silently disagrees. (v0.259.0: the HUNGARIAN side of the drift risk is
|
||||
// unchanged — hu.json carries the same bytes the const carried. The ENGLISH side is new: the hub's
|
||||
// English `mail.event.backup_target_absent` and this key must say the same thing too, and nothing
|
||||
// binds those either. Same row, now twice as wide.)
|
||||
backupTargetAbsentKey = "backup.target.absent"
|
||||
)
|
||||
|
||||
// degradedMessageFor is the single decision point for "does the customer see anything?" — extracted
|
||||
@@ -182,6 +191,10 @@ const (
|
||||
// Returns "" for BOTH healthy and unknown. They are different states with the same rendering, and
|
||||
// collapsing them here is deliberate: unknown means we could not ask, which is not evidence of
|
||||
// degradation (the absence-read-as-a-value mistake R-88 Part 2 closed).
|
||||
// It returns a bundle KEY, not a sentence (R-598). The decision — "does the customer see anything,
|
||||
// and which fact is it?" — is language-free and stays here; the words are chosen by the caller that
|
||||
// knows who is reading. Returning a key also means an untranslated state is a visible key on the
|
||||
// page rather than a Hungarian sentence on an English one, which is the failure mode worth having.
|
||||
func degradedMessageFor(st BackupTargetState) string {
|
||||
if !st.Known || !st.Degraded {
|
||||
return ""
|
||||
@@ -189,9 +202,9 @@ func degradedMessageFor(st BackupTargetState) string {
|
||||
if st.TargetAbsent {
|
||||
// R-114: configured, drive gone. A different fact with a different remedy, so a different
|
||||
// sentence — routed through here so there is still exactly one place that decides copy.
|
||||
return backupTargetAbsentText
|
||||
return backupTargetAbsentKey
|
||||
}
|
||||
return backupTargetDegradedText
|
||||
return backupTargetDegradedKey
|
||||
}
|
||||
|
||||
// ---- the render (R-112) ------------------------------------------------------------------------
|
||||
@@ -220,15 +233,19 @@ type BackupTargetView struct {
|
||||
// backupTargetView resolves the state and reduces it to what the page renders, or nil for the two
|
||||
// states that render NOTHING — healthy and unknown. Returning nil rather than an empty struct means
|
||||
// a template typo cannot accidentally decorate a working box.
|
||||
func (s *Server) backupTargetView(ctx context.Context) *BackupTargetView {
|
||||
//
|
||||
// `lang` is the reader's, resolved once by the handler (s.langFor(r)) — the same shape
|
||||
// buildDataPathCards and buildStorageBars already use. The KEY comes from degradedMessageFor; the
|
||||
// WORDS are chosen here, and nowhere else.
|
||||
func (s *Server) backupTargetView(ctx context.Context, lang string) *BackupTargetView {
|
||||
st := s.resolveBackupTargetState(ctx)
|
||||
msg := degradedMessageFor(st)
|
||||
if msg == "" {
|
||||
key := degradedMessageFor(st)
|
||||
if key == "" {
|
||||
return nil // healthy or unknown — a working configuration must look normal
|
||||
}
|
||||
v := &BackupTargetView{Message: msg}
|
||||
v := &BackupTargetView{Message: s.msgLang(lang, key)}
|
||||
if st.OfferPath != "" {
|
||||
v.OfferPath, v.OfferLabel, v.OfferText = st.OfferPath, st.OfferLabel, backupTargetOfferText
|
||||
v.OfferPath, v.OfferLabel, v.OfferText = st.OfferPath, st.OfferLabel, s.msgLang(lang, backupTargetOfferKey)
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -244,11 +261,11 @@ func (s *Server) handleBackupTargetState(w http.ResponseWriter, r *http.Request)
|
||||
out["target"] = st.TargetID
|
||||
out["label"] = st.Label
|
||||
if st.Degraded {
|
||||
out["message"] = degradedMessageFor(st)
|
||||
out["message"] = s.msg(r, degradedMessageFor(st))
|
||||
if st.OfferPath != "" {
|
||||
out["offer_path"] = st.OfferPath
|
||||
out["offer_label"] = st.OfferLabel
|
||||
out["offer_message"] = backupTargetOfferText
|
||||
out["offer_message"] = s.msg(r, backupTargetOfferKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,17 +283,17 @@ func (s *Server) handleBackupTargetAssign(w http.ResponseWriter, r *http.Request
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, "érvénytelen kérés", nil)
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, s.msg(r, "backup.target.err.bad_request"), nil)
|
||||
return
|
||||
}
|
||||
stable := strings.TrimSpace(req.Path)
|
||||
if stable == "" {
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, "hiányzó meghajtó", nil)
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, s.msg(r, "backup.target.err.missing_drive"), nil)
|
||||
return
|
||||
}
|
||||
agent, err := s.agentClient()
|
||||
if err != nil {
|
||||
writeDiskJSON(w, http.StatusServiceUnavailable, false, "a host-ügynök nem elérhető", nil)
|
||||
writeDiskJSON(w, http.StatusServiceUnavailable, false, s.msg(r, "backup.target.err.agent_unreachable"), nil)
|
||||
return
|
||||
}
|
||||
// The agent operates on the RAW host mount path, not our stable guest path — the same asymmetry
|
||||
@@ -289,7 +306,7 @@ func (s *Server) handleBackupTargetAssign(w http.ResponseWriter, r *http.Request
|
||||
res, err := agent.SetBackupTarget(r.Context(), raw)
|
||||
if err != nil {
|
||||
s.logger.Printf("[WARN] [web] backup-target assign %s (raw %s): %v", stable, raw, err)
|
||||
writeDiskJSON(w, http.StatusBadGateway, false, "a mentési cél beállítása nem sikerült: "+s.errText(r, err), nil)
|
||||
writeDiskJSON(w, http.StatusBadGateway, false, s.msg(r, "backup.target.err.assign_failed")+s.errText(r, err), nil)
|
||||
return
|
||||
}
|
||||
// Record the INTENT only after the agent accepted, so our flag can never claim a target the agent
|
||||
|
||||
Reference in New Issue
Block a user