Files
felhom-controller/controller/internal/web/templates/backups_remote.html
T
admin b5d78d1e0f v0.147.0 — feedback slice 1: pressing a button says something
The systemic complaint, twice in one evening: you press a button and nothing
happens. No progress, no ETA, no named result. Three worst offenders, fixed on
the two patterns already here (deploy 3-step panel, storage-init status poll).
No new framework — that is a ROADMAP item; three targeted cards ship tonight.

4a — a verification restore names its result. The flash said the app had been
restored "to a verification folder on the drive"; which folder, on which drive,
was invisible, so the customer could not go and look at what they had just asked
for. Full path now. The restore page gained a listing of existing verification
copies (app, size, date, path) — nothing anywhere showed these, so they piled up
and the only way to find them was SSH — each with a double-confirmed delete.

That delete is the only one this release adds, so it names a STACK, never a
path: the Manager resolves the name inside a backups/offsite-restore root it
computed itself and refuses anything landing outside. Red-proofed — neutralise
the name guard and stack:"" resolves to the offsite-restore ROOT and takes every
copy with it. Refusals are asserted as non-effects.

4b — Megosztás enable shows what it is waiting for. Enabling ran ReconcileSamba
synchronously inside the POST handler; on a golden without felhom-samba baked
that is compose pulling ~100MB, i.e. minutes of an apparently-hung form post
followed by "Beállítás mentve." whether or not anything came up. Detached +
polled now, distinguishing "képfájl letöltése" from "indítás" — decided BEFORE
the work starts, since afterwards the image is always present. Success is
probed, not inferred (compose up -d exits 0 on a crash-loop). The password form
starts the same job: with UserSet false reconcile deploys nothing, so on a fresh
box that is where the pull actually happens.

4c — "Távoli mentés most" streams real progress. restic was already reporting
bytes and percent; the runner seam used CombinedOutput() and discarded them. The
manual run now passes --json and scans stdout line-by-line: total bytes, percent,
current app. Manual only — the nightly stays silent, pinned by a test that fails
if it ever passes --json. The poll now arms unconditionally, closing a race the
manual trigger always ran: the redirect rendered before the goroutine wrote
LastStatus=running, so the poll never armed and the page sat static during the
very run just started. Red-proofed twice.

Also closes the golden/controller infra-image drift at the source: infra.Images()
derives from the existing pins and --print-infra-images exposes it, so the golden
bake can stop carrying its own copy. That copy had already drifted — felhom-samba
was never added, so the golden baked 3 of 4, which is why enabling Megosztás
pulled at runtime in the first place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 09:30:30 +02:00

