Files
felhom-controller/controller/internal/web/templates/backups.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

297 lines
15 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"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>{{T "backups.biztonsagi_mentes"}}</h2>
<span class="domain-badge">{{.Domain}}</span>
</div>
{{template "backups_flash" .}}
{{if .Backup}}{{if .Backup.SingleCopyWarning}}
<div class="alert alert-warning">{{.Backup.SingleCopyWarning}}</div>
{{end}}{{end}}
{{/* R-112: the whole-system backup-target state. nil = healthy or unknown = render NOTHING. */}}
{{if .BackupTarget}}
<div class="alert alert-warning" id="backup-target-alert">{{.BackupTarget.Message}}</div>
{{if .BackupTarget.OfferPath}}
<div class="alert alert-info" id="backup-target-offer">
<p>{{.BackupTarget.OfferText}}</p>
<p class="form-hint">{{.BackupTarget.OfferLabel}}</p>
<button type="button" class="btn btn-sm" id="backup-target-assign"
data-path="{{.BackupTarget.OfferPath}}"
data-confirm="{{T "backups.kijelolod_ezt_a_meghajtot_a"}}">{{T "backups.kijelolom"}}</button>
<div id="backup-target-assign-result"></div>
</div>
{{end}}
{{end}}
{{if not .Backup}}
{{template "backups_empty" .}}
{{else}}
<!-- Section 0: Storage overview -->
<div class="backup-section-card">
<h3>{{T "backups.tarhely_attekintes"}}</h3>
<div class="storage-overview-grid">
<div class="storage-bars">
{{with $.SystemInfo}}
<div class="storage-item meter {{usageColor .DiskPercent}}">
<div class="storage-header">
<span class="storage-label">{{T "common.rendszer_gyoker"}}</span>
<span class="storage-value">{{fmtGB .DiskUsedGB}} / {{fmtGB .DiskTotalGB}} ({{printf "%.0f" .DiskPercent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
</div>
</div>
{{range $.StorageBars}}
{{if .Disconnected}}
<div class="storage-item meter meter-empty storage-disconnected">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">— <span class="state-text-neutral">{{T "common.levalasztva"}}</span></span>
</div>
<div class="meter-track"></div>
</div>
{{else}}
<div class="storage-item meter {{usageColor .Percent}}">
<div class="storage-header">
<span class="storage-label">{{.Label}}</span>
<span class="storage-value">{{fmtGB .UsedGB}} / {{fmtGB .TotalGB}} ({{printf "%.0f" .Percent}}%)</span>
</div>
<div class="meter-track">
<div class="meter-fill" style="width:{{printf "%.1f" .Percent}}%"></div>
</div>
</div>
{{end}}
{{end}}
{{end}}
</div>
<div class="storage-stats">
<div class="storage-stat-row">
<span class="storage-stat-label">{{T "backups.db_mentesek"}}</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{T "backups.fajl"}}{{else}}–{{end}}</span>
</div>
</div>
</div>
</div>
<!-- Section: Whole-guest (appliance) backup — agent-sourced, read-only + manual trigger -->
{{with .GuestBackup}}
<div class="backup-section-card">
<h3>{{T "backups.rendszermentes_teljes_mentes"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_teljes_szerver_alkalmazasok_beallitaso"}}</p>
{{if not .Available}}
<div class="alert alert-info">{{.Note}}</div>
{{else}}
<div class="stats-grid backup-page-cards">
<div class="stat-card {{if .HasBackup}}{{if .Success}}stat-running{{else}}stat-stopped{{end}}{{end}}">
<div class="stat-value">{{if .HasBackup}}{{if .Success}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">{{T "backups.utolso_teljes_mentes"}}
{{if .HasBackup}}<br><span class="relative-time">{{fmtTime .StartedAt}} ({{timeAgo .StartedAt}})</span>{{end}}
</div>
</div>
<div class="stat-card stat-total">
<div class="stat-value">{{if and .HasBackup (not .SizeUnknown)}}{{fmtBytes .SizeBytes}}{{else}}–{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}{{T "backups.meret_cel"}}{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}{{T "backups.esedekes"}}{{else}}{{T "backups.naprakesz"}}{{end}}</div>
<div class="stat-label">{{T "backups.kovetkezo_mentes"}}
{{if not .Due}}<br><span class="relative-time">{{T "backups.oraja_a_mentesi_ablakon_belul"}}</span>{{end}}
</div>
</div>
<div class="stat-card">
<div class="stat-value">{{if .HasRestoreTest}}{{if .RestorePass}}&#10003;{{else}}&#10007;{{end}}{{else}}–{{end}}</div>
<div class="stat-label">{{T "backups.visszaallitas_ellenorizve"}}
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}{{if .RestoreTier}} · {{.RestoreTier}}{{end}}</span>{{else}}<br><span class="relative-time">{{T "backups.meg_nem_futott"}}</span>{{end}}
</div>
</div>
</div>
{{if .Tiers}}
<table class="backup-tier-table" style="width:100%;margin-top:1rem;border-collapse:collapse">
{{range .Tiers}}
<tr style="border-top:1px solid var(--border)">
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>{{.Label}}</strong></td>
<td style="padding:.5rem 0;vertical-align:top">
{{if .NotSetUp}}
<span class="tag"><span class="dot"></span>{{T "backups.nincs_beallitva"}}</span>
{{else}}
{{if .HasSuccess}}
<div>{{T "backups.utolso_sikeres_mentes"}}{{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">{{T "backups.naprakesz"}}</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span>{{end}}</div>
{{else}}
<div>{{T "backups.meg_nincs_sikeres_mentes"}} <span class="tag tag-warn" style="margin-left:.4rem">{{T "backups.esedekes"}}</span></div>
{{end}}
{{if .FailedAfter}}
<div style="color:var(--red);margin-top:.25rem"{{if .NoSpaceLine}} data-no-space="true"{{end}}>{{T "backups.sikertelen"}}{{if .NoSpaceLine}} — {{.NoSpaceLine}}{{end}}</div>
{{end}}
{{end}}
</td>
</tr>
{{end}}
</table>
{{end}}
{{if .Running}}
<div class="alert alert-info" id="wg-running">{{T "backups.mentes_folyamatban_fazis"}} <span id="wg-phase">{{.Phase}}</span>)</div>
{{end}}
{{if .CanTrigger}}
<div class="schedule-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>{{T "backups.mentes_most"}}</button>
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "backups.a_mentes_alatt_az_alkalmazasok"}}</span>
<div id="wg-backup-result" style="margin-top:.6rem"></div>
</div>
{{end}}
{{end}}
</div>
{{end}}
<!-- Section: customer-configurable backup window (v0.168.0) — one setting drives every nightly leg -->
<div class="backup-section-card">
<h3>{{T "backups.mentesi_idoablak"}}</h3>
<p class="form-hint" style="margin-bottom:1rem">{{T "backups.a_mentesek_egymas_utan_futnak"}}</p>
<form method="POST" action="/backups/window" class="schedule-actions" style="display:flex;align-items:flex-end;gap:.75rem;flex-wrap:wrap">
{{.CSRFField}}
<div class="form-group" style="margin:0">
<label for="window_start">{{T "backups.mentesi_idoablak_kezdete"}}</label>
<input type="time" id="window_start" name="window_start" value="{{.BackupWindow}}" class="form-control" style="max-width:9rem" required>
</div>
<button type="submit" class="btn btn-sm btn-primary">{{T "backups.mentes"}}</button>
</form>
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
<li>{{T "backups.adatbazis_mentes"}}</li>
<li>{{T "backups.helyi_masolat"}}</li>
<li>{{T "backups.tavoli_mentes"}}</li>
<li>{{T "backups.teljes_rendszermentes_kb_kozott"}}</li>
</ul>
</div>
<!-- Section 1: Status overview cards -->
<div class="stats-grid backup-page-cards">
{{if .Backup.LastDBDump}}
{{if .Backup.LastDBDump.Success}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">{{T "backups.adatmentes_aktiv"}}</div>
</div>
{{else}}
<div class="stat-card stat-stopped">
<div class="stat-value">&#10007;</div>
<div class="stat-label">{{T "backups.adatmentes_sikertelen"}}</div>
</div>
{{end}}
{{else}}
<div class="stat-card">
<div class="stat-value">–</div>
<div class="stat-label">{{T "backups.adatmentes"}}</div>
</div>
{{end}}
{{if and .GuestBackup .GuestBackup.Offsite}}
<div class="stat-card stat-running">
<div class="stat-value">&#10003;</div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.kulon_hardveren_pbs"}}</span></div>
</div>
{{else}}
<div class="stat-card" style="border-left-color: var(--text-3);">
<div class="stat-value" style="background:var(--text-3);-webkit-background-clip:text;background-clip:text;">–</div>
<div class="stat-label">{{T "backups.tavoli_rendszermentes"}}<br><span class="relative-time">{{T "backups.nincs_beallitva"}}</span></div>
</div>
{{end}}
<div class="stat-card stat-total">
{{if eq .DBSectionState "embedded"}}
<div class="stat-value">–</div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}<br><span class="relative-time">{{T "backups.beagyazott_db_k_a_kotetmentesben"}}</span></div>
{{else}}
<div class="stat-value">
{{if .Backup.LastDBDump}}{{len .Backup.LastDBDump.Results}}{{else}}{{len .Backup.DumpFiles}}{{end}}
</div>
<div class="stat-label">{{T "backups.adatbazis_mentve"}}</div>
{{end}}
</div>
</div>
{{end}}
<script>
// Whole-guest (appliance) backup — manual trigger via the quiesce loop (controller-owned quiesce:
// stop stacks → agent vzdump → resume). Returns immediately; we poll /api/guest-backup/status.
function triggerGuestBackup() {
var btn = document.getElementById('wg-backup-btn');
felhomConfirm(btn, '{{T "backups.elinditod_a_teljes_rendszermentest_most"}}', function () {
var out = document.getElementById('wg-backup-result');
btn.disabled = true;
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_inditasa"}}</span>';
fetch('/api/guest-backup/trigger', { method: 'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()) })
.then(r => r.json())
.then(data => {
if (!data.ok) { out.innerHTML = '<div class="alert alert-error">' + (data.error || '{{T "common.hiba"}}') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">{{T "backups.mentes_folyamatban"}}</span>';
pollGuestBackup(out, btn);
})
.catch(e => { out.innerHTML = '<div class="alert alert-error">{{T "common.hiba_kettospont"}} ' + e + '</div>'; btn.disabled = false; });
});
}
function pollGuestBackup(out, btn) {
var tries = 0;
var iv = setInterval(function () {
tries++;
fetch('/api/guest-backup/status')
.then(r => r.json())
.then(data => {
if (data.ok && data.data) {
var ph = data.data.phase;
out.innerHTML = '<span class="form-hint">{{T "backups.allapot"}} ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">{{T "backups.a_rendszermentes_elkeszult"}}</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">{{T "backups.a_mentes_sikertelen"}}</div>'; btn.disabled = false; }
}
})
.catch(() => {});
if (tries > 180) { clearInterval(iv); btn.disabled = false; } // ~15 min cap at 5s polls
}, 5000);
}
// R-112 — ACCEPTING the offer. It is an OFFER: this runs only on an explicit click behind the
// standard inline confirm, and declining is simply never calling it. Nothing else on this page may
// write the role, and there is no auto-submit.
(function () {
var btn = document.getElementById('backup-target-assign');
if (!btn) { return; } // no offer in this state — nothing to wire
btn.addEventListener('click', function () {
var out = document.getElementById('backup-target-assign-result');
btn.disabled = true;
out.innerHTML = '<p class="form-hint">{{T "backups.kijeloles_folyamatban"}}</p>';
fetch('/api/storage/backup-target/assign', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, csrfHeaders()),
body: JSON.stringify({ path: btn.getAttribute('data-path') })
})
.then(function (r) { return r.json(); })
.then(function (j) {
if (!j.ok) {
out.innerHTML = '<div class="alert alert-error">{{T "backups.a_kijeloles_nem_sikerult"}} ' + (j.error || '') + '</div>';
btn.disabled = false;
return;
}
// The agent deliberately does NOT restart itself — restarting with a backup in flight
// records a spurious tier failure. So say so plainly instead of hiding it or faking it.
if (j.data && j.data.restart_required) {
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve_a_beallitas"}}</div>';
} else {
out.innerHTML = '<div class="flash flash-success">{{T "backups.a_meghajto_kijelolve"}}</div>';
}
})
.catch(function (e) {
out.innerHTML = '<div class="alert alert-error">{{T "common.hiba_kettospont"}} ' + e.message + '</div>';
btn.disabled = false;
});
});
})();
</script>
{{template "layout_end" .}}
{{end}}