v0.15.1: Backup page Részletek overhaul with per-drive tier sections

Replace Tároló section with collapsible Részletek containing 3 tiers:
- Tier 1: per-drive restic repo stats with storage labels
- Tier 2: cross-drive items grouped by destination, split by method
- Tier 3: remote backup placeholder
Restore UI now shows tier + drive labels in snapshot dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 08:23:33 +01:00
parent 0c0cacbe7c
commit 2befa6877b
7 changed files with 415 additions and 99 deletions
+172 -72
View File
@@ -373,82 +373,143 @@
{{end}}
</div>
<!-- Section 6: Repository -->
<!-- Section 6: Részletek (Details) -->
<div class="repo-card">
<h3>Tároló</h3>
<h3>Részletek</h3>
<!-- Tier 1: Local restic backup (per-drive) -->
<div class="repo-tier">
<h4 class="repo-tier-title">1. mentés — Helyi mentés (restic)</h4>
<div class="repo-info-rows">
<!-- Tier 1: Helyi mentés (collapsible, open by default) -->
<div class="details-tier">
<div class="details-tier-header" onclick="toggleTier(this)">
<span class="expand-icon"></span>
<h4 class="repo-tier-title">1. szint — Helyi mentés (restic)</h4>
</div>
<div class="details-tier-body">
{{if .PerDriveRepoStats}}
{{range .PerDriveRepoStats}}
<div class="drive-detail-card">
<div class="drive-detail-header">{{.DriveLabel}}</div>
<div class="repo-info-rows">
<div class="repo-info-row">
<span class="repo-label">Méret:</span>
<span class="repo-value">{{if .TotalSize}}{{.TotalSize}}{{else}}—{{end}}</span>
</div>
<div class="repo-info-row">
<span class="repo-label">Pillanatképek:</span>
<span class="repo-value">{{.SnapshotCount}}</span>
</div>
</div>
</div>
{{end}}
{{if gt (len .PerDriveRepoStats) 1}}
<div class="repo-info-rows" style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid var(--border-color)">
<div class="repo-info-row">
<span class="repo-label">Összesen:</span>
<span class="repo-value">{{if .Backup.RepoStats}}{{.Backup.RepoStats.TotalSize}} · {{.Backup.RepoStats.SnapshotCount}} pillanatkép{{end}}</span>
</div>
</div>
{{end}}
{{else}}
{{if .Backup.RepoStats}}
<div class="repo-info-row">
<span class="repo-label">Méret:</span>
<span class="repo-value">{{.Backup.RepoStats.TotalSize}}</span>
</div>
<div class="repo-info-row">
<span class="repo-label">Pillanatképek:</span>
<span class="repo-value">{{.Backup.RepoStats.SnapshotCount}}</span>
<div class="repo-info-rows">
<div class="repo-info-row">
<span class="repo-label">Méret:</span>
<span class="repo-value">{{.Backup.RepoStats.TotalSize}}</span>
</div>
<div class="repo-info-row">
<span class="repo-label">Pillanatképek:</span>
<span class="repo-value">{{.Backup.RepoStats.SnapshotCount}}</span>
</div>
</div>
{{end}}
<div class="repo-info-row">
<span class="repo-label">Adatbázis mentések:</span>
<span class="repo-value">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} dump fájl{{if gt .DBDumpTotalBytes 0}} — {{fmtBytes .DBDumpTotalBytes}}{{end}}{{else}}Nincs dump fájl{{end}}</span>
{{end}}
<div class="repo-info-rows" style="margin-top:0.5rem">
<div class="repo-info-row">
<span class="repo-label">Adatbázis mentések:</span>
<span class="repo-value">{{if .Backup.DumpFiles}}{{len .Backup.DumpFiles}} dump fájl{{if gt .DBDumpTotalBytes 0}} — {{fmtBytes .DBDumpTotalBytes}}{{end}}{{else}}Nincs dump fájl{{end}}</span>
</div>
<div class="repo-info-row">
<span class="repo-label">Integritás:</span>
<span class="repo-value">
{{if .Backup.LastCheckTime.IsZero}}
<span class="relative-time">Még nem ellenőrzött</span>
{{else if .Backup.LastCheckOK}}
<span class="backup-status-ok">Rendben</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
{{else}}
<span class="backup-status-fail">Hiba</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
{{end}}
</span>
</div>
</div>
<div class="repo-info-row">
<span class="repo-label">Integritás:</span>
<span class="repo-value">
{{if .Backup.LastCheckTime.IsZero}}
<span class="relative-time">Még nem ellenőrzött</span>
{{else if .Backup.LastCheckOK}}
<span class="backup-status-ok">Rendben</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
{{else}}
<span class="backup-status-fail">Hiba</span> <span class="relative-time">({{fmtTime .Backup.LastCheckTime}})</span>
<!-- Encryption key -->
{{if $.ResticPassword}}
<div class="repo-encryption">
<span class="repo-label">Titkosítási kulcs:</span>
<div class="repo-encryption-row">
<input type="password" id="restic-pw" class="restic-pw-field mono" value="{{$.ResticPassword}}" readonly>
<button type="button" class="btn btn-sm" onclick="toggleResticPw()">Megjelenítés</button>
<button type="button" class="btn btn-sm" onclick="copyResticPw()">Másolás</button>
</div>
<div class="repo-encryption-warn">
Mentse el biztonságos helyre! A kulcs nélkül a biztonsági mentések NEM állíthatók vissza.
</div>
</div>
{{end}}
</div>
</div>
<!-- Tier 2: Másodlagos másolat (collapsible, collapsed by default) -->
<div class="details-tier">
<div class="details-tier-header" onclick="toggleTier(this)">
<span class="expand-icon"></span>
<h4 class="repo-tier-title">2. szint — Másodlagos másolat</h4>
</div>
<div class="details-tier-body" style="display:none">
{{if .Tier2DriveGroups}}
{{range .Tier2DriveGroups}}
<div class="drive-detail-card">
<div class="drive-detail-header">{{.DestLabel}} <span class="relative-time mono">({{.DestPath}})</span></div>
{{if .ResticItems}}
<div class="method-group">
<div class="method-group-label">Restic:</div>
{{range .ResticItems}}
<div class="repo-info-row">
<span class="repo-label">{{.DisplayName}}</span>
<span class="repo-value">{{if .SizeHuman}}{{.SizeHuman}}{{else}}—{{end}}</span>
</div>
{{end}}
</span>
</div>
</div>
<!-- Encryption key -->
{{if $.ResticPassword}}
<div class="repo-encryption">
<span class="repo-label">Titkosítási kulcs:</span>
<div class="repo-encryption-row">
<input type="password" id="restic-pw" class="restic-pw-field mono" value="{{$.ResticPassword}}" readonly>
<button type="button" class="btn btn-sm" onclick="toggleResticPw()">Megjelenítés</button>
<button type="button" class="btn btn-sm" onclick="copyResticPw()">Másolás</button>
</div>
<div class="repo-encryption-warn">
Mentse el biztonságos helyre! A kulcs nélkül a biztonsági mentések NEM állíthatók vissza.
</div>
</div>
{{end}}
</div>
<!-- Tier 2: Cross-drive backup destinations -->
{{if .Tier2Dests}}
<div class="repo-tier">
<h4 class="repo-tier-title">2. mentés — Másodlagos másolat</h4>
{{range .Tier2Dests}}
<div class="repo-info-rows">
<div class="repo-info-row">
<span class="repo-label">Cél:</span>
<span class="repo-value mono">{{index . "Path"}}{{if index . "Label"}} <span class="relative-time">({{index . "Label"}})</span>{{end}}</span>
</div>
<div class="repo-info-row">
<span class="repo-label">Módszer:</span>
<span class="repo-value">{{index . "Method"}}</span>
</div>
{{if index . "SizeHuman"}}
<div class="repo-info-row">
<span class="repo-label">Méret:</span>
<span class="repo-value">{{index . "SizeHuman"}}</span>
</div>
{{end}}
{{if .RsyncItems}}
<div class="method-group">
<div class="method-group-label">Rsync:</div>
{{range .RsyncItems}}
<div class="repo-info-row">
<span class="repo-label">{{.DisplayName}}</span>
<span class="repo-value">{{if .SizeHuman}}{{.SizeHuman}}{{else}}—{{end}}</span>
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
{{else}}
<div class="tier-empty-state">Nincs 2. szintű mentés konfigurálva.</div>
{{end}}
</div>
</div>
<!-- Tier 3: Távoli mentés (collapsible, collapsed by default, placeholder) -->
<div class="details-tier">
<div class="details-tier-header" onclick="toggleTier(this)">
<span class="expand-icon"></span>
<h4 class="repo-tier-title" style="opacity:.6">3. szint — Távoli mentés (offsite)</h4>
</div>
<div class="details-tier-body" style="display:none">
<div class="tier-empty-state">B2 / S3 / SFTP — hamarosan elérhető</div>
</div>
{{end}}
</div>
{{end}}
</div>
<!-- Section 7: Restore -->
@@ -508,6 +569,18 @@ function toggleBackupDetail(header) {
}
}
function toggleTier(header) {
var body = header.nextElementSibling;
var icon = header.querySelector('.expand-icon');
if (body.style.display === 'none') {
body.style.display = 'block';
icon.textContent = '▼';
} else {
body.style.display = 'none';
icon.textContent = '▶';
}
}
function triggerCrossDriveBackup(stackName, btn) {
btn.disabled = true;
btn.textContent = 'Fut...';
@@ -607,9 +680,16 @@ var huDays = ['vasárnap', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek
function formatSnapshot(s) {
var t = new Date(s.time);
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return t.getFullYear() + '-' + pad(t.getMonth()+1) + '-' + pad(t.getDate()) +
var label = t.getFullYear() + '-' + pad(t.getMonth()+1) + '-' + pad(t.getDate()) +
' ' + huDays[t.getDay()] + ' ' + pad(t.getHours()) + ':' + pad(t.getMinutes()) +
' (' + s.short_id + ')';
var tierLabel = s.tier === 2 ? '2. szint' : '1. szint';
if (s.drive_label) {
label += ' — ' + tierLabel + ', ' + s.drive_label;
} else {
label += ' — ' + tierLabel;
}
return label;
}
function onRestoreAppChange() {
@@ -653,12 +733,32 @@ function onRestoreAppChange() {
.then(function(data) {
snapSel.innerHTML = '<option value="">— Válasszon —</option>';
if (data.ok && data.data && data.data.length > 0) {
data.data.forEach(function(s) {
var o = document.createElement('option');
o.value = s.short_id;
o.textContent = formatSnapshot(s);
snapSel.appendChild(o);
});
// Group by tier
var tier1 = data.data.filter(function(s) { return s.tier !== 2; });
var tier2 = data.data.filter(function(s) { return s.tier === 2; });
if (tier1.length > 0) {
var grp1 = document.createElement('optgroup');
grp1.label = '1. szint — Helyi mentés (ajánlott)';
tier1.forEach(function(s) {
var o = document.createElement('option');
o.value = s.short_id;
o.textContent = formatSnapshot(s);
grp1.appendChild(o);
});
snapSel.appendChild(grp1);
}
if (tier2.length > 0) {
var grp2 = document.createElement('optgroup');
grp2.label = '2. szint — Másodlagos másolat';
tier2.forEach(function(s) {
var o = document.createElement('option');
o.value = s.short_id;
o.textContent = formatSnapshot(s);
grp2.appendChild(o);
});
snapSel.appendChild(grp2);
}
} else {
snapSel.innerHTML = '<option value="">— Nincs elérhető mentés —</option>';
noSnaps.style.display = 'block';
@@ -1600,6 +1600,65 @@ a.stat-card:hover {
gap: .15rem;
}
/* Details tier (collapsible sections in Részletek) */
.details-tier {
border-top: 1px solid var(--border-color);
margin-top: .5rem;
}
.details-tier:first-child {
border-top: none;
margin-top: 0;
}
.details-tier-header {
display: flex;
align-items: center;
gap: .5rem;
cursor: pointer;
padding: .5rem 0;
user-select: none;
}
.details-tier-header:hover {
opacity: .8;
}
.details-tier-header .expand-icon {
font-size: .7rem;
color: var(--text-muted);
min-width: 1rem;
text-align: center;
}
.details-tier-header .repo-tier-title {
margin-bottom: 0;
}
.details-tier-body {
padding-bottom: .75rem;
}
.drive-detail-card {
background: var(--bg-secondary);
border-radius: var(--radius);
padding: .75rem;
margin-bottom: .5rem;
}
.drive-detail-header {
font-size: .85rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: .5rem;
}
.method-group {
margin-top: .5rem;
}
.method-group-label {
font-size: .8rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: .25rem;
}
.tier-empty-state {
font-size: .85rem;
color: var(--text-muted);
padding: .5rem 0;
}
.relative-time {
color: var(--text-muted);
font-size: .8rem;