controller v0.47.0: backups page — whole-guest backup visibility + manual trigger
Part 2 of the USB/backup spec. agentapi: StatusResponse.Backup record, DueResponse
age_seconds, RestoreTestStatus(). New "Rendszermentés (teljes mentés)" section
(read-only: last backup/target PBS-vs-local/next-due/restore-test) + "Mentés most"
manual trigger that goes through the quiesce loop (controller owns quiescing):
quiesce.Loop gains mutex + TriggerNow() (single-flight, async). New
/api/guest-backup/{trigger,status} (distinct from apiRouter's /api/backup/*).
App-data rows relabeled under an "Alkalmazás-mentések" divider. Config → slice 10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,56 @@
|
||||
</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}}✓{{else}}✗{{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}}✓{{else}}✗{{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">⚠ 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}}
|
||||
|
||||
<h3 class="backup-tier-divider">Alkalmazás-mentések (adatbázis + konfiguráció)</h3>
|
||||
<p class="form-hint" style="margin:-0.25rem 0 1rem">Az egyes alkalmazások részletes, granulált mentése — adatbázis-kiírások, beállítások és alkalmazás-fájlok. A fenti teljes mentéstől függetlenül, alkalmazásonként visszaállítható.</p>
|
||||
|
||||
<!-- Section 1: Status overview cards -->
|
||||
<div class="stats-grid backup-page-cards">
|
||||
{{if .Backup.LastDBDump}}
|
||||
@@ -507,6 +557,43 @@ function startBackupPolling() {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (!confirm('Elindítja a teljes rendszermentést most? A mentés ideje alatt az alkalmazások rövid időre leállhatnak.')) return;
|
||||
var btn = document.getElementById('wg-backup-btn');
|
||||
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);
|
||||
}
|
||||
|
||||
// Restic password toggle/copy
|
||||
function toggleResticPw() {
|
||||
var el = document.getElementById('restic-pw');
|
||||
|
||||
@@ -3125,6 +3125,11 @@ a.stat-card:hover {
|
||||
.badge-lock { background: rgba(210, 153, 34, 0.18); color: var(--yellow); }
|
||||
.badge-muted { background: rgba(110, 118, 129, 0.18); color: var(--text-muted); }
|
||||
.badge-info { background: rgba(0, 136, 204, 0.18); color: var(--accent-light); }
|
||||
/* Backup-page tier divider between the whole-guest section and the per-app section. */
|
||||
.backup-tier-divider {
|
||||
margin: 2rem 0 0.25rem; padding-top: 1.25rem;
|
||||
border-top: 1px solid var(--border-color); font-size: 1.05rem;
|
||||
}
|
||||
/* Per-card storage purpose description + the tiering one-liner above the drive list. */
|
||||
.drive-purpose { font-size: .8rem; color: var(--text-secondary); line-height: 1.4; }
|
||||
.drive-tiering-note {
|
||||
|
||||
Reference in New Issue
Block a user