v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The notes a background run SAVES — last night's backup line, the last error, the proof result, the restore outcome — are written in the BOX's language at the moment they are written. A household that switches sees the previous run's note in the old language until the next run rewrites it: the operator's §16 option 1, stated rather than hidden. EndRestoreOp no longer receives a Hungarian literal from anywhere. The language switch is a globe. Two text links wrapped in the sidebar footer and asked the reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is <details>/<summary> — a menu with no script, drawn inline because the icon sprite lives only in layout.html and the visitor pages have their own shell. Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household can never inherit a language a previous visitor picked in the same browser. POST /lang is CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the language of the page the victim's own browser shows them — and safeBackPath refuses //evil.example as well as https://, because "starts with /" alone is not the test. §16 taken: a successful claim carries the cookie into the household's setting. TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the three pages that must not change. I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. UpdateOffboxStatus holds the settings write lock while running its callback; boxLang() wants the read lock; sync.RWMutex is not reentrant. On a real box an off-site run would have hung forever HOLDING the settings lock. Fixed by resolving the language before the callback, and guarded by a test that names the file and line in a second instead of hanging for 25 minutes. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -369,13 +369,32 @@ func (s *Server) templatesFor(lang string) *template.Template {
|
||||
return s.tmpl
|
||||
}
|
||||
|
||||
// langFor decides the language of one request: `?lang=hu|en` (a testing override, never persisted),
|
||||
// else the household's saved setting, else Hungarian.
|
||||
// langFor decides the language of one request. The order is fixed, and each step exists for a
|
||||
// different reader (v0.254.0, R-557 release C):
|
||||
//
|
||||
// 1. `?lang=hu|en` — the testing override. Never persisted, never sets a cookie.
|
||||
// 2. A REQUEST WITH A SESSION is the household's own: their saved setting, and the visitor cookie is
|
||||
// NOT read. A signed-in household must never see a language a previous visitor chose on the
|
||||
// sign-in page of the same browser.
|
||||
// 3. A request with NO session is a visitor at the door — sign-in, claim, recovery. They have no
|
||||
// setting to read and no right to change the household's, so their choice lives in their own
|
||||
// browser: the `felhom_lang` cookie, display-only.
|
||||
// 4. The household's setting anyway (a box whose visitor expressed no preference).
|
||||
// 5. Hungarian.
|
||||
//
|
||||
// Why a cookie and not the setting: the sign-in page is reachable by anyone who can reach the box.
|
||||
// Letting that change what the HOUSEHOLD reads would be an anonymous write to something they own.
|
||||
// Changing what the VISITOR THEMSELVES reads is not — 04-control-plane-authorization.md.
|
||||
func (s *Server) langFor(r *http.Request) string {
|
||||
if r != nil {
|
||||
if q := r.URL.Query().Get("lang"); i18n.IsSupported(q) {
|
||||
return q
|
||||
}
|
||||
if !s.hasSession(r) {
|
||||
if c, err := r.Cookie(langCookieName); err == nil && i18n.IsSupported(c.Value) {
|
||||
return c.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.settings != nil {
|
||||
return s.settings.GetLanguage()
|
||||
@@ -383,6 +402,16 @@ func (s *Server) langFor(r *http.Request) string {
|
||||
return i18n.Default
|
||||
}
|
||||
|
||||
// hasSession reports whether this request carries a VALID household session — the same test the auth
|
||||
// middleware makes, reused rather than restated so the two cannot drift apart.
|
||||
func (s *Server) hasSession(r *http.Request) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
c, err := r.Cookie(sessionCookieName)
|
||||
return err == nil && s.isValidSession(c.Value)
|
||||
}
|
||||
|
||||
// HubPushStatusData holds hub push status for the monitoring page.
|
||||
type HubPushStatusData struct {
|
||||
LastAttempt time.Time
|
||||
@@ -599,6 +628,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// i18n (v0.247.0): the household's dashboard language.
|
||||
case path == "/settings/language" && r.Method == http.MethodPost:
|
||||
s.languageSwitchHandler(w, r)
|
||||
// i18n (v0.254.0): the VISITOR's display language, for a page with no household signed in.
|
||||
// RequireAuth intercepts this on a password-protected box; the route is here so it also works on
|
||||
// a box with no password set and on an unclaimed one, where the middleware passes straight
|
||||
// through. Same handler either way.
|
||||
case path == langCookiePath && r.Method == http.MethodPost:
|
||||
s.langCookieHandler(w, r)
|
||||
case path == "/settings/notifications" && r.Method == http.MethodPost:
|
||||
s.settingsNotificationsHandler(w, r)
|
||||
case path == "/settings/notifications/test" && r.Method == http.MethodPost:
|
||||
@@ -880,6 +915,17 @@ func (s *Server) executeTemplateLang(w io.Writer, r *http.Request, name string,
|
||||
lang := s.langFor(r)
|
||||
if data != nil {
|
||||
data["Lang"] = lang
|
||||
// v0.254.0: the globe on the pages a VISITOR meets. It posts to /lang, not to the household
|
||||
// switch, and carries NO CSRF field — there is no session to mint one from, and the handler
|
||||
// writes only a display cookie in the visitor's own browser (CsrfProtect carries the reasoning).
|
||||
//
|
||||
// `back` is the path the visitor is ON, so the switch returns them to it. safeBackPath in the
|
||||
// handler is what makes that safe to take from an anonymous form.
|
||||
back := "/"
|
||||
if r != nil && r.URL != nil && r.URL.Path != "" {
|
||||
back = r.URL.Path
|
||||
}
|
||||
addLangOptions(data, lang, langCookiePath, back, "")
|
||||
}
|
||||
return s.templatesFor(lang).ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user