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

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:
2026-09-21 07:45:56 +02:00
parent f6909492cc
commit 0fe04bb6d5
15 changed files with 736 additions and 143 deletions
+20 -15
View File
@@ -278,12 +278,17 @@ func (s *Server) handleClaimPage(w http.ResponseWriter, r *http.Request, errorMs
s.logger.Printf("[ERROR] [web] claim: cannot read the persisted claim state while rendering the claim page: %v", cerr)
hash = ""
if errorMsg == "" {
errorMsg = "A beállító állapot most nem olvasható — próbáld újra néhány perc múlva."
errorMsg = s.msg(r, "claim.msg.state_unreadable")
}
}
reset := s.authEnabled() // a set password means this is the reset flow, not first-claim
// `Title` USED TO BE HERE, carrying the Hungarian "A szerver beállítása", and R-596 named it as
// one of the strings an English household reads. It is not: claim.html is a STANDALONE page with
// its own <title> (already `claim.jelszo_visszaallitasa` / `claim.a_szerver_beallitasa`), and
// `.Title` is consumed only by layout.html, which this page never includes. So the field was dead
// — rendered by nothing, in either language. Deleted rather than translated: a translated dead
// field would read, for ever after, as evidence that this page's title is handled here.
data := map[string]interface{}{
"Title": "A szerver beállítása",
"CustomerName": s.cfg.Customer.Name,
"Domain": s.cfg.Customer.Domain,
"Version": s.version,
@@ -307,18 +312,18 @@ func (s *Server) handleClaimPage(w http.ResponseWriter, r *http.Request, errorMs
func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
if !s.validClaimCSRF(r) {
s.handleClaimPage(w, r, "Érvénytelen űrlap — töltsd újra az oldalt.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.invalid_form"), "")
return
}
wasReset := s.authEnabled() // a password already set → this is a reset, not a first-claim
ip := clientIP(r)
if locked, _ := s.claimRateLocked(); locked {
s.handleClaimPage(w, r, "Túl sok próbálkozás — próbáld újra 15 perc múlva.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.too_many"), "")
return
}
if locked, _ := s.claimSourceLocked(ip); locked {
s.handleClaimPage(w, r, "Túl sok próbálkozás — próbáld újra 15 perc múlva.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.too_many"), "")
return
}
@@ -331,11 +336,11 @@ func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
// FAIL CLOSED: refuse the claim rather than validate against a possibly-superseded cache.
// NOT counted as a failed attempt — the customer typed nothing wrong.
s.logger.Printf("[ERROR] [web] claim: refusing the submission — the persisted claim state is unreadable: %v", cerr)
s.handleClaimPage(w, r, "A beállító állapot most nem olvasható — próbáld újra néhány perc múlva.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.state_unreadable"), "")
return
}
if hash == "" {
s.handleClaimPage(w, r, "Nincs aktív kód — kérj újat az alábbi gombbal.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.no_active_code"), "")
return
}
@@ -356,32 +361,32 @@ func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
if !valid {
if s.claimRegisterFailure(ip) {
s.reportClaimLockout(ip)
s.handleClaimPage(w, r, "Túl sok próbálkozás — próbáld újra 15 perc múlva.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.too_many"), "")
return
}
s.handleClaimPage(w, r, "Hibás vagy lejárt kód", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.bad_code"), "")
return
}
// Password rules (min length, match).
if len(newPassword) < claimMinPassword {
s.handleClaimPage(w, r, fmt.Sprintf("A jelszónak legalább %d karakter hosszúnak kell lennie", claimMinPassword), "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.password_too_short", claimMinPassword), "")
return
}
if newPassword != confirm {
s.handleClaimPage(w, r, "A két jelszó nem egyezik", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.password_mismatch"), "")
return
}
pwHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 10)
if err != nil {
s.logger.Printf("[ERROR] [web] claim: hashing new password: %v", err)
s.handleClaimPage(w, r, "Belső hiba a jelszó mentésekor", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.save_failed"), "")
return
}
if err := s.settings.SetPasswordHash(string(pwHash)); err != nil {
s.logger.Printf("[ERROR] [web] claim: saving password: %v", err)
s.handleClaimPage(w, r, "Belső hiba a jelszó mentésekor", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.save_failed"), "")
return
}
// Consume the generation (single-use) + mark claimed (set-only). Order: consume BEFORE
@@ -441,11 +446,11 @@ func (s *Server) handleClaimSubmit(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleClaimRequestNewCode(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
if !s.validClaimCSRF(r) {
s.handleClaimPage(w, r, "Érvénytelen űrlap — töltsd újra az oldalt.", "")
s.handleClaimPage(w, r, s.msg(r, "claim.msg.invalid_form"), "")
return
}
go s.requestHubResetCode() // fire-and-forget; the neutral response never reveals the outcome
s.handleClaimPage(w, r, "", "Ha az e-mail cím regisztrálva van, elküldtük a kódot.")
s.handleClaimPage(w, r, "", s.msg(r, "claim.msg.code_sent"))
}
// requestHubResetCode calls POST /api/v1/claim/reset-request with the box's own report key.