v0.253.0 — errors carry the key of the sentence they are (R-557 slice 2 release B)
gates / gates (push) Successful in 24s

179 Hungarian sentences were built deep inside a package with fmt.Errorf and printed by
whoever caught them: too late to translate where they are shown, too early where they are
made. Every one now carries its key across that gap. ZERO Hungarian error literals remain.

util.MsgError does three things at once, each earned:
  - Error() is the Hungarian, byte for byte, so every un-converted printer is unchanged;
  - errors.Is answers for the kind AND for a wrapped cause (KindErrorf dropped the cause);
  - an error ARGUMENT renders recursively, so "formázás sikertelen: %w" translates whole.
A foreign error — restic, docker, ssh, the stdlib — prints verbatim. It is not ours.

76 display sites go through errText, and TestNoErrErrorInPageOutput convicts any that do
not. memoryVerdict returns an error rather than a sentence, so the deploy's 409 and the
household's language come from one value; UpdateRefusal gained a Cause to carry it.

Plurals, one rule, stated once: a key with .one/.other takes its COUNT first. Not a
per-call-site flag — the producer somebody forgot would read "3 app is not running". The
guard caught a real key collision (alert.deadapp.one) the day the rule landed.

TWO DEFECTS FOUND IN MY OWN TOOLING, recorded rather than quietly fixed. The bulk converter
silently dropped multi-line concatenations, damaging 7 producers — and the parity gate could
not see it, because every surviving fragment WAS a real base literal while the CALL had lost
text; two behaviour tests caught it. And the counting script was case-sensitive, so it said
"0 left" while five remained.

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 11:44:30 +02:00
parent 5270bad76e
commit 7c05b59708
70 changed files with 1853 additions and 377 deletions
+29 -28
View File
@@ -23,6 +23,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
"io"
"io/fs"
"log"
@@ -163,7 +164,7 @@ func (m *Manager) acquireMigrating() error {
m.migrateMu.Lock()
defer m.migrateMu.Unlock()
if m.migrating {
return fmt.Errorf("migráció már folyamatban")
return util.MsgError("err.stacks.migracio_mar_folyamatban")
}
m.migrating = true
return nil
@@ -242,12 +243,12 @@ func (m *Manager) startMigration(scope, sourcePath, appName, targetPath string,
case "app":
cfg := m.LoadAppConfigByName(appName)
if cfg == nil {
return "", fmt.Errorf("alkalmazás nem található: %s", appName)
return "", util.MsgError("err.stacks.alkalmazas_nem_talalhato", appName)
}
j.Source, j.SourceNS = m.appSourceNS(cfg)
apps = []string{appName}
default:
return "", fmt.Errorf("ismeretlen migrációs hatókör: %s", scope)
return "", util.MsgError("err.stacks.ismeretlen_migracios_hatokor", scope)
}
j.TargetNS = appbackup.NamespaceRoot(j.Target, m.inGuest(j.Target))
@@ -266,7 +267,7 @@ func (m *Manager) startMigration(scope, sourcePath, appName, targetPath string,
j.UpdatedAt = time.Now().UTC()
m.setJob(j)
if err := m.writeJournal(j); err != nil {
return "", fmt.Errorf("migrációs napló írása: %w", err)
return "", util.MsgError("err.stacks.migracios_naplo_irasa", err)
}
launched = true
@@ -359,18 +360,18 @@ func (m *Manager) appsOnDrive(sourcePath string) []string {
// migValidate runs the pre-flight checks. Any failure aborts before any side-effect.
func (m *Manager) migValidate(j *MigrationJob) error {
if m.backupRunning != nil && m.backupRunning() {
return fmt.Errorf("biztonsági mentés folyamatban, próbáld újra")
return util.MsgError("err.stacks.biztonsagi_mentes_folyamatban_probald_ujra")
}
if m.testSeams == nil { // real rsync is only required when not using injected copy/verify seams
if _, err := exec.LookPath("rsync"); err != nil {
return fmt.Errorf("az rsync nem érhető el a rendszeren")
return util.MsgError("err.stacks.az_rsync_nem_erheto_el_a")
}
}
if j.Target == j.Source {
return fmt.Errorf("a cél és a forrás tároló megegyezik")
return util.MsgError("err.stacks.a_cel_es_a_forras_tarolo")
}
if m.settings == nil || !m.settings.IsStoragePathSchedulable(j.Target) {
return fmt.Errorf("a céltároló nem elérhető vagy nem választható")
return util.MsgError("err.stacks.a_celtarolo_nem_elerheto_vagy_nem")
}
// App-dir collision: refuse if any of the app's resolved appdata dir(s) already exists at target.
var collide []string
@@ -383,7 +384,7 @@ func (m *Manager) migValidate(j *MigrationJob) error {
}
}
if len(collide) > 0 {
return fmt.Errorf("ütközés a céltárolón — már létezik ezeknek az alkalmazásoknak az adata: %s", strings.Join(collide, ", "))
return util.MsgError("err.stacks.utkozes_a_celtarolon_mar_letezik_ezeknek", strings.Join(collide, ", "))
}
// Free-space check (best-effort; GetDiskUsage is nil on non-linux).
need := m.migSourceSize(j)
@@ -391,7 +392,7 @@ func (m *Manager) migValidate(j *MigrationJob) error {
if du := system.GetDiskUsage(j.Target); du != nil {
avail := int64(du.AvailGB * 1e9)
if need > avail {
return fmt.Errorf("nincs elég hely a céltárolón (kb. %d GB szükséges, %.1f GB szabad)", need/1_000_000_000, du.AvailGB)
return util.MsgError("err.stacks.nincs_eleg_hely_a_celtarolon_kb", need/1_000_000_000, du.AvailGB)
}
}
return nil
@@ -451,7 +452,7 @@ func (m *Manager) runMigration(ctx context.Context, j *MigrationJob) {
case PhaseDone, PhaseAborted:
return
default:
err = fmt.Errorf("ismeretlen migrációs fázis: %q", j.Phase)
err = util.MsgError("err.stacks.ismeretlen_migracios_fazis", j.Phase)
}
if err != nil {
m.migAbort(j, err)
@@ -484,7 +485,7 @@ func (m *Manager) migAbort(j *MigrationJob, cause error) {
func (m *Manager) migStop(j *MigrationJob) error {
for _, app := range j.Apps {
if err := m.doStop(app); err != nil {
return fmt.Errorf("alkalmazás leállítása sikertelen (%s): %w", app, err)
return util.MsgError("err.stacks.alkalmazas_leallitasa_sikertelen", app, err)
}
}
return nil
@@ -513,13 +514,13 @@ func (m *Manager) migCopy(ctx context.Context, j *MigrationJob) error {
}
if err := m.copySubtree(ctx, j, src, appbackup.AppDataDir(j.TargetNS, name)); err != nil {
u.Error = err.Error()
return fmt.Errorf("másolás sikertelen (%s appdata): %w", app, err)
return util.MsgError("err.stacks.masolas_sikertelen_appdata", app, err)
}
}
// the app's recovery unit (db-dumps + volume-dumps + compose + manifest)
if err := m.copySubtree(ctx, j, appbackup.RecoveryUnitPath(j.SourceNS, app), appbackup.RecoveryUnitPath(j.TargetNS, app)); err != nil {
u.Error = err.Error()
return fmt.Errorf("másolás sikertelen (%s mentés): %w", app, err)
return util.MsgError("err.stacks.masolas_sikertelen_mentes", app, err)
}
u.State = UnitCopied
_ = m.persistJob(j)
@@ -530,7 +531,7 @@ func (m *Manager) migCopy(ctx context.Context, j *MigrationJob) error {
j.CurrentApp = ""
if err := walkMerge(m.logger, j.SourceNS, j.TargetNS, m.appDataSkipSet(j), false, func(b int64) { j.BytesDone += b }); err != nil {
u.Error = err.Error()
return fmt.Errorf("ügyfél-adatok összefésülése sikertelen: %w", err)
return util.MsgError("err.stacks.ugyfel_adatok_osszefesulese_sikertelen", err)
}
u.State = UnitCopied
_ = m.persistJob(j)
@@ -557,12 +558,12 @@ func (m *Manager) migVerify(ctx context.Context, j *MigrationJob) error {
for _, name := range m.ResolveAppDataDirNames(app) {
if err := m.verifySubtree(ctx, appbackup.AppDataDir(j.SourceNS, name), appbackup.AppDataDir(j.TargetNS, name)); err != nil {
u.Error = err.Error()
return fmt.Errorf("ellenőrzés sikertelen (%s appdata): %w", app, err)
return util.MsgError("err.stacks.ellenorzes_sikertelen_appdata", app, err)
}
}
if err := m.verifySubtree(ctx, appbackup.RecoveryUnitPath(j.SourceNS, app), appbackup.RecoveryUnitPath(j.TargetNS, app)); err != nil {
u.Error = err.Error()
return fmt.Errorf("ellenőrzés sikertelen (%s mentés): %w", app, err)
return util.MsgError("err.stacks.ellenorzes_sikertelen_mentes", app, err)
}
u.State = UnitVerified
_ = m.persistJob(j)
@@ -573,7 +574,7 @@ func (m *Manager) migVerify(ctx context.Context, j *MigrationJob) error {
// assert-only merge walk: every source file has a content-identical counterpart at target.
if err := walkMerge(m.logger, j.SourceNS, j.TargetNS, m.appDataSkipSet(j), true, nil); err != nil {
u.Error = err.Error()
return fmt.Errorf("ügyfél-adatok ellenőrzése sikertelen: %w", err)
return util.MsgError("err.stacks.ugyfel_adatok_ellenorzese_sikertelen", err)
}
u.State = UnitVerified
_ = m.persistJob(j)
@@ -602,7 +603,7 @@ func (m *Manager) migFlipRedeploy(j *MigrationJob) error {
_ = m.persistJob(j)
if err := m.doFlipRedeploy(app, j.Target); err != nil {
u.Error = err.Error()
return fmt.Errorf("újratelepítés sikertelen (%s): %w", app, err)
return util.MsgError("err.stacks.ujratelepites_sikertelen", app, err)
}
u.State = UnitRedeployed
_ = m.persistJob(j)
@@ -623,11 +624,11 @@ func (m *Manager) migCleanup(j *MigrationJob) error {
}
for _, name := range m.ResolveAppDataDirNames(app) {
if err := os.RemoveAll(appbackup.AppDataDir(j.SourceNS, name)); err != nil {
return fmt.Errorf("forrás törlése sikertelen (%s appdata): %w", app, err)
return util.MsgError("err.stacks.forras_torlese_sikertelen_appdata", app, err)
}
}
if err := os.RemoveAll(appbackup.RecoveryUnitPath(j.SourceNS, app)); err != nil {
return fmt.Errorf("forrás törlése sikertelen (%s mentés): %w", app, err)
return util.MsgError("err.stacks.forras_torlese_sikertelen_mentes", app, err)
}
u.State = UnitCleaned
_ = m.persistJob(j)
@@ -638,11 +639,11 @@ func (m *Manager) migCleanup(j *MigrationJob) error {
// Remove every remaining child of the source namespace (the non-app/customer content).
entries, err := os.ReadDir(j.SourceNS)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("forrás névtér olvasása sikertelen: %w", err)
return util.MsgError("err.stacks.forras_nevter_olvasasa_sikertelen", err)
}
for _, e := range entries {
if err := os.RemoveAll(filepath.Join(j.SourceNS, e.Name())); err != nil {
return fmt.Errorf("forrás törlése sikertelen (%s): %w", e.Name(), err)
return util.MsgError("err.stacks.forras_torlese_sikertelen", e.Name(), err)
}
}
u.State = UnitCleaned
@@ -657,12 +658,12 @@ func (m *Manager) migCleanup(j *MigrationJob) error {
func (m *Manager) migCleanupAllowed(j *MigrationJob) error {
for key, u := range j.Units {
if stateRank(u.State) < stateRank(UnitVerified) {
return fmt.Errorf("cleanup gate: a(z) %q egység nincs ellenőrizve (állapot=%s)", key, u.State)
return util.MsgError("err.stacks.cleanup_gate_a_z_egyseg_nincs", key, u.State)
}
}
for _, app := range j.Apps {
if stateRank(j.Units[app].State) < stateRank(UnitRedeployed) {
return fmt.Errorf("cleanup gate: a(z) %q alkalmazás nincs újratelepítve (állapot=%s)", app, j.Units[app].State)
return util.MsgError("err.stacks.cleanup_gate_a_z_alkalmazas_nincs", app, j.Units[app].State)
}
}
return nil
@@ -749,7 +750,7 @@ func (m *Manager) doFlipRedeploy(name, target string) error {
return err
}
if !m.waitHealthy(name) {
return fmt.Errorf("az alkalmazás nem indult el az új tárhelyen")
return util.MsgError("err.stacks.az_alkalmazas_nem_indult_el_az")
}
return nil
}
@@ -879,7 +880,7 @@ func rsyncVerify(ctx context.Context, src, dst string) error {
}
}
if len(pending) > 0 {
return fmt.Errorf("%d függőben lévő átvitel maradt, pl. %q", len(pending), pending[0])
return util.MsgError("err.stacks.fuggoben_levo_atvitel_maradt_pl", len(pending), pending[0])
}
return nil
}
@@ -979,7 +980,7 @@ func walkMerge(lg *log.Logger, srcNS, dstNS string, skip map[string]bool, assert
return nil // dedup / already present
}
if assertOnly {
return fmt.Errorf("a forrásfájlnak nincs azonos másolata a célon: %s", rel)
return util.MsgError("err.stacks.a_forrasfajlnak_nincs_azonos_masolata_a", rel)
}
out := dst
if pathExists(dst) {