Files
felhom-controller/controller/internal/web/templates/backups_remote.html
T

291 lines
20 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 and (not .OffboxConfigured) .OffsiteHubEnabled}}
<!-- R-70 (v0.161.0): hub-managed offsite is provisioned for this box — "igényelhető" would be a
lie (it is already ordered) and the bare empty state hid the pre-apply window (DIAG-f10). -->
<div class="alert alert-info" style="margin-bottom:1rem" id="felhom-offsite-card">Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem áll be, jelezd az üzemeltetőnek.</div>
{{else 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}}
{{if .OffsiteHubEnabled}}
<p class="form-hint">Felhom offsite tárhely kiépítve — a beállítás automatikus, folyamatban. Ha egy napon belül nem áll be, jelezd az üzemeltetőnek.</p>
{{else}}
<p class="form-hint">Még nincs beállítva távoli mentési cél.</p>
{{end}}
{{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". */
/* A run is not only the per-app loop: the shares leg and the retention/prune step follow
it and can dominate the wall clock (40 of 57 seconds, measured). Name them, or the
card freezes on the last app's finished counters for that whole tail. */
/* The dump pre-phase runs BEFORE the per-app loop (R-44): it refreshes every app's
database dump so the snapshot pairs this run's files with this run's DB. On an app
with a large database it can dominate the early wall clock, and an unnamed silent
stretch at the start reads as a hang. */
if(p.phase === 'dump'){
text.textContent = 'Adatbázisok mentése a pillanatképhez…';
bar.style.width = '100%';
box.className = 'alert alert-info'; box.style.display = '';
return;
}
if(p.phase === 'retention'){
text.textContent = 'Karbantartás: régi mentések rendezése a távoli tárolón…';
bar.style.width = '100%';
box.className = 'alert alert-info'; box.style.display = '';
return;
}
if(p.phase === 'shares'){
text.textContent = 'Megosztott mappák mentése…';
bar.style.width = '100%';
box.className = 'alert alert-info'; box.style.display = '';
return;
}
var label = p.current_app ? ('Mentés: ' + p.current_app) : 'Mentés folyamatban';
if(p.bytes_done > 0){
/* Bytes are genuinely moving — the informative case. */
label += ' — ' + Math.round(p.percent) + '% (' + p.done_human + ' / ' + p.total_human + ')';
bar.style.width = Math.max(0, Math.min(100, p.percent)) + '%';
} else if(p.total_files > 0){
/* No bytes moving. restic 0.14 only counts a file into bytes_done/files_done when it
COMPLETES, so an app dominated by one big archive freezes both counters — measured
on the demo box: immich at 1 of 46 files, 0 bytes, for 42 seconds while restic
worked through a single ~430MB volume tar. A percentage cannot move in that window,
so do not pretend: name the file being processed and the elapsed time, which is
what actually tells the customer it is alive. */
var fpct = Math.round((p.files_done / p.total_files) * 100);
label += ' — ' + p.files_done + ' / ' + p.total_files + ' fájl'
+ (p.total_human ? ' (' + p.total_human + ')' : '');
if(p.current_file){
var base = p.current_file.split('/').pop();
label += ' · feldolgozás alatt: ' + base;
}
if(p.elapsed_sec > 0){ label += ' · ' + p.elapsed_sec + ' mp'; }
bar.style.width = Math.max(0, Math.min(100, fpct)) + '%';
} 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}}