v0.12.0 — Backup page overhaul: unified app rows, bug fixes, sequential chaining

Bug fixes:
- GetFullStatus() returns deep copy; CrossDriveSummary/UnconfiguredApps/CrossDriveWarnings
  are always nil in the copy so the handler builds them fresh (fixes duplicate-apps bug)
- Replace binary IsMountPoint check with tiered CheckBackupDestination() — path-not-exist,
  not-writable, system-drive (warning), disk >90-95% full; shown as warning vs critical
- Remove dead settingsAppBackupHandler / POST /settings/app-backup route (toggle wrote
  to settings.json but nothing consumed the flag)

Architecture:
- Unified per-app backup rows: new AppBackupRow struct + buildAppBackupRows() replaces
  the two old sections with expandable rows showing all 3 layers per app
- Sequential backup chaining: cross-drive runs immediately after restic (removed
  independent cross-drive-daily/cross-drive-weekly scheduler jobs)
- Deploy page: remove "Csak kézi indítás" schedule option; add weekly consistency note

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:56:28 +01:00
parent e002d712cf
commit 1de244646b
10 changed files with 637 additions and 154 deletions
+123 -64
View File
@@ -233,84 +233,109 @@
{{end}}
</div>
<!-- Section 4: App data backup status (read-only) -->
<!-- Section 4: Unified per-app backup status -->
{{if .Backup.AppDataInfo}}
<div class="backup-section-card">
<h3>Alkalmazás adatok</h3>
<p class="backup-section-desc">Az alkalmazások felhasználói adatainak mentési állapota. Beállítás az alkalmazás oldalán.</p>
<div class="app-backup-list">
{{range .Backup.AppDataInfo}}
<div class="app-backup-item">
<div class="app-backup-header">
<a href="/stacks/{{.StackName}}/deploy" class="app-backup-name-link">{{.DisplayName}}</a>
<div class="app-backup-status-row">
{{if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
{{if .BackupEnabled}}
<span class="app-backup-size mono">{{.HDDSizeHuman}}</span>
<span class="app-backup-status app-backup-active">Aktív</span>
{{else}}
<span class="app-backup-status app-backup-inactive">Inaktív</span>
<h3>Alkalmazások mentési állapota</h3>
{{if .NoUserDataBackupWarning}}
<div class="alert alert-error" style="margin-bottom:1.5rem">
<strong>Felhasználói adatokról nincs biztonsági mentés.</strong><br>
A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg csak egy példányban léteznek.
Külső meghajtó csatlakoztatásával biztonsági másolat készíthető a 3-2-1 szabály szerint.
<a href="/settings" style="color:inherit;text-decoration:underline">Meghajtó beállítása →</a>
</div>
{{end}}
{{range .AppBackupRows}}
<div class="app-backup-row" data-status="{{.Status}}">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<span class="status-dot status-{{.Status}}" title="{{.StatusText}}"></span>
<span class="app-backup-row-name">{{.DisplayName}}</span>
<div class="app-backup-row-meta">
{{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}}
<span class="meta-badge">Auto</span>
{{end}}
</div>
<span class="expand-icon"></span>
</div>
<div class="app-backup-row-detail" style="display:none">
<div class="backup-layers">
<!-- DB layer -->
<div class="backup-layer-row">
<span class="layer-label">Adatbázis mentés</span>
{{if .HasDB}}
<span class="layer-badge">Auto</span>
{{if .DBLastRun}}
<span class="layer-last">Utolsó: {{.DBLastRun}}
{{if eq .DBLastStatus "ok"}}<span class="text-ok"></span>
{{else if eq .DBLastStatus "error"}}<span class="text-error"></span>{{end}}
</span>
{{end}}
{{else}}
<span class="app-backup-status app-backup-na">N/A</span>
<span class="layer-na">— (nincs adatbázis)</span>
{{end}}
</div>
<!-- Volume layer -->
<div class="backup-layer-row">
<span class="layer-label">Docker kötetek</span>
<span class="layer-badge">Auto</span>
{{if .VolumeLastRun}}
<span class="layer-last">Utolsó: {{.VolumeLastRun}}
{{if eq .VolumeLastStatus "ok"}}<span class="text-ok"></span>
{{else if eq .VolumeLastStatus "error"}}<span class="text-error"></span>{{end}}
</span>
{{end}}
</div>
<!-- User data layer -->
<div class="backup-layer-row{{if not .HasHDDData}} layer-row-na{{end}}">
<span class="layer-label">Felhasználói adatok</span>
{{if .HasUserData}}
{{if .UserDataConfigured}}
<span class="layer-method">{{.UserDataMethod}}</span>
<span class="layer-dest">→ {{.UserDataDest}}</span>
<span class="layer-schedule">{{.UserDataSchedule}}</span>
{{if .UserDataLastRun}}
<span class="layer-last">Utolsó: {{.UserDataLastRun}}
<span class="{{if eq .UserDataLastStatus "ok"}}text-ok{{else if eq .UserDataLastStatus "error"}}text-error{{else if eq .UserDataLastStatus "running"}}text-muted{{end}}">
{{.UserDataStatusBadge}}
</span>
</span>
{{end}}
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
<button class="btn btn-xs btn-outline"
onclick="triggerCrossDriveBackup('{{.StackName}}', this)">
Futtatás most</button>
</div>
{{else}}
<span class="layer-unconfigured">⚠ Nincs beállítva</span>
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
{{end}}
{{else}}
<span class="layer-na">— (nincs HDD adat)</span>
{{end}}
</div>
</div>
{{if .Warnings}}
<div class="layer-warnings">
{{range .Warnings}}
<div class="backup-layer-warning">{{.}}</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
</div>
{{end}}
<!-- Section 4b: Cross-drive backups -->
{{if or .Backup.CrossDriveSummary .Backup.UnconfiguredApps}}
<div class="backup-section-card">
<h3>Másolatok másik meghajtóra</h3>
<p class="backup-section-desc">Alkalmazás adatok biztonsági másolata külső meghajtóra (3-2-1 szabály).</p>
{{if .Backup.CrossDriveWarnings}}
<div style="margin-bottom:1rem">
{{range .Backup.CrossDriveWarnings}}
<div class="alert alert-warning" style="margin-bottom:.5rem">{{.}}</div>
{{end}}
</div>
{{end}}
{{if .Backup.CrossDriveSummary}}
<div class="cross-drive-list" style="margin-bottom:1rem">
{{range .Backup.CrossDriveSummary}}
<div class="cross-drive-item">
<div class="cross-drive-header">
<a href="/stacks/{{.StackName}}/deploy" class="cross-drive-name">{{.DisplayName}}</a>
<div class="cross-drive-meta">
<span class="meta-badge">{{.MethodLabel}}</span>
{{if .DestLabel}}<span class="meta-badge meta-badge-storage">→ {{.DestLabel}}</span>
{{else if .DestPath}}<span class="meta-badge meta-badge-storage">→ {{.DestPath}}</span>{{end}}
{{if eq .LastStatus "ok"}}<span class="meta-badge meta-badge-ok">{{.LastRunShort}}</span>
{{else if eq .LastStatus "error"}}<span class="meta-badge meta-badge-fail">Hiba</span>
{{else if eq .LastStatus "running"}}<span class="meta-badge">Fut...</span>
{{else}}<span class="meta-badge" style="color:var(--text-muted)">{{.ScheduleLabel}}</span>{{end}}
{{if .SizeHuman}}<span class="mono" style="font-size:.8rem;color:var(--text-muted)">{{.SizeHuman}}</span>{{end}}
</div>
</div>
</div>
{{end}}
<div class="cross-drive-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes HDD mentés futtatása most</button>
</div>
{{end}}
{{if .Backup.UnconfiguredApps}}
<div style="font-size:.85rem;color:var(--yellow);margin-bottom:1rem">
{{len .Backup.UnconfiguredApps}} alkalmazáshoz nincs beállítva:
{{range .Backup.UnconfiguredApps}}
<a href="/stacks/{{.StackName}}/deploy" style="color:var(--accent-blue)">{{.DisplayName}}</a>
{{end}}
</div>
{{end}}
<div class="cross-drive-actions">
<button class="btn btn-sm btn-primary" onclick="triggerAllCrossDrive(this)">Összes futtatása most</button>
</div>
</div>
{{end}}
@@ -463,6 +488,40 @@
{{end}}
<script>
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 triggerCrossDriveBackup(stackName, btn) {
btn.disabled = true;
btn.textContent = 'Fut...';
fetch('/api/stacks/' + stackName + '/cross-backup/run', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (!d.ok) {
alert('Hiba: ' + (d.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Futtatás most';
return;
}
btn.textContent = 'Fut...';
setTimeout(function() { location.reload(); }, 5000);
})
.catch(function(e) {
alert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = 'Futtatás most';
});
}
function triggerAllCrossDrive(btn) {
btn.disabled = true;
btn.textContent = 'Indítás...';