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

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:
2026-09-18 14:19:31 +02:00
parent eb6aa58aa7
commit 48f3336956
142 changed files with 2199 additions and 318 deletions
@@ -8,6 +8,7 @@
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<div class="shell-lang">{{template "lang_globe" .}}</div>
<div class="login-card">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="login-logo">
<h1 class="login-title">{{if .IsReset}}{{T "claim.jelszo"}} <span class="title-accent">{{T "claim.visszaallitasa"}}</span>{{else}}{{T "claim.a_szerver"}} <span class="title-accent">{{T "claim.beallitasa"}}</span>{{end}}</h1>
@@ -0,0 +1,6 @@
{{define "lang_globe"}}<details class="lang-globe">
<summary class="lang-globe-btn" aria-label="{{T "layout.nyelv"}}" title="{{T "layout.nyelv"}}"><svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="M2 12h20" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></svg></summary>
<ul class="lang-globe-menu">{{range .LangOptions}}
<li><form method="POST" action="{{$.LangAction}}">{{$.LangCSRF}}<input type="hidden" name="back" value="{{$.LangBack}}"><button type="submit" name="lang" value="{{.Code}}" lang="{{.Code}}" class="lang-globe-item{{if .Current}} current{{end}}"{{if .Current}} aria-current="true"{{end}}>{{.Name}}</button></form></li>{{end}}
</ul>
</details>{{end}}
@@ -116,7 +116,7 @@
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>{{T "layout.biztonsag_es_hozzaferes"}}</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">{{.Version}}</span>{{if .LangSwitch}}<form method="POST" action="/settings/language" class="lang-switch">{{.CSRFField}}<input type="hidden" name="back" value="{{.LangSwitchBack}}"><button type="submit" name="lang" value="hu" class="lang-switch-btn{{if eq .Lang "hu"}} active{{end}}" lang="hu">Magyar</button><button type="submit" name="lang" value="en" class="lang-switch-btn{{if eq .Lang "en"}} active{{end}}" lang="en">English</button></form>{{end}}
<span class="version">{{.Version}}</span>{{if .LangSwitch}}{{template "lang_globe" .}}{{end}}
{{if .AuthEnabled}}<a href="/logout" class="logout-link">{{T "layout.kijelentkezes"}}</a>{{end}}
</div>
</div>
@@ -8,6 +8,7 @@
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<div class="shell-lang">{{template "lang_globe" .}}</div>
<div class="login-card">
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="login-logo">
<h1 class="login-title">{{T "login.otthoni"}} <span class="title-accent">{{T "login.vezerlopult"}}</span></h1>
@@ -9,6 +9,7 @@
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<div class="shell-lang">{{template "lang_globe" .}}</div>
<div class="login-card" style="max-width:46rem">
<img src="/static/felhom-logo.svg" alt="Felhom.eu" class="login-logo">
+23 -5
View File
@@ -134,11 +134,22 @@ body {
font-size: .8rem;
}
.version { color: var(--text-3); }
/* i18n (v0.247.0): the language switch. Language names are shown in their own language, never translated. */
.lang-switch { display: inline-flex; gap: 2px; margin: 0; }
.lang-switch-btn { background: none; border: 1px solid var(--line); border-radius: var(--radius); color: var(--text-3); font: inherit; font-size: .75rem; padding: 0 .35rem; cursor: pointer; }
.lang-switch-btn:hover { color: var(--blue-bright); }
.lang-switch-btn.active { color: var(--text-1); border-color: var(--blue); }
/* i18n (v0.254.0): 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 find their way out of Hungarian.
<details>/<summary> — a menu with no script: it opens on click and on Enter, and a screen reader
announces it. Language names inside are shown in their OWN language, never translated. */
.lang-globe { position: relative; margin: 0; }
.lang-globe-btn { list-style: none; display: inline-flex; align-items: center; color: var(--text-3); cursor: pointer; padding: .15rem; border-radius: var(--radius); }
.lang-globe-btn::-webkit-details-marker { display: none; }
.lang-globe-btn:hover, .lang-globe[open] .lang-globe-btn { color: var(--blue-bright); }
.lang-globe-menu { position: absolute; bottom: calc(100% + .35rem); left: 50%; transform: translateX(-50%); margin: 0; padding: .25rem; list-style: none; min-width: 7.5rem; background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius); z-index: 20; }
.lang-globe-menu li { margin: 0; }
.lang-globe-menu form { margin: 0; }
.lang-globe-item { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text-2); font: inherit; font-size: .8rem; padding: .3rem .5rem; border-radius: var(--radius); cursor: pointer; white-space: nowrap; }
.lang-globe-item:hover { color: var(--blue-bright); }
.lang-globe-item.current { color: var(--text-1); font-weight: 600; }
.lang-globe-item.current::after { content: " \2713"; }
.logout-link { color: var(--text-3); text-decoration: none; transition: color 0.2s ease; }
.logout-link:hover { color: var(--blue-bright); }
/* Mobile chrome (v0.166.0) — hidden by default so desktop (>768px) stays pixel-identical; the
@@ -3664,3 +3675,10 @@ html::-webkit-scrollbar-track { background: var(--bg-0); }
.restore-danger-card {
border-left: 2px solid var(--crit);
}
/* i18n (v0.254.0): the globe on the pages a VISITOR meets — sign-in, claim, recovery. Those pages have
their own shell and no sidebar, so the globe is pinned to the top-right corner of the viewport, the
same place on all three. It posts to /lang, which sets a display-only cookie in that visitor's own
browser and never the household's setting. */
.shell-lang { position: absolute; top: 1rem; right: 1rem; z-index: 30; }
.shell-lang .lang-globe-menu { bottom: auto; top: calc(100% + .35rem); left: auto; right: 0; transform: none; }