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
+17 -14
View File
@@ -126,7 +126,7 @@ func (m *Manager) selectTier2TargetFrom(stackName, sourceDrive string, fullSize,
return &Tier2Target{
NamespaceRoot: NamespaceRoot(sp.Path, true),
Label: label,
Reason: "kézi választás",
Reason: m.note("note.tier2.reason_manual"),
}, nil
}
}
@@ -149,7 +149,7 @@ func (m *Manager) selectTier2TargetFrom(stackName, sourceDrive string, fullSize,
return &Tier2Target{
NamespaceRoot: NamespaceRoot(sp.Path, true), // Model A: in-guest mount IS the namespace root
Label: label,
Reason: "másik adatmeghajtó",
Reason: m.note("note.tier2.reason_other_drive"),
}, nil
}
}
@@ -171,10 +171,10 @@ func (m *Manager) selectTier2TargetFrom(stackName, sourceDrive string, fullSize,
}
return &Tier2Target{
NamespaceRoot: NamespaceRoot(sys, false), // system path is a real root → felhom-data appended
Label: "belső SSD (rendszer)",
Label: m.note("note.tier2.label_internal_ssd"),
IsSystemDrive: true,
StateOnly: true,
Reason: "nincs 2. adatmeghajtó — csak az adatbázis/konfiguráció fér a belső SSD-re; a nagy fájlokhoz 2. meghajtó kell",
Reason: m.note("note.tier2.reason_no_second"),
}, nil
}
@@ -321,14 +321,14 @@ func (m *Manager) RunTier2(stackName string) error {
target, err := m.selectTier2Target(stackName, fullSize, stateOnlySize)
if err != nil {
reason := tier2NoTargetReason(err)
reason := m.tier2NoTargetReason(err)
m.recordTier2NoTarget(stackName, reason)
m.logger.Printf("[INFO] [backup] Tier 2 for %s: no off-drive target — %s", stackName, reason)
return nil
}
// Defense-in-depth off-drive guard (selection already enforced it).
if m.sameDevice(sourceDrive, target.NamespaceRoot) {
m.recordTier2NoTarget(stackName, "a kiválasztott cél ugyanazon a fizikai lemezen van")
m.recordTier2NoTarget(stackName, m.note("note.tier2.same_disk"))
return nil
}
@@ -345,7 +345,7 @@ func (m *Manager) RunTier2(stackName string) error {
}
legs = kept
if droppedOptional {
warns = append(warns, "A belső SSD-n csak a konfiguráció, adatbázis és a kötelező adatok férnek el — a választható tartalom nem került másolásra.")
warns = append(warns, m.note("note.tier2.ssd_partial"))
}
}
@@ -383,7 +383,7 @@ func (m *Manager) RunTier2(stackName string) error {
// compose/app.yaml carries portable secrets and nothing from inside it is logged here.
m.logger.Printf("[WARN] [backup] Tier 2 %s: unit leg SKIPPED — the recovery unit on the source drive lists no database dumps and no volume tars, while the existing copy at %s does. The copy was PRESERVED rather than replaced with an empty one (R-403). The other legs continue.",
stackName, destUnit)
warns = append(warns, tier2UnitPreservedWarning)
warns = append(warns, m.note(tier2UnitPreservedKey))
} else if err := mirror(unitDir, destUnit); err != nil {
m.recordTier2Failure(stackName, target, err)
if m.tier2Notify != nil {
@@ -549,7 +549,7 @@ func (m *Manager) Tier2Info(stackName string) Tier2Info {
target, err := m.selectTier2Target(stackName, fullSize, stateOnlySize)
if err != nil {
info.NoTarget = true
info.NoTargetReason = tier2NoTargetReason(err)
info.NoTargetReason = m.tier2NoTargetReason(err)
return info
}
info.EffectiveLabel = target.Label
@@ -668,14 +668,17 @@ func (m *Manager) recordTier2NoTarget(stackName, reason string) {
})
}
func tier2NoTargetReason(err error) string {
// tier2NoTargetReason is SAVED (CrossDriveConfig.LastError / Tier2Info.NoTargetReason) and read on the
// per-app card later, so it is written in the box's language at write time (release C, R-557). The
// branch is on a SENTINEL, never on these words (R-553) — which is why translating them is safe.
func (m *Manager) tier2NoTargetReason(err error) string {
switch {
case errors.Is(err, errSSDNoHeadroom):
return "nincs elég hely a belső SSD-n — a nagy fájlok off-drive mentéséhez 2. meghajtó (vagy távoli tárhely) szükséges"
return m.note("note.tier2.no_space_ssd")
case errors.Is(err, errNoOffDiskTarget):
return "nincs másik fizikai meghajtó — a 2. mentéshez 2. meghajtó szükséges"
return m.note("note.tier2.no_other_drive")
default:
return err.Error()
return m.noteErr(err)
}
}
@@ -700,7 +703,7 @@ func rsyncMirror(src, dst string) error {
// tier2UnitPreservedWarning is the customer-facing half of the R-403 skip. It rides in
// CrossDriveBackup.LastWarning, which the per-app card already renders, so the refusal reaches the
// SURFACE and not only the log — the shape recordTier2NoTarget established.
const tier2UnitPreservedWarning = "A fő meghajtón lévő adatcsomag hiányos volt, ezért a másodlagos másolatban meglévő, teljes csomagot megőriztük. A másolat adatcsomagja ezért régebbi, mint ez a mentés."
const tier2UnitPreservedKey = "note.tier2.unit_preserved"
// unitPackageDate returns the `created_at` of a recovery unit's manifest — WHEN the package in that
// directory was captured. "" when the manifest is absent or unparseable, which the surface must read