controller v0.264.0: the household is told when an update is undone or held, in its language
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
app_update_undone / app_update_held events (09 decision 15), on by default and seeded once on existing boxes; R-606 update sentences as key+args rendered per reader; R-646 startup applied-meta backfill for apps current with the catalog; R-620 a disabled notifier WARNs once per event type. Needs hub v0.120.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -330,6 +330,13 @@ func (m *Manager) pruneUndoCopies(dumpDir, stack string) {
|
||||
// on a misconfigured box. The write side logs loudly when it cannot persist (see
|
||||
// holdAppAfterFailedRollback), which is where that case is caught.
|
||||
func (m *Manager) RestoreHoldFor(stack string) (bool, string) {
|
||||
return m.RestoreHoldForLang(stack, m.boxLang())
|
||||
}
|
||||
|
||||
// RestoreHoldForLang is RestoreHoldFor with the sentence in lang (v0.264.0, R-606). The page and the
|
||||
// household mail ask for the READER's language; every other caller takes the box's. The Hungarian is
|
||||
// byte-identical to the literals it replaced (UpdateHoldFmt & co. — TestR606_HoldSentenceHungarianUnchanged).
|
||||
func (m *Manager) RestoreHoldForLang(stack, lang string) (bool, string) {
|
||||
if m == nil || m.settings == nil {
|
||||
return false, ""
|
||||
}
|
||||
@@ -340,43 +347,44 @@ func (m *Manager) RestoreHoldFor(stack string) (bool, string) {
|
||||
// Slice 4: one storage, two reasons. An update hold names the copy it can be restored from; a
|
||||
// restore hold names nothing, because the restore it refers to already consumed the copy.
|
||||
if h.Reason == settings.HoldReasonUpdateFailed {
|
||||
return true, m.undoHoldPrefix(h.UndoState) + m.updateHoldSentence(stack, h)
|
||||
return true, m.undoHoldPrefix(lang, h.UndoState) + updateHoldSentence(lang, stack, h)
|
||||
}
|
||||
when := h.At
|
||||
if t, err := time.Parse(time.RFC3339, h.At); err == nil {
|
||||
when = t.Format("2006-01-02 15:04")
|
||||
}
|
||||
return true, m.note("note.reconstitute.held", stack, when)
|
||||
return true, util.Text(lang, "note.reconstitute.held", stack, when)
|
||||
}
|
||||
|
||||
// undoHoldPrefix (v0.263.0) opens the hold sentence when the box already TRIED to undo the update and
|
||||
// that failed too: what was tried, then what state the data is in. "" when no undo was attempted, so
|
||||
// every hold written before v0.263.0 reads exactly as it did.
|
||||
func (m *Manager) undoHoldPrefix(state string) string {
|
||||
func (m *Manager) undoHoldPrefix(lang, state string) string {
|
||||
switch state {
|
||||
case "untouched", "half", "not_started":
|
||||
return m.note("hold.update.undo_failed") + " " + m.note("hold.update.undo_state."+state) + " "
|
||||
return util.Text(lang, "hold.update.undo_failed") + " " + util.Text(lang, "hold.update.undo_state."+state) + " "
|
||||
case "":
|
||||
return ""
|
||||
}
|
||||
m.logger.Printf("[WARN] [backup] unknown undo state %q on a hold — rendering the plain prefix", state)
|
||||
return m.note("hold.update.undo_failed") + " "
|
||||
return util.Text(lang, "hold.update.undo_failed") + " "
|
||||
}
|
||||
|
||||
// updateHoldSentence is the update hold's own sentence (slice 4, R-475, R-479), unchanged.
|
||||
func (m *Manager) updateHoldSentence(stack string, h settings.RestoreHold) string {
|
||||
copyDate := m.note("note.reconstitute.copy_latest")
|
||||
// updateHoldSentence is the update hold's own sentence (slice 4, R-475, R-479) in lang. Its Hungarian
|
||||
// is UpdateHoldFmt / UpdateHoldTierFmt / UpdateHoldLegacyFmt byte for byte (pinned by a test).
|
||||
func updateHoldSentence(lang, stack string, h settings.RestoreHold) string {
|
||||
copyDate := util.Text(lang, "note.reconstitute.copy_latest")
|
||||
if h.CopyDate != "" {
|
||||
copyDate = fmtHoldTime(h.CopyDate)
|
||||
}
|
||||
// R-475: name the tier when the hold recorded one; an older hold keeps its own sentence.
|
||||
if label := UpdateTierLabel(h.CopyTier); label != "" && h.CopyDate != "" {
|
||||
if label := UpdateTierLabelIn(lang, h.CopyTier); label != "" && h.CopyDate != "" {
|
||||
if h.CopyHolds != "" { // R-479: name what the copy holds
|
||||
return fmt.Sprintf(UpdateHoldFmt, stack, fmtHoldTime(h.At), label, copyDate, h.CopyHolds)
|
||||
return util.Text(lang, "hold.update.sentence", stack, fmtHoldTime(h.At), label, copyDate, copyHoldsIn(lang, h.CopyHolds))
|
||||
}
|
||||
return fmt.Sprintf(UpdateHoldTierFmt, stack, fmtHoldTime(h.At), label, copyDate)
|
||||
return util.Text(lang, "hold.update.sentence_tier", stack, fmtHoldTime(h.At), label, copyDate)
|
||||
}
|
||||
return fmt.Sprintf(UpdateHoldLegacyFmt, stack, fmtHoldTime(h.At), copyDate)
|
||||
return util.Text(lang, "hold.update.sentence_legacy", stack, fmtHoldTime(h.At), copyDate)
|
||||
}
|
||||
|
||||
// holdAppAfterFailedRollback records the R-379/R-380 hold and makes sure nothing restarts the app
|
||||
|
||||
@@ -45,6 +45,11 @@ func TestUndo_HoldSentenceSaysTheUndoWasTriedAndTheDataState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A KNOWN state before the language check — the loop above ranges over a map, so its last state is
|
||||
// random (this test read "half" and failed once for exactly that reason, 2026-09-23).
|
||||
if err := m.HoldAfterFailedUpdateHolding("app", at, copyAt, UpdateTierSecondDrive, "", "not_started"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.settings.SetLanguage("en"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -52,7 +57,11 @@ func TestUndo_HoldSentenceSaysTheUndoWasTriedAndTheDataState(t *testing.T) {
|
||||
if !strings.HasPrefix(why, "The update did not succeed, and the automatic undo did not either. The data is back as it was before the update") {
|
||||
t.Errorf("an English box gets the English prefix, got %q", why)
|
||||
}
|
||||
if strings.Contains(why, "automatikus visszaállítás") {
|
||||
t.Errorf("the Hungarian prefix must be GONE on an English box, got %q", why)
|
||||
// v0.264.0 (R-606): the WHOLE sentence is English now, not only the prefix.
|
||||
if !strings.Contains(why, "The update of app at 2026-09-23 10:00 did not succeed") || !strings.Contains(why, "second drive, 2026-09-23 03:30") {
|
||||
t.Errorf("the hold's own sentence must be English on an English box, got %q", why)
|
||||
}
|
||||
if strings.Contains(why, "automatikus visszaállítás") || strings.Contains(why, "frissítése") || strings.Contains(why, "meghajtó") {
|
||||
t.Errorf("no Hungarian may remain on an English box, got %q", why)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -142,37 +143,53 @@ func (m *Manager) UpdateTierOrderFor(stackName string) []int {
|
||||
|
||||
// UpdateCopyHolds is the customer phrase for what a copy on `tier` holds for this app — the second half
|
||||
// of the R-479 ruling: the hold sentence names WHAT the chosen copy holds, not only where it is.
|
||||
//
|
||||
// v0.264.0 (R-606): the phrase is a bundle key — it is a PROMISE ABOUT WHETHER THE HOUSEHOLD'S FILES
|
||||
// COME BACK and must reach an English household in English. The value returned (and stored in the
|
||||
// hold) is still the Hungarian, byte for byte; copyHoldsIn maps it back to its key at render time, so
|
||||
// a hold written by any earlier version localises too.
|
||||
func (m *Manager) UpdateCopyHolds(stackName string, tier int) string {
|
||||
outside := m.DataOutsideUnit(stackName)
|
||||
return util.Text(i18n.Default, updateCopyHoldsKey(m.DataOutsideUnit(stackName), tier))
|
||||
}
|
||||
|
||||
func updateCopyHoldsKey(outside bool, tier int) string {
|
||||
switch tier {
|
||||
case UpdateTierLocal:
|
||||
if outside {
|
||||
return "csak a beállításokat és az adatbázist tartalmazza, a fájlokat nem"
|
||||
return "hold.copy_holds.db_only"
|
||||
}
|
||||
return "a beállításokat, az adatbázist és az adatköteteket tartalmazza"
|
||||
case UpdateTierSecondDrive:
|
||||
return "hold.copy_holds.volumes"
|
||||
case UpdateTierSecondDrive, UpdateTierOffsite:
|
||||
if outside {
|
||||
return "a beállításokat, az adatbázist és a fájlokat tartalmazza"
|
||||
return "hold.copy_holds.files"
|
||||
}
|
||||
return "a beállításokat, az adatbázist és az adatköteteket tartalmazza"
|
||||
case UpdateTierOffsite:
|
||||
if outside {
|
||||
return "a beállításokat, az adatbázist és a fájlokat tartalmazza"
|
||||
}
|
||||
return "a beállításokat, az adatbázist és az adatköteteket tartalmazza"
|
||||
return "hold.copy_holds.volumes"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UpdateTierLabel is a tier's name in the customer's hold sentence. "" for an unknown tier.
|
||||
func UpdateTierLabel(tier int) string {
|
||||
// copyHoldKeys are the phrases a hold may have stored; the stored value is the Hungarian.
|
||||
var copyHoldKeys = []string{"hold.copy_holds.db_only", "hold.copy_holds.volumes", "hold.copy_holds.files"}
|
||||
|
||||
// copyHoldsIn renders a STORED copy-holds phrase in lang. An unknown phrase is returned as stored —
|
||||
// never an empty clause (a hold must not lose the sentence that says what the copy holds).
|
||||
func copyHoldsIn(lang, stored string) string {
|
||||
for _, k := range copyHoldKeys {
|
||||
if util.Text(i18n.Default, k) == stored {
|
||||
return util.Text(lang, k)
|
||||
}
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
// UpdateTierLabel is a tier's name in the customer's hold sentence (Hungarian). "" for an unknown tier.
|
||||
func UpdateTierLabel(tier int) string { return UpdateTierLabelIn(i18n.Default, tier) }
|
||||
|
||||
// UpdateTierLabelIn is UpdateTierLabel in lang (v0.264.0, R-606).
|
||||
func UpdateTierLabelIn(lang string, tier int) string {
|
||||
switch tier {
|
||||
case UpdateTierSecondDrive:
|
||||
return "második meghajtó"
|
||||
case UpdateTierLocal:
|
||||
return "saját meghajtó"
|
||||
case UpdateTierOffsite:
|
||||
return "távoli mentés"
|
||||
case UpdateTierSecondDrive, UpdateTierLocal, UpdateTierOffsite:
|
||||
return util.Text(lang, fmt.Sprintf("hold.update.tier.%d", tier))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user