controller: Tier-3 rename — customer-facing 'NAS-mentés' branding becomes 'Távoli mentés' (backups page, offbox flashes, quota copy); manual-target form generalized to any SFTP target; offbox_rename_gate.py enforces zero regressions

Claude-Session: https://claude.ai/code/session_01GzammAMzsJTgpQHqxwM2bC
This commit is contained in:
2026-07-13 08:23:43 +02:00
parent 8987ce0f67
commit a00afcc79d
4 changed files with 83 additions and 26 deletions
+11 -11
View File
@@ -44,7 +44,7 @@ func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
knownHosts := r.FormValue("known_hosts")
if host == "" || user == "" || repoPath == "" {
offboxRedirect(w, r, "A NAS címe, a felhasználó és a tárhely útvonala kötelező.", true)
offboxRedirect(w, r, "A cél címe, a felhasználó és a tárhely útvonala kötelező.", true)
return
}
if !strings.HasPrefix(repoPath, "/") {
@@ -60,7 +60,7 @@ func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
// First-time config requires the SSH key + a pinned known-host line (no blind TOFU).
existing := s.backupMgr.OffboxConfigured()
if !existing && (strings.TrimSpace(sshKey) == "" || strings.TrimSpace(knownHosts) == "") {
offboxRedirect(w, r, "Az első beállításhoz az SSH privát kulcs és a NAS ismert-host sora is kötelező.", true)
offboxRedirect(w, r, "Az első beállításhoz az SSH privát kulcs és a célgép ismert-host sora is kötelező.", true)
return
}
@@ -105,7 +105,7 @@ func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
return
}
s.logger.Printf("[INFO] [web] off-box target configured: %s@%s:%s (port %d, enabled=%v, escrow=%s)", user, host, repoPath, port, tgt.Enabled, tgt.EscrowState)
offboxRedirect(w, r, "A NAS mentési cél elmentve."+stageErr, stageErr != "")
offboxRedirect(w, r, "A távoli mentési cél elmentve."+stageErr, stageErr != "")
}
// offboxConfirmEscrowHandler marks the offsite repo password as escrowed under R (fork-4).
@@ -115,7 +115,7 @@ func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
// no restic_pw_sha256 and can never auto-confirm; the operator vouches by hand after a verified ceremony.
func (s *Server) offboxConfirmEscrowHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
offboxRedirect(w, r, "A távoli mentési cél nincs beállítva.", true)
return
}
if err := s.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.EscrowState = "escrowed" }); err != nil {
@@ -129,7 +129,7 @@ func (s *Server) offboxConfirmEscrowHandler(w http.ResponseWriter, r *http.Reque
if err := s.wipeStagedEscrow(r.Context()); err != nil {
s.logger.Printf("[ERROR] [web] escrow confirmed but the agent-staged secret was NOT wiped (re-confirm to retry): %v", err)
}
offboxRedirect(w, r, "A kulcs letétbe helyezése megerősítve — a NAS-mentés mostantól futhat.", false)
offboxRedirect(w, r, "A kulcs letétbe helyezése megerősítve — a távoli mentés mostantól futhat.", false)
}
// wipeStagedEscrow calls the injected seam (tests), else the agent's DELETE /escrow/stage-secret over the
@@ -183,18 +183,18 @@ func (s *Server) offboxToggleHandler(w http.ResponseWriter, r *http.Request) {
offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
return
}
offboxRedirect(w, r, "A NAS-mentés beállítása frissítve.", false)
offboxRedirect(w, r, "A távoli mentés beállítása frissítve.", false)
}
// offboxRunHandler triggers an off-box backup now (async — it can run for minutes).
func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
offboxRedirect(w, r, "A távoli mentési cél nincs beállítva.", true)
return
}
// fork-4 atomicity: refuse the run until the repo password is escrowed under R.
if !s.backupMgr.OffboxRunnable() {
offboxRedirect(w, r, "A NAS-mentés a kulcs letétbe helyezésére vár.", true)
offboxRedirect(w, r, "A távoli mentés a kulcs letétbe helyezésére vár.", true)
return
}
go func() {
@@ -204,14 +204,14 @@ func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("[WARN] [web] manual off-box backup failed: %v", err)
}
}()
offboxRedirect(w, r, "A NAS-mentés elindult (a futás után az állapot frissül).", false)
offboxRedirect(w, r, "A távoli mentés elindult (a futás után az állapot frissül).", false)
}
// offboxRestoreHandler restores an app's off-box data to a scratch dir (non-destructive — does NOT
// overwrite live data; the operator inspects the restored files).
func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
offboxRedirect(w, r, "A távoli mentési cél nincs beállítva.", true)
return
}
_ = r.ParseForm()
@@ -240,5 +240,5 @@ func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("[INFO] [web] off-box restore %s completed (async) → %s", app, dest)
s.backupMgr.EndRestoreOp(true, "A(z) "+app+" visszaállítva ide (ellenőrzésre): "+dest)
}()
offboxRedirect(w, r, "A NAS-visszaállítás elindult — az állapot itt frissül.", false)
offboxRedirect(w, r, "A távoli visszaállítás elindult — az állapot itt frissül.", false)
}
+13 -13
View File
@@ -132,7 +132,7 @@
<div class="stats-grid backup-page-cards">
<div class="stat-card {{if eq .Offbox.LastStatus "error"}}stat-warn{{end}}">
<div class="stat-value" style="font-size:1.15rem">{{if eq .Offbox.LastStatus "ok"}}&#10003; Rendben{{else if eq .Offbox.LastStatus "error"}}&#10007; Hiba{{else if eq .Offbox.LastStatus "running"}}Fut…{{else}}{{end}}</div>
<div class="stat-label">Utolsó NAS-mentés{{if .Offbox.LastRun}}<br><span class="relative-time">{{timeAgoStr .Offbox.LastRun}}</span>{{end}}</div>
<div class="stat-label">Utolsó távoli mentés{{if .Offbox.LastRun}}<br><span class="relative-time">{{timeAgoStr .Offbox.LastRun}}</span>{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}{{end}}</div>
@@ -140,7 +140,7 @@
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.05rem">{{if .Offbox.Enabled}}{{.Offbox.User}}@{{.Offbox.Host}}{{else}}Kikapcsolva{{end}}</div>
<div class="stat-label">{{if .Offbox.Enabled}}{{.Offbox.RepoPath}}{{else}}A NAS-mentés ki van kapcsolva{{end}}</div>
<div class="stat-label">{{if .Offbox.Enabled}}{{.Offbox.RepoPath}}{{else}}A távoli mentés ki van kapcsolva{{end}}</div>
</div>
</div>
{{if and .Offbox.Enabled (gt .Offbox.QuotaGB 0)}}
@@ -156,7 +156,7 @@
{{if .Offbox.LastWarning}}<p class="form-hint" style="color:var(--warn)">{{.Offbox.LastWarning}}</p>{{end}}
{{if and .OffboxConfigured (ne .Offbox.EscrowState "escrowed")}}
<div class="card" style="border-left:3px solid var(--warn);margin:.75rem 0;padding:.75rem 1rem">
<p class="form-hint" style="color:var(--warn);margin:0 0 .5rem">A NAS-mentés a kulcs letétbe helyezésére vár — a mentés addig nem fut (így nem keletkezik visszaállíthatatlan másolat). Futtasd a letéti szertartást, majd erősítsd meg.</p>
<p class="form-hint" style="color:var(--warn);margin:0 0 .5rem">A távoli mentés a kulcs letétbe helyezésére vár — a mentés addig nem fut (így nem keletkezik visszaállíthatatlan másolat). Futtasd a letéti szertartást, majd erősítsd meg.</p>
<form method="POST" action="/backup/offbox/confirm-escrow" style="display:inline">{{.CSRFField}}
<button type="submit" class="btn btn-sm">Letét megerősítése</button>
</form>
@@ -165,10 +165,10 @@
{{if .OffboxConfigured}}
<div class="schedule-actions" style="margin-top:1rem">
<form method="POST" action="/backup/offbox/run" style="display:inline">{{.CSRFField}}
<button type="submit" class="btn btn-sm btn-primary">NAS-mentés most</button>
<button type="submit" class="btn btn-sm btn-primary">Távoli mentés most</button>
</form>
</div>
<h4 style="margin-top:1.25rem">Mely alkalmazások mentődnek a NAS-ra?</h4>
<h4 style="margin-top:1.25rem">Mely alkalmazások mentődnek a távoli tárolóra?</h4>
{{if .OffboxApps}}
<div class="storage-paths-list">
{{range .OffboxApps}}
@@ -179,7 +179,7 @@
<form method="POST" action="/backup/offbox/toggle" style="display:inline">{{$.CSRFField}}
<input type="hidden" name="app" value="{{.Name}}">
<input type="hidden" name="enabled" value="{{if .Enabled}}false{{else}}true{{end}}">
<button type="submit" class="btn btn-xs {{if .Enabled}}btn-outline{{else}}btn-primary{{end}}">{{if .Enabled}}NAS-mentés kikapcsolása{{else}}NAS-mentés bekapcsolása{{end}}</button>
<button type="submit" class="btn btn-xs {{if .Enabled}}btn-outline{{else}}btn-primary{{end}}">{{if .Enabled}}Távoli mentés kikapcsolása{{else}}Távoli mentés bekapcsolása{{end}}</button>
</form>
{{if .Enabled}}
<form method="POST" action="/backup/offbox/restore" style="display:inline">{{$.CSRFField}}
@@ -195,21 +195,21 @@
{{else}}<p class="form-hint">Nincs telepített alkalmazás.</p>{{end}}
{{end}}
{{else}}
<p class="form-hint">Még nincs beállítva külső NAS mentési cél.</p>
<p class="form-hint">Még nincs beállítva távoli mentési cél.</p>
{{end}}
<details style="margin-top:1rem">
<summary class="btn btn-xs btn-primary" style="cursor:pointer;display:inline-block">NAS mentési cél beállítása</summary>
<summary class="btn btn-xs btn-primary" style="cursor:pointer;display:inline-block">Távoli mentési cél beállítása</summary>
<form method="POST" action="/backup/offbox/config" style="margin-top:.75rem;padding:1rem;border:1px solid var(--border,#ddd);border-radius:6px;max-width:560px">{{.CSRFField}}
<div class="form-row"><label>NAS címe (IP vagy hosztnév)</label><input type="text" name="host" class="form-input" value="{{if .Offbox}}{{.Offbox.Host}}{{end}}" placeholder="pl. 192.168.0.10"></div>
<div class="form-row"><label>Cél címe (IP vagy hosztnév — NAS vagy SFTP-kiszolgáló)</label><input type="text" name="host" class="form-input" value="{{if .Offbox}}{{.Offbox.Host}}{{end}}" placeholder="pl. 192.168.0.10"></div>
<div class="form-row"><label>SSH port</label><input type="number" name="port" class="form-input" value="{{if .Offbox}}{{.Offbox.Port}}{{else}}22{{end}}"></div>
<div class="form-row"><label>Felhasználó</label><input type="text" name="user" class="form-input" value="{{if .Offbox}}{{.Offbox.User}}{{end}}" placeholder="pl. felhom"></div>
<div class="form-row"><label>Tároló útvonala a NAS-on</label><input type="text" name="repo_path" class="form-input" value="{{if .Offbox}}{{.Offbox.RepoPath}}{{end}}" placeholder="pl. /volume1/felhom-backup/repo"></div>
<div class="form-row"><label><input type="checkbox" name="enabled" {{if .Offbox}}{{if .Offbox.Enabled}}checked{{end}}{{else}}checked{{end}}> Napi automatikus NAS-mentés engedélyezve</label></div>
<div class="form-row"><label>Tároló útvonala a célgépen</label><input type="text" name="repo_path" class="form-input" value="{{if .Offbox}}{{.Offbox.RepoPath}}{{end}}" placeholder="pl. /volume1/felhom-backup/repo"></div>
<div class="form-row"><label><input type="checkbox" name="enabled" {{if .Offbox}}{{if .Offbox.Enabled}}checked{{end}}{{else}}checked{{end}}> Napi automatikus távoli mentés engedélyezve</label></div>
<div class="form-row"><label>SSH privát kulcs {{if .OffboxConfigured}}(üresen hagyva változatlan){{end}}</label>
<textarea name="ssh_key" class="form-input" rows="4" autocomplete="off" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea>
<span class="form-hint">A kulcsot 0600-as fájlba írjuk; sosem naplózzuk és nem tároljuk a beállításokban.</span></div>
<div class="form-row"><label>NAS ismert-host sora (known_hosts) {{if .OffboxConfigured}}(üresen hagyva változatlan){{end}}</label>
<div class="form-row"><label>A célgép ismert-host sora (known_hosts) {{if .OffboxConfigured}}(üresen hagyva változatlan){{end}}</label>
<textarea name="known_hosts" class="form-input" rows="2" autocomplete="off" placeholder="nas.local ssh-ed25519 AAAA…"></textarea>
<span class="form-hint">A host-kulcs rögzítése (no blind TOFU). Lekérdezhető: <span class="mono">ssh-keyscan -p &lt;port&gt; &lt;host&gt;</span></span></div>
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
@@ -593,7 +593,7 @@
if (!banner) return;
var sawRunning = false;
function opLabel(op){ return op === 'tier2-restore' ? 'Fájl-visszaállítás'
: op === 'offbox-restore' ? 'NAS-visszaállítás' : 'Visszaállítás'; }
: op === 'offbox-restore' ? 'Távoli visszaállítás' : 'Visszaállítás'; }
function render(st){
if (st.running) {
sawRunning = true;