v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
gates / gates (push) Successful in 23s

Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still
Hungarian literals in Go, so an English household clicked an English button and was
answered in Hungarian. 226 of them move into the bundle here.

A flash was the hard part: it travels inside the redirect URL and is rendered by a
DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by
an older controller carries prose and is shown verbatim — never a raw key, never dropped.

Also converted: page data and view-model text, the internal/api JSON answers, the alert
banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at
display, and the four page titles built around an app name (R-566 closed).

Hungarian is byte-identical, and that is measured rather than read:
scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses
a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict.
Its own first version filtered the capture through an ASCII-Hungarian word list and missed
seven real literals — the R-565 class. The filter is gone.

Nothing on the wire moved, and wire goldens now hold it there: the report's health
warnings and every notify event message stay Hungarian, because the hub MAILS the
controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those.

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 10:15:56 +02:00
parent 736f54b496
commit 5270bad76e
52 changed files with 10755 additions and 478 deletions
+31 -32
View File
@@ -109,8 +109,8 @@ func NewRouter(cfg *config.Config, configPath string, sett *settings.Settings, s
func (r *Router) SetRestarter(fn func()) { r.restart = fn }
// refuseNetworkStubDeploy is the deploy-time stub gate (RCA fix 2). Non-empty return = the
// Hungarian refusal for a registered NETWORK HDD_PATH whose filesystem in THIS namespace is a
// local stub. Everything else proceeds: idle autofs is HEALTHY (first app access mounts it);
// refusal KEY for a registered NETWORK HDD_PATH whose filesystem in THIS namespace is a
// local stub; the caller renders it in the request's language (v0.252.0, R-557). Everything else proceeds: idle autofs is HEALTHY (first app access mounts it);
// classification timeout/unknown fails OPEN (a wedged share is the unreachable badge's business);
// local (non-network) and empty paths keep today's behavior exactly.
func (r *Router) refuseNetworkStubDeploy(hdd string) string {
@@ -120,7 +120,7 @@ func (r *Router) refuseNetworkStubDeploy(hdd string) string {
if r.classifyFSPath(hdd) != system.FSClassStub {
return ""
}
return "A kiválasztott hálózati tárhely jelenleg nem érhető el az alkalmazások környezetéből — a telepítés nem indítható. Próbálja újra pár perc múlva, vagy jelezze az üzemeltetőnek."
return "api.deploy.netstorage_unreachable"
}
// SetReportPushTrigger wires the out-of-band hub report push used after geo changes.
@@ -223,19 +223,19 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// POST /api/stacks/{name}/start
case hasSuffix(path, "/start") && req.Method == http.MethodPost:
r.actionStack(w, "start", extractName(path, "/start"))
r.actionStack(w, req, "start", extractName(path, "/start"))
// POST /api/stacks/{name}/stop
case hasSuffix(path, "/stop") && req.Method == http.MethodPost:
r.actionStack(w, "stop", extractName(path, "/stop"))
r.actionStack(w, req, "stop", extractName(path, "/stop"))
// POST /api/stacks/{name}/restart
case hasSuffix(path, "/restart") && req.Method == http.MethodPost:
r.actionStack(w, "restart", extractName(path, "/restart"))
r.actionStack(w, req, "restart", extractName(path, "/restart"))
// POST /api/stacks/{name}/update
case hasSuffix(path, "/update") && req.Method == http.MethodPost:
r.actionStack(w, "update", extractName(path, "/update"))
r.actionStack(w, req, "update", extractName(path, "/update"))
// POST /api/stacks/{name}/optional-config
case hasSuffix(path, "/optional-config") && req.Method == http.MethodPost:
@@ -441,7 +441,7 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
r.logger.Printf("[WARN] [api] Deploy refused for %s: lifecycle=%s (not offered for new installs)",
name, st.Meta.EffectiveLifecycle())
writeJSON(w, http.StatusConflict, apiResponse{OK: false,
Error: "Ez az alkalmazás jelenleg nem telepíthető."})
Error: r.msg(req, "api.deploy.not_installable")})
return
}
@@ -452,9 +452,8 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
if hr := system.GetDockerVolumeHeadroom(); hr.OK && hr.BelowReserve {
r.logger.Printf("[WARN] [api] Deploy refused for %s: Docker volume below reserved buffer (%.1fG free, reserve %.1fG of %.0fG)",
name, hr.AvailGB, hr.ReserveGB, hr.TotalGB)
writeJSON(w, http.StatusInsufficientStorage, apiResponse{OK: false, Error: fmt.Sprintf(
"Nincs elég szabad tárhely a telepítéshez: csak %.0f GB szabad, és a rendszer %.0f GB tartalékot tart fenn az alapszolgáltatások (vezérlő, proxy) védelmében. Szabadítson fel helyet, vagy bővítse a tárhelyet.",
hr.AvailGB, hr.ReserveGB)})
writeJSON(w, http.StatusInsufficientStorage, apiResponse{OK: false,
Error: r.msg(req, "api.deploy.no_space", hr.AvailGB, hr.ReserveGB)})
return
}
@@ -462,9 +461,9 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
// must see a network filesystem (or its healthy idle autofs trigger) in THIS namespace — the one
// the app will consume the path in. A stub (plain local dir after a guest reboot) would silently
// send the app's data to the system drive.
if msg := r.refuseNetworkStubDeploy(body.Values["HDD_PATH"]); msg != "" {
if key := r.refuseNetworkStubDeploy(body.Values["HDD_PATH"]); key != "" {
r.logger.Printf("[WARN] [api] Deploy refused for %s: network HDD_PATH %s is a stub in the controller namespace", name, body.Values["HDD_PATH"])
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: msg})
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.msg(req, key)})
return
}
@@ -499,7 +498,7 @@ func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name stri
// polls GET /api/stacks/{name}). The old "Stack X deployed" message asserted completion before it
// was true — misleading for API/script consumers. Report that the deploy STARTED, not that it
// finished. 202 Accepted reflects "accepted, processing"; ok:true is preserved for the UI.
resp := apiResponse{OK: true, Message: "Telepítés elindítva – az állapot a kártyán követhető"}
resp := apiResponse{OK: true, Message: r.msg(req, "api.deploy.started")}
if warning != "" {
resp.Data = map[string]string{"warning": warning}
}
@@ -579,7 +578,7 @@ func desiredStateForAction(action string) (string, bool) {
}
}
func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
func (r *Router) actionStack(w http.ResponseWriter, req *http.Request, action, name string) {
r.logger.Printf("[INFO] [api] %s requested for stack: %s", action, name)
r.dbg("actionStack: action=%s name=%s", action, name)
@@ -610,7 +609,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
if gated, hdd := r.startGatedByMissingDrive(name); gated {
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Error: fmt.Sprintf("A(z) %s tárhely jelenleg nem elérhető — az alkalmazás nem indítható, amíg a meghajtó vissza nem csatlakozik.", hdd),
Error: r.msg(req, "api.start.drive_missing", hdd),
})
return
}
@@ -630,7 +629,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
if afterMB > usableMB {
writeJSON(w, http.StatusConflict, apiResponse{
OK: false,
Error: fmt.Sprintf("Nincs elég memória az indításhoz. Szükséges: %d MB, elérhető: %d MB (használt: %d MB / használható: %d MB)", stackMemMB, usableMB-usedMB, usedMB, usableMB),
Error: r.msg(req, "api.start.not_enough_memory", stackMemMB, usableMB-usedMB, usedMB, usableMB),
})
return
}
@@ -666,7 +665,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
r.logger.Printf("[ERROR] [api] %s for %s refused: could not record desired state: %v", action, name, derr)
writeJSON(w, http.StatusInternalServerError, apiResponse{
OK: false,
Error: "A művelet nem hajtható végre: az alkalmazás beállításai nem menthetők.",
Error: r.msg(req, "api.action.state_not_saved"),
})
return
}
@@ -708,7 +707,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
// finished — and "completed" exists only as update_phase=done on GET /api/stacks/{name}, which is
// written after the app's health is known. Pinned by TestR443_UpdateIsNeverReportedCompleteSynchronously.
if action == "update" {
writeJSON(w, http.StatusAccepted, apiResponse{OK: true, Message: "Frissítés elindult – az állapot a kártyán követhető",
writeJSON(w, http.StatusAccepted, apiResponse{OK: true, Message: r.msg(req, "api.update.started"),
Data: map[string]interface{}{"accepted": true, "completed": false}})
return
}
@@ -743,7 +742,7 @@ func (r *Router) updateOptionalConfig(w http.ResponseWriter, req *http.Request,
return
}
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Beállítások frissítve"})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.settings.updated")})
}
// --- Integration API handlers ---
@@ -788,9 +787,9 @@ func (r *Router) toggleIntegration(w http.ResponseWriter, req *http.Request, pro
return
}
msg := "Integráció engedélyezve"
msg := r.msg(req, "api.integration.enabled")
if !body.Enabled {
msg = "Integráció kikapcsolva"
msg = r.msg(req, "api.integration.disabled")
}
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: state, Message: msg})
}
@@ -1117,7 +1116,7 @@ func (r *Router) backupSnapshots(w http.ResponseWriter, req *http.Request) {
// triggerBackup runs the app-data database dumps. Disk-tier (restic) backup has
// moved to the host agent (slice 8C).
func (r *Router) triggerBackup(w http.ResponseWriter, _ *http.Request) {
func (r *Router) triggerBackup(w http.ResponseWriter, req *http.Request) {
r.dbg("triggerBackup: backupMgr=%v", r.backupMgr != nil)
if r.backupMgr == nil {
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "Backup not configured"})
@@ -1126,7 +1125,7 @@ func (r *Router) triggerBackup(w http.ResponseWriter, _ *http.Request) {
if r.backupMgr.IsRunning() {
r.dbg("triggerBackup: backup already running, rejecting")
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: "Mentés már folyamatban"})
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.msg(req, "api.backup.already_running")})
return
}
@@ -1134,23 +1133,23 @@ func (r *Router) triggerBackup(w http.ResponseWriter, _ *http.Request) {
r.backupMgr.MarkManualRun() // R-182: operator-triggered — its digest must not be collapsed into the nightly one
go r.backupMgr.RunDBDumps(context.Background())
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Mentés elindítva"})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.backup.started")})
}
// triggerTier2 runs the off-drive Tier 2 copies for all HDD apps (recovery unit + userdata to a
// different physical disk). Auto-targets and applies the rootfs-headroom guard internally.
func (r *Router) triggerTier2(w http.ResponseWriter, _ *http.Request) {
func (r *Router) triggerTier2(w http.ResponseWriter, req *http.Request) {
if r.backupMgr == nil {
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "Backup not configured"})
return
}
if r.backupMgr.IsRunning() {
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: "Mentés már folyamatban"})
writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.msg(req, "api.backup.already_running")})
return
}
r.logger.Println("[INFO] [api] Manual Tier 2 (off-drive) backup triggered")
go r.backupMgr.RunAllTier2()
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "2. mentés elindítva"})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.backup.tier2_started")})
}
// --- Metrics handlers ---
@@ -1337,7 +1336,7 @@ func (r *Router) selfupdateCheck(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: result})
}
func (r *Router) selfupdateTrigger(w http.ResponseWriter, _ *http.Request) {
func (r *Router) selfupdateTrigger(w http.ResponseWriter, req *http.Request) {
if r.updater == nil {
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "Self-update not configured"})
return
@@ -1347,7 +1346,7 @@ func (r *Router) selfupdateTrigger(w http.ResponseWriter, _ *http.Request) {
return
}
r.logger.Println("[INFO] [api] Manual self-update triggered")
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Frissítés elindítva"})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.update.kicked_off")})
}
// --- Config apply handler ---
@@ -1377,7 +1376,7 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) {
// idempotently, and a self-restart on every push would be a needless flap.
if prior, rerr := os.ReadFile(r.configPath); rerr == nil && bytes.Equal(prior, body) {
r.logger.Printf("[INFO] [api] Config apply: identical to current config (%d bytes) — no change, no restart", len(body))
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "A konfiguráció változatlan — nincs szükség újraindításra."})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.config.unchanged")})
return
}
@@ -1394,7 +1393,7 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) {
// effect on restart — singletons such as the Cloudflare client are built once at
// startup, so an in-process write alone would leave e.g. a rotated CF token unused.
r.logger.Printf("[INFO] [api] Config applied from Hub (%d bytes) — self-restarting to take effect", len(body))
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Konfiguráció alkalmazva — a vezérlő újraindul."})
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: r.msg(req, "api.config.applied")})
flushResponse(w)
if r.restart != nil {
r.restart()