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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user