Files
felhom-controller/controller/internal/web/templates/backups.html
T
admin 82c67e32e1 v0.168.0: customer-configurable backup window (Mentési időablak)
ONE setting (window start W) drives every nightly leg at fixed, never-stored
offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence
settings > controller.yaml db_dump_schedule > 02:30.

- scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job
  buffered resched chan + a select case in runDailyJob.
- new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow).
- quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a
  safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated.
  Backend.Due now also returns the backup age (from the agent's own /backup/due).
- backup page: Mentési időablak card (time input + derived leg/gate rows); POST
  /backups/window validates -> saves -> UpdateDaily x3 -> flash.

Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
2026-07-24 20:55:44 +02:00

222 lines
11 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>Biztonsági mentés</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}}
{{if not .Backup}}
{{template "backups_empty" .}}
{{else}}
<!-- Section 0: Storage overview -->
<div class="backup-section-card">
<h3>Tárhely áttekintés</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">Rendszer (/)</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">Leválasztva</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">DB mentések</span>
<span class="storage-stat-value mono">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} fájl{{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>Rendszermentés (teljes mentés)</h3>
<p class="form-hint" style="margin-bottom:1rem">A teljes szerver — alkalmazások, beállítások és adatbázisok együtt — időszakos mentése, amelyből az egész készülék visszaállítható. Ezt a host-ügynök készíti és kezeli.</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">Utolsó teljes mentés
{{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 .HasBackup}}{{fmtBytes .SizeBytes}}{{else}}{{end}}</div>
<div class="stat-label">{{if .HasBackup}}{{.Target}}{{else}}Méret / cél{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if .Due}}Esedékes{{else}}Naprakész{{end}}</div>
<div class="stat-label">Következő mentés
{{if not .Due}}<br><span class="relative-time">{{.AgeHours}} órája — a mentési ablakon belül</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">Visszaállítás ellenőrizve
{{if .HasRestoreTest}}<br><span class="relative-time">{{fmtTime .RestoreTestedAt}}</span>{{else}}<br><span class="relative-time">Még nem futott</span>{{end}}
</div>
</div>
</div>
{{if .Running}}
<div class="alert alert-info" id="wg-running">Mentés folyamatban… (fázis: <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}}>Mentés most</button>
{{if .StopMode}}<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés idejére az alkalmazások rövid időre leállnak.</span>
{{else}}<span class="form-hint" style="margin-left:.5rem">Pillanatkép-mód: az alkalmazások csak néhány másodpercre állnak le.</span>{{end}}
<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>Mentési időablak</h3>
<p class="form-hint" style="margin-bottom:1rem">A mentések egymás után futnak: adatbázis-mentés, helyi másolat, távoli mentés, majd a teljes rendszermentés.</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">Mentési időablak 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">Mentés</button>
</form>
<ul class="form-hint" style="margin-top:1rem;margin-bottom:0;list-style:none;padding:0;line-height:1.9">
<li>Adatbázis-mentés: <strong>{{.BackupLegDB}}</strong></li>
<li>Helyi másolat: <strong>{{.BackupLegTier2}}</strong></li>
<li>Távoli mentés: <strong>{{.BackupLegOffbox}}</strong></li>
<li>Teljes rendszermentés: kb. <strong>{{.BackupGateFrom}}{{.BackupGateTo}}</strong> között</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">Adatmentés aktív</div>
</div>
{{else}}
<div class="stat-card stat-stopped">
<div class="stat-value">&#10007;</div>
<div class="stat-label">Adatmentés sikertelen</div>
</div>
{{end}}
{{else}}
<div class="stat-card">
<div class="stat-value"></div>
<div class="stat-label">Adatmentés</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ávoli rendszermentés<br><span class="relative-time">külön 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ávoli rendszermentés<br><span class="relative-time">nincs beállítva</span></div>
</div>
{{end}}
<div class="stat-card stat-total">
{{if eq .DBSectionState "embedded"}}
<div class="stat-value"></div>
<div class="stat-label">Adatbázis mentve<br><span class="relative-time">beágyazott DB-k a kötetmentésben</span></div>
{{else}}
<div class="stat-value">
{{if .Backup.LastDBDump}}{{len .Backup.LastDBDump.Results}}{{else}}{{len .Backup.DumpFiles}}{{end}}
</div>
<div class="stat-label">Adatbázis 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, 'Elindítja a teljes rendszermentést most? A mentés ideje alatt az alkalmazások rövid időre leállhatnak.', function () {
var out = document.getElementById('wg-backup-result');
btn.disabled = true;
out.innerHTML = '<span class="form-hint">Mentés indítása…</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 || 'Hiba') + '</div>'; btn.disabled = false; return; }
out.innerHTML = '<span class="form-hint">Mentés folyamatban…</span>';
pollGuestBackup(out, btn);
})
.catch(e => { out.innerHTML = '<div class="alert alert-error">Hiba: ' + 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">Állapot: ' + ph + '</span>';
if (ph === 'done') { clearInterval(iv); out.innerHTML = '<div class="flash flash-success">A rendszermentés elkészült.</div>'; setTimeout(() => window.location.reload(), 1500); }
else if (ph === 'failed') { clearInterval(iv); out.innerHTML = '<div class="alert alert-error">A mentés sikertelen.</div>'; btn.disabled = false; }
}
})
.catch(() => {});
if (tries > 180) { clearInterval(iv); btn.disabled = false; } // ~15 min cap at 5s polls
}, 5000);
}
</script>
{{template "layout_end" .}}
{{end}}