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:
@@ -93,7 +93,7 @@ var ErrOffsiteQuota = errors.New("offsite quota exceeded")
|
||||
// of showing the previous run's verdict under a „started" message.
|
||||
// offboxWholeUnitGap is the pseudo-path used to report a WHOLE-unit gap through the mandatory-gap
|
||||
// notification, so a skipped app and a skipped directory reach the operator in one vocabulary.
|
||||
const offboxWholeUnitGap = "(a teljes alkalmazás — nincs helyi mentési egysége)"
|
||||
const offboxWholeUnitGapKey = "note.offsite.whole_unit_gap"
|
||||
|
||||
var ErrOffboxRunInFlight = fmt.Errorf("an off-box backup is already running; this request did not start a new one")
|
||||
|
||||
@@ -226,20 +226,24 @@ func (m *Manager) OffsiteFailureMessage(err error, dur time.Duration) string {
|
||||
if m != nil && m.settings != nil {
|
||||
t = m.settings.GetOffboxTarget()
|
||||
}
|
||||
return offsiteFailureMessage(t, err, dur)
|
||||
return offsiteFailureMessage(t, err, dur, m.boxLang())
|
||||
}
|
||||
|
||||
func offsiteFailureMessage(t *settings.OffboxTarget, err error, dur time.Duration) string {
|
||||
// offsiteFailureMessage builds the note that is SAVED in OffboxTarget.LastError and read on the page
|
||||
// days later, so it is written in the box's language at write time (release C, R-557 option 1).
|
||||
// The failure CLASS is still decided by ClassifyOffsiteFailure from a kind, never from these words
|
||||
// (R-553) — which is why translating them is safe.
|
||||
func offsiteFailureMessage(t *settings.OffboxTarget, err error, dur time.Duration, lang string) string {
|
||||
head := map[OffsiteFailureClass]string{
|
||||
OffsiteFailQuota: "A távoli mentés nem fért el a tárhelykereten belül",
|
||||
OffsiteFailOrphaned: "A távoli tárhely egy korábbi, már nem elérhető kulccsal készült",
|
||||
OffsiteFailNoRepo: "A távoli tárhelyen nincs mentési adattár",
|
||||
OffsiteFailNoUnits: "Nem volt mit menteni: egyetlen kijelölt alkalmazásnak sem található mentése",
|
||||
OffsiteFailTransport: "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés)",
|
||||
OffsiteFailUnknown: "A távoli mentés ismeretlen okból nem sikerült",
|
||||
OffsiteFailQuota: util.Text(lang, "note.offsite.fail_quota"),
|
||||
OffsiteFailOrphaned: util.Text(lang, "note.offsite.fail_orphaned"),
|
||||
OffsiteFailNoRepo: util.Text(lang, "note.offsite.fail_no_repo"),
|
||||
OffsiteFailNoUnits: util.Text(lang, "note.offsite.fail_no_units"),
|
||||
OffsiteFailTransport: util.Text(lang, "note.offsite.fail_transport"),
|
||||
OffsiteFailUnknown: util.Text(lang, "note.offsite.fail_unknown"),
|
||||
}[ClassifyOffsiteFailure(err)]
|
||||
if head == "" {
|
||||
head = "A távoli mentés nem sikerült"
|
||||
head = util.Text(lang, "note.offsite.fail_head")
|
||||
}
|
||||
return fmt.Sprintf("%s (%s): %s", head, dur.Round(time.Second), sanitiseOffsiteErrorFor(t, err))
|
||||
}
|
||||
@@ -988,6 +992,14 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
if runErr == nil {
|
||||
snapshots = m.offboxRecordStats(ctx, base, env)
|
||||
}
|
||||
// THE LANGUAGE IS RESOLVED HERE, OUTSIDE THE CALLBACK, AND IT IS NOT A STYLE CHOICE.
|
||||
//
|
||||
// UpdateOffboxStatus holds the settings WRITE lock while it runs `fn`, and boxLang() reads the
|
||||
// language through the settings READ lock. sync.RWMutex is not reentrant, so calling m.note()
|
||||
// inside this callback DEADLOCKS — and it deadlocks holding the settings lock, which then wedges
|
||||
// everything else that touches settings.json on that box. Written this way once and caught by the
|
||||
// suite timing out at 25 minutes; TestNoteHelpersAreNotCalledUnderTheSettingsLock keeps it fixed.
|
||||
lang := m.boxLang()
|
||||
if perr := m.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
|
||||
o.LastRun = time.Now().UTC().Format(time.RFC3339)
|
||||
// R-100: LastRun above records the ATTEMPT; this records the RESULT. The hub's staleness
|
||||
@@ -1047,7 +1059,7 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
notify = cp
|
||||
}
|
||||
for _, a := range runResult.missingUnprotected {
|
||||
notify[a] = append(notify[a], offboxWholeUnitGap)
|
||||
notify[a] = append(notify[a], util.Text(lang, offboxWholeUnitGapKey))
|
||||
}
|
||||
}
|
||||
m.offboxGapNotify(notify)
|
||||
@@ -1074,13 +1086,11 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
// available backup and were left out", which names a problem with no next step and reads
|
||||
// the same whether the customer must act or simply wait.
|
||||
if len(runResult.missingUnprotected) > 0 {
|
||||
warns = append(warns, fmt.Sprintf(
|
||||
"Ezek az alkalmazások NEM kerültek be a távoli mentésbe, mert még nincs helyi mentési egységük: %s. A következő mentés általában már elkészíti — ha a második futás után is itt szerepelnek, szólj az üzemeltetőnek.",
|
||||
warns = append(warns, util.Text(lang, "note.offsite.no_local_unit",
|
||||
strings.Join(runResult.missingUnprotected, ", ")))
|
||||
}
|
||||
if len(runResult.missingNotDeployed) > 0 {
|
||||
warns = append(warns, fmt.Sprintf(
|
||||
"Ezek az alkalmazások ki vannak jelölve távoli mentésre, de nincsenek telepítve, ezért nem menthetők: %s. Ha már nincs rájuk szükséged, vedd ki a kijelölésüket a Távoli mentés oldalon.",
|
||||
warns = append(warns, util.Text(lang, "note.offsite.not_installed",
|
||||
strings.Join(runResult.missingNotDeployed, ", ")))
|
||||
}
|
||||
// 3a: capture-gap warnings (structurally-refused / on-disk-missing mandatory paths, undeployed).
|
||||
@@ -1100,14 +1110,14 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
|
||||
blockedApps = append(blockedApps, n)
|
||||
}
|
||||
if len(blockedApps) > 0 {
|
||||
warns = append(warns, fmt.Sprintf("Figyelmeztetés: a tárhelykeret miatt %d alkalmazásnál csak konfiguráció- és adatbázis-mentés készült: %s.",
|
||||
warns = append(warns, util.Text(lang, "note.offsite.quota_partial",
|
||||
len(blockedApps), strings.Join(blockedApps, ", ")))
|
||||
}
|
||||
if sharesBlocked {
|
||||
warns = append(warns, sharesBlockedWarning())
|
||||
}
|
||||
// SLICE 4: approaching the soft quota (≥80%, <100%) — warn on an otherwise-OK run.
|
||||
if qw := offboxQuotaWarning(o); qw != "" {
|
||||
if qw := offboxQuotaWarning(o, lang); qw != "" {
|
||||
warns = append(warns, qw)
|
||||
}
|
||||
o.LastWarning = strings.Join(warns, " ")
|
||||
@@ -1739,7 +1749,7 @@ func offboxQuotaState(t *settings.OffboxTarget) (usedGB, quotaGB int, over bool)
|
||||
|
||||
// offboxQuotaWarning returns the Hungarian ≥80% (<100%) usage notice, or "" (quota unset / usage fine /
|
||||
// already over — over-quota is the run-refusal error, not a warning).
|
||||
func offboxQuotaWarning(t *settings.OffboxTarget) string {
|
||||
func offboxQuotaWarning(t *settings.OffboxTarget, lang string) string {
|
||||
if t == nil || t.QuotaGB <= 0 || t.RepoSizeBytes <= 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -1748,7 +1758,7 @@ func offboxQuotaWarning(t *settings.OffboxTarget) string {
|
||||
if pct < 80 || t.RepoSizeBytes >= limit {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("A távoli mentés a keret %d%%-át használja (%d/%d GB).", pct, t.RepoSizeBytes/offboxGiB, t.QuotaGB)
|
||||
return util.Text(lang, "note.offsite.quota_usage", int(pct), int(t.RepoSizeBytes/offboxGiB), t.QuotaGB)
|
||||
}
|
||||
|
||||
// OffboxQuotaPercent returns the usage percentage for the /backups usage bar (0 when no quota/size).
|
||||
|
||||
Reference in New Issue
Block a user