Files
felhom-controller/controller/internal/web/templates/backups_apps.html
T
admin 6be6c53e29
gates / gates (push) Successful in 25s
v0.283.0: apps go off-site by themselves (decision 50) with a size warning; a Stop holds during a backup (R-721); page slips (R-724/R-725)
Red-proofs RP31-RP38. MinAgent 0.131.0 (unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-30 10:31:55 +02:00

435 lines
26 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_apps"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>{{T "backups_apps.biztonsagi_mentes_alkalmazasok"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{template "backups_flash" .}}
{{template "restore_banner" .}}
{{- if .HollowCopyLines}}
<div class="alert alert-error" style="margin-bottom:1.5rem">
{{range .HollowCopyLines}}<div>{{.}}</div>{{end}}
</div>
{{- end}}
{{if not .Backup}}
{{template "backups_empty" .}}
{{else}}
{{if .OffboxOfferCount}}
<div class="alert alert-info" style="margin-top:.75rem" data-offsite-offer="true">
<span>{{T "backups_offsite.offer_text"}} ({{.OffboxOfferCount}})</span>
<form method="POST" action="/backup/offbox/enable-all" style="display:inline;margin-left:.5rem">{{$.CSRFField}}<button type="submit" class="btn btn-xs btn-primary">{{T "backups_offsite.offer_all"}}</button></form>
<form method="POST" action="/backup/offbox/offer-dismiss" style="display:inline">{{$.CSRFField}}<button type="submit" class="btn btn-xs btn-outline">{{T "backups_offsite.offer_no"}}</button></form>
</div>
{{end}}<h3 class="backup-tier-divider">{{T "backups_apps.alkalmazas_mentesek_adatbazis_konfigurac"}}</h3>
<p class="form-hint" style="margin:-0.25rem 0 1rem">{{T "backups_apps.az_egyes_alkalmazasok_reszletes_granulal"}}</p>
<!-- Section 2: Schedule -->
<div class="schedule-card">
<h3>{{T "backups_apps.utemezes"}}</h3>
<div class="schedule-rows">
<div class="schedule-row">
<span class="schedule-task">{{T "backups_apps.adatbazis_mentes"}}</span>
<span class="schedule-time">{{.Backup.DBDumpSchedule}}</span>
<span class="schedule-next">{{T "backups_apps.kovetkezo"}}</span>
</div>
</div>
<div class="schedule-summary">
{{if .Backup.LastDBDump}}
<div class="schedule-summary-row">
<span>{{T "backups_apps.utolso_adatbazis_mentes"}}</span>
<span class="schedule-summary-value">{{fmtTime .Backup.LastDBDump.LastRun}} ({{timeAgo .Backup.LastDBDump.LastRun}})</span>
</div>
{{else}}
<div class="schedule-summary-row">
<span>{{T "backups_apps.utolso_adatbazis_mentes"}}</span>
<span class="schedule-summary-value relative-time">{{T "backups_apps.meg_nem_futott"}}</span>
</div>
{{end}}
</div>
<div class="schedule-actions">
<button class="btn btn-sm btn-primary" onclick="triggerBackupFromPage()" id="backup-page-btn"
{{if .Backup.Running}}disabled{{end}}>
{{if .Backup.Running}}{{T "backups_apps.mentes_folyamatban"}}{{else}}{{T "backups_apps.mentes_most"}}{{end}}
</button>
</div>
</div>
<!-- Section 3: Databases -->
<div class="backup-section-card">
<h3>{{T "backups_apps.adatbazisok"}}</h3>
{{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
<div class="backup-table-wrap">
<table class="db-table">
<thead>
<tr>
<th>{{T "backups_apps.alkalmazas"}}</th>
<th>{{T "backups_apps.tipus"}}</th>
<th>{{T "backups_apps.meret"}}</th>
<th>{{T "backups_apps.utolso"}}</th>
<th>{{T "backups_apps.ervenyesites"}}</th>
<th>{{T "backups_apps.allapot"}}</th>
</tr>
</thead>
<tbody>
{{if .Backup.LastDBDump}}
{{range .Backup.LastDBDump.Results}}
<tr>
<td>{{.DB.StackName}}</td>
<td><span class="db-type-badge db-type-{{.DB.DBType}}">{{dbTypeLabel .DB.DBType}}</span></td>
<td class="mono">{{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}</td>
<td class="mono">{{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}</td>
<td>
{{if .Error}}
<span class="validation-badge validation-na">–</span>
{{else if .Validation.Valid}}
<span class="validation-badge validation-ok">{{T "backups_apps.tabla"}}</span>
{{else if .Validation.Error}}
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">{{T "backups_apps.hiba"}}</span>
{{else}}
<span class="validation-badge validation-na" title="{{T "backups_apps.az_ervenyesites_nem_futott_le"}}">–</span>
{{end}}
</td>
<td>
{{if .Error}}
<span class="validation-badge validation-fail" title="{{.Error}}">{{T "backups_apps.hiba"}}</span>
{{else}}
<span class="validation-badge validation-ok">OK</span>
{{end}}
</td>
</tr>
{{end}}
{{else}}
{{range .Backup.DumpFiles}}
<tr>
<td>{{.StackName}}</td>
<td><span class="db-type-badge db-type-{{.DBType}}">{{dbTypeLabel .DBType}}</span></td>
<td class="mono">{{fmtBytes .Size}}</td>
<td class="mono">{{fmtTimeShort .ModTime}}</td>
<td>
{{if .Validation.Valid}}
<span class="validation-badge validation-ok">{{T "backups_apps.tabla"}}</span>
{{else if .Validation.Error}}
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">{{T "backups_apps.hiba"}}</span>
{{else}}
<span class="validation-badge validation-na">–</span>
{{end}}
</td>
<td><span class="validation-badge validation-ok">OK</span></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{else if eq .DBSectionState "pending"}}
<div class="backup-table-empty">{{T "backups_apps.meg_nem_keszult_adatbazis_mentes"}}</div>
{{else}}
<div class="backup-table-empty">{{T "backups_apps.a_telepitett_alkalmazasok_beagyazott_ada"}}</div>
{{end}}
</div>
<!-- Section 4: Unified per-app backup status -->
{{if .Backup.AppDataInfo}}
<div class="backup-section-card">
<h3>{{T "backups_apps.alkalmazasok_mentesi_allapota"}}</h3>
{{if .NoUserDataBackupWarning}}
<div class="alert alert-error" style="margin-bottom:1.5rem">
{{T "backups_apps.felhasznaloi_adatokrol_nincs_biztonsagi"}}
</div>
{{end}}
{{range .AppBackupRows}}
<div class="app-backup-row" data-status="{{.Status}}">
<!-- Aligned to the shared app-row grammar (icon + name left, status + chevron right);
the expander behavior is untouched — this header is the one allowlisted aligned
copy in scripts/app_row_dedup_gate.py, NOT a partial render (it owns the toggle).
The icon chain matches app_row.html: SVG -> PNG -> /static/app-placeholder.svg, so a
logo-less app shows the placeholder here too instead of a hidden dead-end (v0.163.1). -->
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<img class="app-row-icon" src="{{logoURL .Slug}}" alt="" data-fallback="/static/app-placeholder.svg"
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
<span class="app-backup-row-name">{{.DisplayName}}</span>
<div class="app-backup-row-meta">
{{if .Removed}}
<span class="tag tag-warn">{{T "backups_apps.eltavolitva_visszaallithato"}}</span>
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
{{else if .DriveDisconnected}}
<span class="tag tag-warn">{{T "backups_apps.meghajto_levalasztva"}}</span>
{{else if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
{{else if .HasVolumeData}}
<span class="meta-badge">{{T "backups_apps.konfig"}}{{if .HasDB}} + DB{{end}} {{T "backups_apps.adatok"}}</span>
{{else}}
<span class="meta-badge">{{T "backups_apps.konfig"}}{{if .HasDB}} + DB{{end}}</span>
{{end}}
<span class="status-dot status-{{.Status}}" title="{{.StatusText}}"></span>
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
{{if .Removed}}
{{/* R-487: a removed app's kept unit. One action — the same restore the picker on the
Visszaállítás page runs (POST /backup/restore, snapshot „helyi"); it reinstalls the app
from the unit (R-253). No tier rows: nothing is scheduled for an app that is not here. */}}
<div class="backup-layers">
<div class="backup-layer-row">
<span class="tier-label">{{T "backups_apps.mentes"}}</span>
<span class="layer-badge">{{T "backups_apps.megtartva"}}</span>
<span class="tier-location">{{T "backups_apps.helyi"}}</span>
<span class="tier-contents">{{T "backups_apps.az_alkalmazast_eltavolitottad_a_mentese"}}</span>
<div class="layer-actions">
<form method="POST" action="/backup/restore" style="display:inline">{{$.CSRFField}}
<input type="hidden" name="stack_name" value="{{.StackName}}">
<input type="hidden" name="snapshot_id" value="helyi">
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.ujratelepiti_a_z_alkalmazast_a"}}">{{T "backups_apps.visszaallitas_a_mentesbol"}}</button>
</form>
</div>
</div>
</div>
{{else}}
<div class="backup-layers">
<!-- Tier 1: Nightly backup (mandatory, same drive) -->
<div class="backup-layer-row">
<span class="tier-label">{{T "backups_apps.1_mentes"}}</span>
<span class="layer-badge">Auto</span>
<span class="tier-location">{{T "backups_apps.helyi"}}</span>
{{if .Tier1LastRun}}
{{/* R-258: three states. The tick is THIS app's own most recent dump result,
not the box's most recent run — and a third value renders NO icon, which is
where "we have no result for this backup" belongs. A tick standing for
"a file exists" is what this replaced. */}}
<span class="layer-last">{{T "backups_apps.utolso_2"}}
{{if eq .Tier1LastStatus "ok"}}<span class="text-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg></span>
{{else if eq .Tier1LastStatus "error"}}<span class="text-error"><svg class="ico ico-sm"><use href="#i-x"/></svg></span>
{{else}}<span class="state-text-neutral" title="{{T "backups_apps.errol_a_mentesrol_nincs_eredmenyunk"}}">—</span>{{end}}
</span>
{{end}}
<span class="tier-contents">{{.Tier1Contents}}</span>
{{if .DriveFilesNote}}<span class="state-text-neutral" style="font-size:.8rem">{{.DriveFilesNote}}{{if .DriveFilesNoteLink}} <a href="{{.DriveFilesNoteLink}}">{{.DriveFilesNoteLinkText}} &rarr;</a>{{end}}</span>{{end}}
{{if and .HasDB (eq .Tier1DBStatus "error")}}
<span class="text-error" style="font-size:.8rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups_apps.db_dump_hiba"}}</span>
{{end}}
</div>
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
<div class="backup-layer-row">
<span class="tier-label">{{T "backups_apps.2_mentes"}}</span>
{{if .Tier2UserDisabled}}
<span class="layer-unconfigured">{{T "backups_apps.2_mentes_kikapcsolva"}}</span>
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
{{else if and .Tier2Configured .Tier2DestDisconnected}}
<span class="layer-method" style="opacity:.6">rsync</span>
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
<span class="tag tag-warn">{{T "backups_apps.cel_meghajto_levalasztva"}}</span>
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_3"}}</span>
{{end}}{{else if .Tier2LastSuccess}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_sikeres"}}</span>
{{else}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.meg_nincs_sikeres_masolat"}}</span>
{{end}}
<span class="tier-contents" style="opacity:.6">{{.Tier23Contents}}</span>
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
{{else if and .Tier2Configured .Tier2DestInactive}}
<span class="layer-method" style="opacity:.6">rsync</span>
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
<span class="tag tag-warn">{{T "backups_apps.cel_meghajto_inaktiv"}}</span>
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_3"}}</span>
{{end}}{{else if .Tier2LastSuccess}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.utolso_sikeres"}}</span>
{{else}}
<span class="layer-last" style="opacity:.6">{{T "backups_apps.meg_nincs_sikeres_masolat"}}</span>
{{end}}
<span class="tier-contents" style="opacity:.6">{{.Tier23Contents}}</span>
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
{{else if .Tier2Configured}}
<span class="layer-method">rsync</span>
<span class="layer-dest">→ {{.Tier2Dest}}</span>
<span class="layer-schedule">{{.Tier2Schedule}}</span>
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
<span class="layer-last">{{T "backups_apps.utolso_3"}}
<span class="{{if eq .Tier2LastStatus "ok"}}text-ok{{else if eq .Tier2LastStatus "error"}}text-error{{else if eq .Tier2LastStatus "running"}}text-muted{{end}}">
{{.Tier2StatusBadge}}
</span>
</span>
{{end}}{{else if .Tier2LastSuccess}}
<span class="layer-last">{{T "backups_apps.utolso_sikeres"}}
<span class="{{if eq .Tier2LastStatus "ok"}}text-ok{{else if eq .Tier2LastStatus "error"}}text-error{{else if eq .Tier2LastStatus "running"}}text-muted{{end}}">
{{.Tier2StatusBadge}}
</span>
</span>
{{else}}
<span class="layer-last">{{T "backups_apps.meg_nincs_sikeres_masolat"}}
<span class="{{if eq .Tier2LastStatus "error"}}text-error{{else}}text-muted{{end}}">{{.Tier2StatusBadge}}</span>
</span>
{{end}}
{{if .Tier2SizeHuman}}<span class="tier-size">{{.Tier2SizeHuman}}</span>{{end}}
{{if .Tier2LastWarning}}<span class="layer-reason" style="color:var(--warn);opacity:.9">{{.Tier2LastWarning}}</span>{{end}}
{{/* R-403: a run that PRESERVED the copy's package instead of refreshing it must
not render as a plain fresh copy. The status line above is about the RUN;
this one is about the PACKAGE, and after a preserved leg they differ. */}}
{{if .Tier2UnitStaleNotice}}<span class="layer-reason" style="color:var(--warn);opacity:.9">{{.Tier2UnitStaleNotice}}</span>{{end}}
<span class="tier-contents">{{.Tier23Contents}}</span>
<span class="tier-browsable" title="{{T "backups_apps.a_mentes_bongeszheto_fajlrendszerben"}}"><svg class="ico ico-sm"><use href="#i-file-text"/></svg></span>
<div class="layer-actions">
{{if not .Tier2SuccessTracked}}{{if .Tier2LastRun}}
<form method="POST" action="/backup/tier2/restore" style="display:inline">{{$.CSRFField}}
<input type="hidden" name="stack_name" value="{{.StackName}}">
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.visszaallitja_a_hianyzo_fajlokat_a"}}">{{T "backups_apps.fajlok_visszaallitasa"}}</button>
</form>
{{end}}{{else if .Tier2LastSuccess}}
<form method="POST" action="/backup/tier2/restore" style="display:inline">{{$.CSRFField}}
<input type="hidden" name="stack_name" value="{{.StackName}}">
<button type="submit" class="btn btn-xs btn-outline" data-confirm="{{T "backups_apps.visszaallitja_legutobbi_sikeres_masolat"}}{{if .Tier2StaleCopy}}{{T "backups_apps.figyelem_legutobbi_kiserlet_nem_sikerult"}}{{end}}">{{T "backups_apps.fajlok_visszaallitasa"}}</button>
</form>
{{else}}
<span class="layer-reason" style="opacity:.85">{{T "backups_apps.meg_nincs_sikeres_masolat_amibol"}}</span>
{{end}}
{{/* R-102/R-103: the DESTRUCTIVE twin of the button above. Offered only when the
copy holds an openable recovery unit (Tier2UnitRestorable — a second
predicate, never a widening of the file restore's). It is deliberately a
SEPARATE button in a danger style: the one beside it only adds what is
missing, this one overwrites the app's live data with the copy's. */}}
{{if .Tier2UnitRestorable}}
<form method="POST" action="/backup/tier2/unit-restore" style="display:inline">{{$.CSRFField}}
<input type="hidden" name="stack_name" value="{{.StackName}}">
<button type="submit" class="btn btn-xs btn-danger-outline" data-confirm="{{.Tier2UnitConfirm}}">{{T "backups_apps.teljes_visszaallitas_a_masolatbol"}}</button>
</form>
{{end}}
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
{{else}}
<span class="layer-auto-ok"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "backups_apps.1_mentes_auto"}}</span>
<span class="layer-unconfigured"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups_apps.nincs_2_off_drive_masolat"}}</span>
{{if .Tier2LastError}}
<span class="layer-reason" style="opacity:.85" title="{{T "backups_apps.a_2_mentes_automatikus_kulon"}}">{{.Tier2LastError}}</span>
{{end}}
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/backup" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
{{end}}
</div>
<!-- Tier 3: Off-box (NAS) backup — real state (see tier3State). -->
{{if eq .Tier3State "active"}}
<div class="backup-layer-row">
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
<span class="layer-badge {{if eq $.Offbox.LastStatus "ok"}}text-ok{{else if eq $.Offbox.LastStatus "error"}}text-error{{else if eq $.Offbox.LastStatus "running"}}text-muted{{end}}">{{if eq $.Offbox.LastStatus "ok"}}{{T "backups_apps.sikeres"}}{{else if eq $.Offbox.LastStatus "error"}}{{T "backups_apps.hiba"}}{{else if eq $.Offbox.LastStatus "running"}}{{T "backups_apps.fut"}}{{else}}—{{end}}</span>
<span class="tier-location">restic → {{$.Offbox.Host}}</span>
{{if $.Offbox.LastRun}}<span class="layer-last">{{T "backups_apps.utolso_4"}}</span>{{end}}
<span class="tier-contents">{{T "backups_apps.helyreallitasi_egyseg_titkositva"}}</span>
</div>
{{else if eq .Tier3State "escrow_pending"}}
<div class="backup-layer-row">
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
<span class="layer-badge text-error">{{T "backups_apps.kulcsletetre_var"}}</span>
<span class="tier-contents">{{T "backups_apps.a_tavoli_mentes_a_titkositasi"}}</span>
</div>
{{else if eq .Tier3State "off"}}
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
<span class="layer-badge">{{T "backups_apps.kikapcsolva"}}</span>
<span class="tier-contents">{{T "backups_apps.ez_az_alkalmazas_nincs_kijelolve"}}</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">{{T "backups_apps.bekapcsolas"}}</a>
</div>
</div>
{{else}}
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">{{T "backups_apps.3_mentes"}}</span>
<span class="layer-badge" style="background:var(--bg-2);color:var(--text-3)">{{T "backups_apps.nincs_beallitva"}}</span>
<span class="tier-location">{{T "backups_apps.tavoli_offsite"}}</span>
<span class="tier-contents">{{T "backups_apps.nincs_tavoli_mentesi_cel_beallitva"}}</span>
<div class="layer-actions">
<a href="/backups/remote#offbox-section" class="btn btn-xs btn-outline">{{T "backups_apps.beallitas"}}</a>
</div>
</div>
{{end}}
</div>
{{if .Warnings}}
<div class="layer-warnings">
{{range .Warnings}}
<div class="backup-layer-warning">{{.}}</div>
{{end}}
</div>
{{end}}
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
{{end}}
<script>
{{template "restore_banner_js"}}
function toggleBackupDetail(header) {
var detail = header.nextElementSibling;
var icon = header.querySelector('.expand-icon');
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.textContent = '▼';
} else {
detail.style.display = 'none';
icon.textContent = '▶';
}
}
function triggerBackupFromPage() {
const btn = document.getElementById('backup-page-btn');
btn.disabled = true;
btn.textContent = '{{T "backups_apps.mentes_inditasa"}}';
fetch('/api/backup/run', { method: 'POST', headers: csrfHeaders() })
.then(r => r.json())
.then(data => {
if (data.ok) {
btn.textContent = '{{T "backups_apps.mentes_folyamatban"}}';
btn.classList.add('loading');
startBackupPolling();
} else {
btn.textContent = data.error || '{{T "backups_apps.hiba"}}';
setTimeout(() => { btn.textContent = '{{T "backups_apps.mentes_most"}}'; btn.disabled = false; }, 3000);
}
})
.catch(() => {
btn.textContent = '{{T "backups_apps.hiba"}}';
setTimeout(() => { btn.textContent = '{{T "backups_apps.mentes_most"}}'; btn.disabled = false; }, 3000);
});
}
function startBackupPolling() {
const poll = setInterval(() => {
fetch('/api/backup/status')
.then(r => r.json())
.then(data => {
if (data.ok && data.data && !data.data.running) {
clearInterval(poll);
window.location.reload();
}
})
.catch(() => {});
}, 3000);
}
// Auto-poll if backup is already running on page load
{{if .Backup}}{{if .Backup.Running}}
startBackupPolling();
{{end}}{{end}}
</script>
{{template "layout_end" .}}
{{end}}