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:
@@ -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...';
|
||||
|
||||
@@ -98,26 +98,18 @@
|
||||
<h4>Biztonsági mentés</h4>
|
||||
|
||||
<div class="cross-drive-nightly">
|
||||
<div class="cross-drive-nightly-status">
|
||||
{{if .AppBackupEnabled}}
|
||||
<span class="nightly-status-indicator nightly-enabled"></span>
|
||||
{{else}}
|
||||
<span class="nightly-status-indicator nightly-disabled"></span>
|
||||
{{end}}
|
||||
<span class="toggle-label">Napi mentésbe foglalás (restic, helyi)</span>
|
||||
</div>
|
||||
<span class="form-hint" style="display:block;margin-top:.25rem">
|
||||
Az alkalmazás adatai bekerülnek az éjszakai biztonsági mentésbe.
|
||||
<a href="/backups" style="color:var(--accent-blue)">Beállítás a mentési oldalon</a>
|
||||
Az alkalmazás adatbázisa és Docker kötetei automatikusan bekerülnek az éjszakai biztonsági mentésbe.
|
||||
<a href="/backups" style="color:var(--accent-blue)">Mentési állapot →</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<hr style="border-color:var(--border);margin:1rem 0">
|
||||
|
||||
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra:</p>
|
||||
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra (felhasználói adatok):</p>
|
||||
|
||||
{{if .BackupDestWarning}}
|
||||
<div class="alert alert-warning" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
|
||||
<div class="alert {{if eq .BackupDestWarningSeverity "critical"}}alert-error{{else}}alert-warning{{end}}" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .BackupDestPaths}}
|
||||
@@ -177,18 +169,23 @@
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Ütemezés</span>
|
||||
<select name="cross_drive_schedule" id="cd-schedule" class="form-control cross-drive-field" style="max-width:20rem"
|
||||
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
|
||||
<option value="daily" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "daily")}}selected{{end}}>
|
||||
Naponta (03:30)
|
||||
</option>
|
||||
<option value="weekly" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "weekly")}}selected{{end}}>
|
||||
Hetente (vasárnap 04:30)
|
||||
</option>
|
||||
<option value="manual" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "manual")}}selected{{end}}>
|
||||
Csak kézi indítás
|
||||
</option>
|
||||
</select>
|
||||
<div>
|
||||
<select name="cross_drive_schedule" id="cd-schedule" class="form-control cross-drive-field" style="max-width:20rem"
|
||||
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}
|
||||
onchange="onScheduleChange()">
|
||||
<option value="daily" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "daily")}}selected{{end}}>
|
||||
Naponta (az éjszakai mentés után)
|
||||
</option>
|
||||
<option value="weekly" {{if or (and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "weekly")) (and .CrossDriveConfig (eq .CrossDriveConfig.Schedule "manual"))}}selected{{end}}>
|
||||
Hetente, vasárnap (az éjszakai mentés után)
|
||||
</option>
|
||||
</select>
|
||||
<div id="weekly-note" class="form-hint" style="margin-top:.5rem;display:{{if and .CrossDriveConfig (or (eq .CrossDriveConfig.Schedule "weekly") (eq .CrossDriveConfig.Schedule "manual"))}}block{{else}}none{{end}}">
|
||||
ℹ Heti mentés esetén visszaállításkor az adatbázis is a mentés napjára áll vissza
|
||||
a konzisztencia érdekében. A mentés napja és a visszaállítás között keletkezett
|
||||
adatbázis-változások elvesznek (max. 7 nap).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -382,6 +379,14 @@ function toggleCrossDriveFields() {
|
||||
}
|
||||
}
|
||||
|
||||
function onScheduleChange() {
|
||||
var sel = document.getElementById('cd-schedule');
|
||||
var note = document.getElementById('weekly-note');
|
||||
if (sel && note) {
|
||||
note.style.display = sel.value === 'weekly' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCrossDriveBackup(stackName, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Mentés folyamatban...';
|
||||
|
||||
@@ -2436,3 +2436,140 @@ a.stat-card:hover {
|
||||
background: var(--red-bg);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* ─── Unified App Backup Rows ──────────────────────────────────────── */
|
||||
.app-backup-row {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
margin-bottom: .5rem;
|
||||
background: var(--bg-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-backup-row[data-status="red"] { border-left: 3px solid var(--red); }
|
||||
.app-backup-row[data-status="yellow"] { border-left: 3px solid var(--yellow); }
|
||||
.app-backup-row[data-status="green"] { border-left: 3px solid var(--green); }
|
||||
.app-backup-row[data-status="auto"] { border-left: 3px solid var(--text-muted); }
|
||||
|
||||
.app-backup-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
padding: .65rem 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.app-backup-row-header:hover {
|
||||
background: var(--bg-hover, rgba(255,255,255,0.03));
|
||||
}
|
||||
.app-backup-row-name {
|
||||
font-weight: 500;
|
||||
font-size: .9rem;
|
||||
flex: 1;
|
||||
}
|
||||
.app-backup-row-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
.expand-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: .75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status dot */
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.status-dot.status-green { background: var(--green); box-shadow: 0 0 0 2px rgba(76,175,80,.2); }
|
||||
.status-dot.status-yellow { background: var(--yellow); box-shadow: 0 0 0 2px rgba(255,193,7,.2); }
|
||||
.status-dot.status-red { background: var(--red); box-shadow: 0 0 0 2px rgba(244,67,54,.2); }
|
||||
.status-dot.status-auto { background: var(--text-muted); opacity: .6; }
|
||||
|
||||
/* Expanded detail */
|
||||
.app-backup-row-detail {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: .75rem 1rem;
|
||||
}
|
||||
.backup-layers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .4rem;
|
||||
}
|
||||
.backup-layer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: .4rem;
|
||||
font-size: .85rem;
|
||||
padding: .2rem 0;
|
||||
}
|
||||
.backup-layer-row.layer-row-na {
|
||||
opacity: .55;
|
||||
}
|
||||
.layer-label {
|
||||
font-weight: 500;
|
||||
min-width: 14rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.layer-badge {
|
||||
background: rgba(0,136,204,.1);
|
||||
color: var(--accent-light);
|
||||
padding: .1rem .45rem;
|
||||
border-radius: 3px;
|
||||
font-size: .75rem;
|
||||
}
|
||||
.layer-na {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
font-size: .82rem;
|
||||
}
|
||||
.layer-method {
|
||||
color: var(--text-secondary);
|
||||
font-size: .82rem;
|
||||
}
|
||||
.layer-dest {
|
||||
color: var(--text-secondary);
|
||||
font-size: .82rem;
|
||||
}
|
||||
.layer-schedule {
|
||||
color: var(--text-muted);
|
||||
font-size: .8rem;
|
||||
}
|
||||
.layer-last {
|
||||
color: var(--text-muted);
|
||||
font-size: .8rem;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
.layer-unconfigured {
|
||||
color: var(--yellow);
|
||||
font-weight: 500;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
gap: .35rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
.btn-xs {
|
||||
padding: .15rem .5rem;
|
||||
font-size: .75rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.layer-warnings {
|
||||
margin-top: .5rem;
|
||||
padding-top: .5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
.backup-layer-warning {
|
||||
font-size: .82rem;
|
||||
color: var(--yellow);
|
||||
padding: .2rem 0;
|
||||
}
|
||||
.text-ok { color: var(--green); }
|
||||
.text-error { color: var(--red); }
|
||||
.text-muted { color: var(--text-muted); }
|
||||
|
||||
Reference in New Issue
Block a user