241 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "backups_remote"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Biztonsági mentés — Távoli mentés</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{template "backups_flash" .}}
{{if not .Backup}}
{{template "backups_empty" .}}
{{else}}
<!-- Felhom-offsite status card (v0.124.0, decision 2): three honest states from LOCAL data only —
display + opt-in pointers, NEVER a state change (no form, no button in this card). State 3
(applied + apps toggled) is the existing status block below; the card renders nothing then. -->
{{if not .OffboxConfigured}}
<div class="alert alert-info" style="margin-bottom:1rem" id="felhom-offsite-card">Felhom offsite tárhely — igényelhető szolgáltatás. Titkosított, házon kívüli másolat a mentéseidről. Érdeklődj az üzemeltetőnél.</div>
{{else if eq .OffboxToggledCount 0}}
<div class="alert alert-info" style="margin-bottom:1rem" id="felhom-offsite-card">Aktív — nincs kijelölt alkalmazás</div>
{{end}}
<!-- Off-box (NAS) backup — Part B: encrypted restic repo over SFTP (the off-site 3-2-1 leg). -->
<h3 id="offbox-section" class="backup-tier-divider">Távoli mentés (3. mentés) — titkosított, offsite</h3>
<p class="form-hint" style="margin:-0.25rem 0 1rem">Az alkalmazás-mentések titkosított másolata egy távoli tárolóra — saját NAS vagy Felhom offsite tárhely — restic + SFTP kapcsolaton. A tároló csak titkosított adatot lát. Ez a 3-2-1 szabály „1 off-site" lába — független a helyi másodpéldánytól és a teljes rendszermentéstől.</p>
<div class="card" style="margin-bottom:1.5rem">
{{if .Offbox}}
<div class="stats-grid backup-page-cards">
<div class="stat-card {{if eq .Offbox.LastStatus "error"}}stat-warn{{end}}">
<div class="stat-value" id="offbox-status-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ó 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>
<div class="stat-label">Tároló méret · {{.Offbox.SnapshotCount}} pillanatkép</div>
</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 távoli mentés ki van kapcsolva{{end}}</div>
</div>
</div>
{{if and .Offbox.Enabled (gt .Offbox.QuotaGB 0)}}
<!-- SLICE 4: soft-quota usage bar (shared model; quota_gb from the hub descriptor). -->
<div id="offbox-quota-bar" style="max-width:560px;margin:.5rem 0">
<div class="stat-label" style="margin-bottom:.25rem">Tárhelykeret: {{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}0{{end}} / {{.Offbox.QuotaGB}} GB ({{.OffboxQuotaPct}}%)</div>
<div style="background:var(--border,#334);border-radius:4px;height:8px;overflow:hidden">
<div style="height:8px;border-radius:4px;width:{{.OffboxQuotaPct}}%;background:{{if ge .OffboxQuotaPct 100}}var(--crit,#e5484d){{else if ge .OffboxQuotaPct 80}}var(--warn,#f5a524){{else}}var(--ok,#30a46c){{end}}"></div>
</div>
</div>
{{end}}
{{if .Offbox.LastError}}<p class="form-hint" style="color:var(--crit)">Utolsó hiba: {{.Offbox.LastError}}</p>{{end}}
{{/* Part E: display pick — a stale zero-toggle warning is replaced once the selection
changed (neutral color: the replacement is reassurance, not a deviation). */}}
{{if .OffboxWarningDisplay}}<p class="form-hint"{{if eq .OffboxWarningDisplay .Offbox.LastWarning}} style="color:var(--warn)"{{end}}>{{.OffboxWarningDisplay}}</p>{{end}}
{{/* v0.142.0 offsite-repo continuity: the ORPHANED card replaces the raw restic error banner. The
remote holds backups written under a previous, no-longer-available key (reinstall shape). A
reset moves the old history aside (never deletes) and starts a fresh repo under the current key. */}}
{{if eq .Offbox.RepoState "orphaned"}}
<div class="card" style="border-left:3px solid var(--crit,#e5484d);margin:.75rem 0;padding:.75rem 1rem" id="offbox-orphan-card">
<p style="margin:0 0 .35rem;font-weight:600">A távoli tároló másik kulccsal készült mentéseket tartalmaz</p>
<p class="form-hint" style="margin:0 0 .5rem">A távoli tárhelyen lévő mentések egy korábbi, már nem elérhető kulccsal készültek (jellemzően újratelepítés után). Emiatt új mentés jelenleg nem írható a tárolóba. A meglévő mentések nem sérültek — a hozzájuk tartozó helyreállítási kóddal később visszaállíthatók lehetnek.</p>
<button type="button" class="btn btn-sm btn-outline" id="orphan-reveal" onclick="var c=document.getElementById('orphan-confirm');c.style.display='block';this.style.display='none'">Új távoli mentés indítása…</button>
<div id="orphan-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A régi előzmény <strong>félretéve marad</strong> (nem törlődik), és a hozzá tartozó helyreállítási kóddal később visszaállítható lehet. Egy üres, új tároló jön létre a mostani kulccsal, és a következő mentés ide készül.</p>
<form method="POST" action="/backup/offbox/reset" style="display:inline">{{.CSRFField}}
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Megerősítés — új távoli mentés indítása</button>
</form>
</div>
</div>
{{end}}
{{/* Escrow ceremony card (v0.127.0): the customer-driveable wizard replaced the manual-confirm
button (that deprecated endpoint stays for legacy blobs; its button is gone). States:
awaiting (v0.138.0: ceremony done, hub confirm pending) → info; awaiting timed out → warn +
re-ceremony CTA; pending → CTA; escrowed+stale → warning + re-ceremony CTA; escrowed clean →
secondary link; agent too old → the honest version note, no CTA. */}}
{{if .OffboxCeremonyAwaiting}}
{{/* v0.138.0: the report-cycle gap between a completed ceremony and the hub-verified flip.
Info (blue) accent — NOT a warning: nothing is wrong, the confirmation is simply in flight. */}}
<div class="card" style="border-left:3px solid var(--blue);margin:.75rem 0;padding:.75rem 1rem">
<p style="margin:0 0 .35rem"><strong>Helyreállítási kód létrehozva</strong></p>
<p class="form-hint" style="margin:0">A helyreállítási csomag elküldve a központba, megerősítésre vár — ez általában néhány másodperc, legfeljebb 15 perc.</p>
</div>
{{else if .OffboxCeremonyTimedOut}}
{{/* v0.138.0: the confirmation never arrived within two report cycles + slack — degrade to a
warning with the re-ceremony CTA, never an indefinite "waiting". */}}
<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 megerősítés nem érkezett meg a várt időn belül. Ellenőrizze az internetkapcsolatot, majd készítsen új helyreállítási kódot.</p>
{{if .EscrowAgentOK}}
<a href="/backup/escrow" class="btn btn-sm btn-primary">Új helyreállítási kód készítése</a>
{{else}}
<p class="form-hint" style="margin:0">A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</p>
{{end}}
</div>
{{else if and .OffboxConfigured (ne .Offbox.EscrowState "escrowed")}}
<div class="card" style="border-left:3px solid var(--warn);margin:.75rem 0;padding:.75rem 1rem">
<p style="margin:0 0 .35rem"><strong>Helyreállítási kód szükséges</strong></p>
<p class="form-hint" style="margin:0 0 .5rem">A távoli mentések csak akkor állíthatók vissza egy teljes meghibásodás után, ha létrehozza a helyreállítási kódot.</p>
{{if .EscrowAgentOK}}
<a href="/backup/escrow" class="btn btn-sm btn-primary">Helyreállítási kód létrehozása</a>
{{else}}
<p class="form-hint" style="margin:0">A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</p>
{{end}}
</div>
{{else if and .OffboxConfigured .EscrowStale}}
<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 letétben lévő helyreállítási csomag nem fedi a jelenlegi távoli mentési jelszót. Hozzon létre új helyreállítási kódot.</p>
{{if .EscrowAgentOK}}
<a href="/backup/escrow" class="btn btn-sm btn-primary">Új helyreállítási kód készítése</a>
{{else}}
<p class="form-hint" style="margin:0">A funkcióhoz az ügynök frissítése szükséges — a frissítés automatikusan megérkezik.</p>
{{end}}
</div>
{{else if .OffboxConfigured}}
{{/* v0.127.1: the re-ceremony CTA is a real secondary button (outline, NOT primary — escrowed
is a healthy state; available-not-urgent, unlike the stale variant above). */}}
<div style="margin:.5rem 0">
<p class="form-hint" style="margin:0 0 .4rem">A helyreállítási kód letétbe helyezve.</p>
{{if .EscrowAgentOK}}<a href="/backup/escrow" class="btn btn-sm btn-outline">Új helyreállítási kód készítése</a>{{end}}
</div>
{{end}}
{{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">Távoli mentés most</button>
</form>
</div>
<!-- v0.147.0 (4c): live progress for a MANUAL run — total bytes, percent and which app is being
pushed, parsed from restic's --json status stream. Hidden until a run reports progress; the
nightly run never populates it. -->
<div id="offbox-progress" class="alert alert-info" style="display:none;margin-top:.75rem">
<span id="offbox-progress-text"></span>
<div class="progress-bar-task" style="margin-top:.5rem">
<div id="offbox-progress-bar" class="progress-fill" style="width:0%"></div>
</div>
</div>
<h4 style="margin-top:1.25rem">Mely alkalmazások mentődnek a távoli tárolóra?</h4>
{{if and .OffboxApps (eq .Offbox.EscrowState "escrowed") (eq .OffboxToggledCount 0)}}
<p class="form-hint">Nincs távoli mentésre jelölt alkalmazás — jelölj ki legalább egyet.</p>
{{end}}
{{if .OffboxApps}}
<div class="app-row-list">
{{range .OffboxApps}}
{{template "app_list_row" dict "Slug" .Slug "Name" .DisplayName}}
<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}}Távoli mentés kikapcsolása{{else}}Távoli mentés bekapcsolása{{end}}</button>
</form>
{{if $.OffboxBlockedSet}}{{if index $.OffboxBlockedSet .Name}}
<span class="form-hint" style="display:block;margin-top:.25rem">A teljes mentés túllépné a tárhelykeretet — csak a konfiguráció és az adatbázis kerül mentésre.</span>
{{end}}{{end}}
{{template "app_list_row_end"}}
{{end}}
</div>
{{else}}<p class="form-hint">Nincs telepített alkalmazás.</p>{{end}}
{{end}}
{{else}}
<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">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>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 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>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>
</form>
</details>
</div>
{{end}}
<script>
/* Part C (v0.142.0) + 4c (v0.147.0): poll the run status, render live progress for a manual run, and
reload once it reaches a terminal state so the customer sees Rendben/Hiba + fresh numbers WITHOUT a
manual reload.
v0.147.0 changed WHEN polling starts. It used to begin only if the page already rendered "Fut…",
which loses a race the manual trigger always runs: the POST redirects and this page renders before
the detached goroutine has written LastStatus=running, so the poll never armed and the customer
watched a static page during the very run they had just started. Now it always arms and gives up
after GRACE quiet ticks if nothing is (or ever was) in flight. */
(function(){
var v = document.getElementById('offbox-status-value');
var box = document.getElementById('offbox-progress');
var text = document.getElementById('offbox-progress-text');
var bar = document.getElementById('offbox-progress-bar');
var GRACE = 5; // ~15s of quiet before concluding nothing is running
var quiet = 0;
var sawRunning = (v && v.textContent.indexOf('Fut') >= 0);
var timer = setInterval(function(){
fetch('/backup/offbox/status',{headers:{'Accept':'application/json'}})
.then(function(r){return r.json();})
.then(function(d){
if(!d) return;
var p = d.progress || {};
var running = (d.status === 'running') || p.active;
if(running){
sawRunning = true; quiet = 0;
if(p.active && box){
/* Only claim a percentage once restic has told us a total — before the scan finishes,
percent is 0 of 0, and a bar pinned at 0% reads as "stuck" rather than "measuring". */
var label = p.current_app ? ('Mentés: ' + p.current_app) : 'Mentés folyamatban';
if(p.total_bytes > 0){
label += ' — ' + Math.round(p.percent) + '% (' + p.done_human + ' / ' + p.total_human + ')';
bar.style.width = Math.max(0, Math.min(100, p.percent)) + '%';
} else {
label += ' — a mentendő adatok felmérése…';
bar.style.width = '0%';
}
text.textContent = label;
box.style.display = '';
}
return;
}
/* Not running. Terminal only if we ever saw it running — otherwise this is the pre-start
race (or a page nobody triggered anything from). */
if(sawRunning){ clearInterval(timer); location.reload(); return; }
if(++quiet >= GRACE){ clearInterval(timer); }
})
.catch(function(){ /* transient — keep polling */ });
}, 3000);
})();
</script>
{{template "layout_end" .}}
{{end}}