c929948f27
- Add Docker named volume backup to Tier 1 (dump to tar, include in restic) and Tier 2 (copy tars to rsync mirror _volumes/ dir) - Fix volume name resolution: use project-prefixed names (mealie_mealie_data) - Fix double Tier 1 in restore dropdown: filter snapshots by app's home drive - Add Tier 2 restore: RestoreAppFromTier2() restores from rsync mirror - Show Tier 2 entry in restore dropdown when cross-drive backup succeeded - Add .fab import link in restore section - Volume-aware restore type banners and backup content labels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
840 lines
37 KiB
HTML
840 lines
37 KiB
HTML
{{define "backups"}}
|
||
{{template "layout_start" .}}
|
||
|
||
<div class="page-header">
|
||
<h2>Biztonsági mentés</h2>
|
||
<span class="domain-badge">{{.Domain}}</span>
|
||
</div>
|
||
|
||
{{if .Backup}}{{if .Backup.FlashSuccess}}
|
||
<div class="flash flash-success">{{.Backup.FlashSuccess}}</div>
|
||
{{end}}{{end}}
|
||
{{if .Backup}}{{if .Backup.FlashError}}
|
||
<div class="flash flash-error">{{.Backup.FlashError}}</div>
|
||
{{end}}{{end}}
|
||
|
||
{{if not .Backup}}
|
||
<div class="backup-empty-state">
|
||
<div class="backup-empty-icon">🛡</div>
|
||
<h3>Biztonsági mentés nincs beállítva</h3>
|
||
<p>A biztonsági mentés funkció nem aktív.<br>
|
||
Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.</p>
|
||
</div>
|
||
{{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">
|
||
<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="system-bar">
|
||
<div class="system-bar-fill {{usageColor .DiskPercent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .DiskPercent}}%"></div>
|
||
</div>
|
||
</div>
|
||
{{range $.StorageBars}}
|
||
{{if .Disconnected}}
|
||
<div class="storage-item storage-disconnected">
|
||
<div class="storage-header">
|
||
<span class="storage-label">{{.Label}}</span>
|
||
<span class="storage-value badge-error" style="font-size:.75rem">Leválasztva</span>
|
||
</div>
|
||
<div class="system-bar"><div class="system-bar-disconnected"></div></div>
|
||
</div>
|
||
{{else}}
|
||
<div class="storage-item">
|
||
<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="system-bar">
|
||
<div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|
||
{{end}}
|
||
</div>
|
||
<div class="storage-stats">
|
||
{{if .Backup.RepoStats}}
|
||
<div class="storage-stat-row">
|
||
<span class="storage-stat-label">Mentési tároló</span>
|
||
<span class="storage-stat-value mono">{{.Backup.RepoStats.TotalSize}}</span>
|
||
</div>
|
||
{{end}}
|
||
<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 class="storage-stat-row">
|
||
<span class="storage-stat-label">Pillanatképek</span>
|
||
<span class="storage-stat-value mono">{{if .Backup.RepoStats}}{{.Backup.RepoStats.SnapshotCount}}{{else}}0{{end}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 1: Status overview cards -->
|
||
<div class="stats-grid backup-page-cards">
|
||
{{if .Backup.LastBackup}}
|
||
{{if .Backup.LastBackup.Success}}
|
||
<div class="stat-card stat-running">
|
||
<div class="stat-value">✓</div>
|
||
<div class="stat-label">Helyi mentés aktív</div>
|
||
</div>
|
||
{{else}}
|
||
<div class="stat-card stat-stopped">
|
||
<div class="stat-value">✗</div>
|
||
<div class="stat-label">Helyi mentés sikertelen</div>
|
||
</div>
|
||
{{end}}
|
||
{{else}}
|
||
<div class="stat-card">
|
||
<div class="stat-value">–</div>
|
||
<div class="stat-label">Helyi mentés</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="stat-card" style="border-left-color: var(--gray);">
|
||
<div class="stat-value" style="background:var(--gray);-webkit-background-clip:text;background-clip:text;">–</div>
|
||
<div class="stat-label">Távoli mentés<br><span class="relative-time">nincs beállítva</span></div>
|
||
</div>
|
||
|
||
<div class="stat-card stat-total">
|
||
<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>
|
||
</div>
|
||
|
||
<div class="stat-card stat-total">
|
||
<div class="stat-value">
|
||
{{if .Backup.RepoStats}}{{.Backup.RepoStats.TotalSize}}{{else}}–{{end}}
|
||
</div>
|
||
<div class="stat-label">Tároló méret
|
||
{{if .Backup.RepoStats}}<br><span class="relative-time">{{.Backup.RepoStats.SnapshotCount}} pillanatkép</span>{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 2: Schedule -->
|
||
<div class="schedule-card">
|
||
<h3>Ütemezés</h3>
|
||
<div class="schedule-rows">
|
||
<div class="schedule-row">
|
||
<span class="schedule-task">Adatbázis mentés</span>
|
||
<span class="schedule-time">{{.Backup.DBDumpSchedule}}</span>
|
||
<span class="schedule-next">Következő: {{nextRunLabel .Backup.NextDBDump}}</span>
|
||
</div>
|
||
<div class="schedule-row">
|
||
<span class="schedule-task">Restic pillanatkép</span>
|
||
<span class="schedule-time">{{.Backup.ResticSchedule}}</span>
|
||
<span class="schedule-next">Következő: {{nextRunLabel .Backup.NextBackup}}</span>
|
||
</div>
|
||
<div class="schedule-row">
|
||
<span class="schedule-task">Karbantartás</span>
|
||
<span class="schedule-time">{{pruneLabel .Backup.PruneSchedule}}</span>
|
||
<span class="schedule-next">Következő: {{nextPruneLabel .Backup.PruneSchedule}}</span>
|
||
</div>
|
||
</div>
|
||
<div class="schedule-summary">
|
||
{{if .Backup.LastBackup}}
|
||
<div class="schedule-summary-row">
|
||
<span>Utolsó sikeres mentés:</span>
|
||
<span class="schedule-summary-value">{{fmtTime .Backup.LastBackup.LastRun}} ({{timeAgo .Backup.LastBackup.LastRun}})</span>
|
||
</div>
|
||
<div class="schedule-summary-row">
|
||
<span>Mentés időtartam:</span>
|
||
<span class="schedule-summary-value">{{fmtDuration .Backup.LastBackup.Duration}}</span>
|
||
</div>
|
||
{{else}}
|
||
<div class="schedule-summary-row">
|
||
<span>Utolsó sikeres mentés:</span>
|
||
<span class="schedule-summary-value relative-time">Még nem futott</span>
|
||
</div>
|
||
{{end}}
|
||
<div class="schedule-summary-row">
|
||
<span>Megőrzés:</span>
|
||
<span class="schedule-summary-value">{{.Backup.Retention.KeepDaily}} napi · {{.Backup.Retention.KeepWeekly}} heti · {{.Backup.Retention.KeepMonthly}} havi</span>
|
||
</div>
|
||
</div>
|
||
<div class="schedule-actions">
|
||
<button class="btn btn-sm btn-primary" onclick="triggerBackupFromPage()" id="backup-page-btn"
|
||
{{if .Backup.Running}}disabled{{end}}>
|
||
{{if .Backup.Running}}Mentés folyamatban...{{else}}Mentés most{{end}}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 3: Databases -->
|
||
<div class="backup-section-card">
|
||
<h3>Adatbázisok</h3>
|
||
{{if or .Backup.DumpFiles .Backup.DiscoveredDBs}}
|
||
<div class="backup-table-wrap">
|
||
<table class="db-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Alkalmazás</th>
|
||
<th>Típus</th>
|
||
<th>Méret</th>
|
||
<th>Utolsó</th>
|
||
<th>Érvényesítés</th>
|
||
<th>Állapot</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{if .Backup.LastDBDump}}
|
||
{{range .Backup.LastDBDump.Results}}
|
||
<tr>
|
||
<td>{{.DB.StackName}}</td>
|
||
<td><span class="db-type-badge db-type-{{.DB.DBType}}">{{dbTypeLabel .DB.DBType}}</span></td>
|
||
<td class="mono">{{if .Error}}–{{else}}{{fmtBytes .Size}}{{end}}</td>
|
||
<td class="mono">{{if .Error}}–{{else}}{{fmtTimeShort $.Backup.LastDBDump.LastRun}}{{end}}</td>
|
||
<td>
|
||
{{if .Error}}
|
||
<span class="validation-badge validation-na">–</span>
|
||
{{else if .Validation.Valid}}
|
||
<span class="validation-badge validation-ok">{{.Validation.TableCount}} tábla</span>
|
||
{{else if .Validation.Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">Hiba</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-na" title="Az érvényesítés nem futott le">–</span>
|
||
{{end}}
|
||
</td>
|
||
<td>
|
||
{{if .Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Error}}">Hiba</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-ok">OK</span>
|
||
{{end}}
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
{{else}}
|
||
{{range .Backup.DumpFiles}}
|
||
<tr>
|
||
<td>{{.StackName}}</td>
|
||
<td><span class="db-type-badge db-type-{{.DBType}}">{{dbTypeLabel .DBType}}</span></td>
|
||
<td class="mono">{{fmtBytes .Size}}</td>
|
||
<td class="mono">{{fmtTimeShort .ModTime}}</td>
|
||
<td>
|
||
{{if .Validation.Valid}}
|
||
<span class="validation-badge validation-ok">{{.Validation.TableCount}} tábla</span>
|
||
{{else if .Validation.Error}}
|
||
<span class="validation-badge validation-fail" title="{{.Validation.Error}}">Hiba</span>
|
||
{{else}}
|
||
<span class="validation-badge validation-na">–</span>
|
||
{{end}}
|
||
</td>
|
||
<td><span class="validation-badge validation-ok">OK</span></td>
|
||
</tr>
|
||
{{end}}
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{{else}}
|
||
<div class="backup-table-empty">Nem található adatbázis mentés.</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Section 4: Unified per-app backup status -->
|
||
{{if .Backup.AppDataInfo}}
|
||
<div class="backup-section-card">
|
||
<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 .DriveDisconnected}}
|
||
<span class="badge badge-error" style="font-size:.7rem">Meghajtó leválasztva</span>
|
||
{{else 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 if .HasVolumeData}}
|
||
<span class="meta-badge">Konfig{{if .HasDB}} + DB{{end}} + Adatok</span>
|
||
{{else}}
|
||
<span class="meta-badge">Konfig{{if .HasDB}} + DB{{end}}</span>
|
||
{{end}}
|
||
</div>
|
||
<span class="expand-icon">▶</span>
|
||
</div>
|
||
<div class="app-backup-row-detail" style="display:none">
|
||
<div class="backup-layers">
|
||
<!-- Tier 1: Nightly backup (mandatory, same drive) -->
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">1. mentés</span>
|
||
<span class="layer-badge">Auto</span>
|
||
<span class="tier-location">helyi</span>
|
||
{{if .Tier1LastRun}}
|
||
<span class="layer-last">Utolsó: {{.Tier1LastRun}}
|
||
{{if eq .Tier1LastStatus "ok"}}<span class="text-ok">✓</span>
|
||
{{else if eq .Tier1LastStatus "error"}}<span class="text-error">✗</span>{{end}}
|
||
</span>
|
||
{{end}}
|
||
<span class="tier-contents">{{.BackupContents}}</span>
|
||
{{if and .HasDB (eq .Tier1DBStatus "error")}}
|
||
<span class="text-error" style="font-size:.8rem">⚠ DB dump hiba</span>
|
||
{{end}}
|
||
</div>
|
||
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
|
||
<div class="backup-layer-row">
|
||
<span class="tier-label">2. mentés</span>
|
||
{{if and .Tier2Configured .Tier2DestDisconnected}}
|
||
<span class="layer-method" style="opacity:.6">rsync</span>
|
||
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
|
||
<span class="badge badge-warn" style="font-size:.7rem">Cél meghajtó leválasztva</span>
|
||
{{if .Tier2LastRun}}
|
||
<span class="layer-last" style="opacity:.6">Utolsó: {{.Tier2LastRun}}</span>
|
||
{{end}}
|
||
<span class="tier-contents" style="opacity:.6">{{.BackupContents}}</span>
|
||
<div class="layer-actions">
|
||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
|
||
</div>
|
||
{{else if and .Tier2Configured .Tier2DestInactive}}
|
||
<span class="layer-method" style="opacity:.6">rsync</span>
|
||
<span class="layer-dest" style="opacity:.6">→ {{.Tier2Dest}}</span>
|
||
<span class="badge badge-warn" style="font-size:.7rem">Cél meghajtó inaktív</span>
|
||
{{if .Tier2LastRun}}
|
||
<span class="layer-last" style="opacity:.6">Utolsó: {{.Tier2LastRun}}</span>
|
||
{{end}}
|
||
<span class="tier-contents" style="opacity:.6">{{.BackupContents}}</span>
|
||
<div class="layer-actions">
|
||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
|
||
</div>
|
||
{{else if .Tier2Configured}}
|
||
<span class="layer-method">rsync</span>
|
||
<span class="layer-dest">→ {{.Tier2Dest}}</span>
|
||
<span class="layer-schedule">{{.Tier2Schedule}}</span>
|
||
{{if .Tier2LastRun}}
|
||
<span class="layer-last">Utolsó: {{.Tier2LastRun}}
|
||
<span class="{{if eq .Tier2LastStatus "ok"}}text-ok{{else if eq .Tier2LastStatus "error"}}text-error{{else if eq .Tier2LastStatus "running"}}text-muted{{end}}">
|
||
{{.Tier2StatusBadge}}
|
||
</span>
|
||
</span>
|
||
{{end}}
|
||
{{if .Tier2SizeHuman}}<span class="tier-size">{{.Tier2SizeHuman}}</span>{{end}}
|
||
<span class="tier-contents">{{.BackupContents}}</span>
|
||
<span class="tier-browsable" title="A mentés böngészhető fájlrendszerben">📁</span>
|
||
<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-auto-ok">✓ 1. mentés auto</span>
|
||
<span class="layer-unconfigured">⚠ Nincs 2. másolat</span>
|
||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
|
||
{{end}}
|
||
</div>
|
||
<!-- Tier 3: Remote backup (future) -->
|
||
<div class="backup-layer-row" style="opacity:.5">
|
||
<span class="tier-label">3. mentés</span>
|
||
<span class="layer-badge" style="background:var(--bg-tertiary);color:var(--text-muted)">Hamarosan</span>
|
||
<span class="tier-location">távoli (offsite)</span>
|
||
<span class="tier-contents" style="font-style:normal;color:var(--text-muted)">B2 / S3 / SFTP — hamarosan elérhető</span>
|
||
</div>
|
||
</div>
|
||
{{if .Warnings}}
|
||
<div class="layer-warnings">
|
||
{{range .Warnings}}
|
||
<div class="backup-layer-warning">{{.}}</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{if .Backup.CrossDriveSummary}}
|
||
<div class="cross-drive-actions" style="margin-top:1rem">
|
||
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes 2. mentés futtatása most</button>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<!-- Section 5: Snapshots -->
|
||
<div class="backup-section-card">
|
||
<h3>Pillanatképek</h3>
|
||
{{if .Backup.SnapshotHistory}}
|
||
<div class="backup-table-wrap">
|
||
<table class="snapshot-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Azonosító</th>
|
||
<th>Időpont</th>
|
||
<th>Hozzáadott <span class="col-subtitle">(új adat)</span></th>
|
||
<th>Új fájl</th>
|
||
<th>Változott</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Backup.SnapshotHistory}}
|
||
<tr>
|
||
<td class="mono">{{shortID .SnapshotID}}</td>
|
||
<td class="mono">{{fmtTime .Time}}</td>
|
||
<td class="mono">{{if .HasStats}}+{{.DataAdded}}{{else}}0{{end}}</td>
|
||
<td class="mono">{{if .HasStats}}{{.FilesNew}}{{else}}0{{end}}</td>
|
||
<td class="mono">{{if .HasStats}}{{.FilesChanged}}{{else}}0{{end}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="snapshot-footer">
|
||
Összesen: {{len .Backup.SnapshotHistory}} pillanatkép
|
||
{{if .Backup.RepoStats}} · {{.Backup.RepoStats.TotalSize}}{{end}}
|
||
</div>
|
||
{{else}}
|
||
<div class="backup-table-empty">Még nincs pillanatkép.</div>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- Section 6: Részletek (Details) -->
|
||
<div class="repo-card">
|
||
<h3>Részletek</h3>
|
||
|
||
<!-- 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-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}}
|
||
{{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>
|
||
|
||
<!-- 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>
|
||
{{range .Items}}
|
||
<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}}
|
||
{{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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section 7: Restore -->
|
||
{{if .Backup.AppDataInfo}}
|
||
<div class="backup-section-card">
|
||
<h3>Visszaállítás</h3>
|
||
<div class="restore-section">
|
||
<div class="restore-form-row">
|
||
<label class="restore-label">Alkalmazás:</label>
|
||
<select id="restore-app" class="restore-select" onchange="onRestoreAppChange()">
|
||
<option value="">— Válasszon —</option>
|
||
{{range .Backup.AppDataInfo}}
|
||
<option value="{{.StackName}}" data-has-hdd="{{.HasHDDData}}" data-has-db="{{.HasDBDump}}" data-has-volumes="{{.HasVolumeData}}">{{.DisplayName}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="restore-form-row">
|
||
<label class="restore-label">Pillanatkép:</label>
|
||
<select id="restore-snapshot" class="restore-select" onchange="onRestoreConfirmChange()">
|
||
<option value="">— Válasszon alkalmazást —</option>
|
||
</select>
|
||
</div>
|
||
<div id="restore-type-info" class="restore-info" style="display:none;margin-bottom:0.5rem">
|
||
</div>
|
||
<div id="restore-no-snapshots" class="restore-warning" style="display:none;">
|
||
Még nincs mentés felhasználói adattal.
|
||
</div>
|
||
<div class="restore-warning">
|
||
⚠ A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
|
||
Az alkalmazás a folyamat során automatikusan leáll és újraindul.
|
||
</div>
|
||
<div class="restore-confirm">
|
||
<label>
|
||
<input type="checkbox" id="restore-confirm-cb" onchange="onRestoreConfirmChange()">
|
||
Megértettem, visszaállítás indítása.
|
||
</label>
|
||
</div>
|
||
<div class="restore-actions">
|
||
<button type="button" class="btn btn-sm btn-danger" id="restore-btn" disabled onclick="submitRestore()">Visszaállítás indítása</button>
|
||
</div>
|
||
<div style="margin-top: 1rem; text-align: center; border-top: 1px solid var(--border); padding-top: 1rem;">
|
||
<a href="/import" class="btn btn-sm btn-outline">Importálás mentett csomagból (.fab)</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{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 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...';
|
||
fetch('/api/stacks/' + stackName + '/cross-backup/run', {method: 'POST', headers: csrfHeaders()})
|
||
.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...';
|
||
fetch('/api/backup/cross-drive/run-all', {method: 'POST', headers: csrfHeaders()})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (!d.ok) {
|
||
alert('Hiba: ' + (d.error || 'Ismeretlen hiba'));
|
||
btn.disabled = false;
|
||
btn.textContent = 'Összes futtatása most';
|
||
return;
|
||
}
|
||
btn.textContent = 'Mentések futnak...';
|
||
setTimeout(function() { location.reload(); }, 5000);
|
||
})
|
||
.catch(function(e) {
|
||
alert('Hálózati hiba: ' + e.message);
|
||
btn.disabled = false;
|
||
btn.textContent = 'Összes futtatása most';
|
||
});
|
||
}
|
||
|
||
function triggerBackupFromPage() {
|
||
const btn = document.getElementById('backup-page-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Mentés indítása...';
|
||
fetch('/api/backup/run', { method: 'POST', headers: csrfHeaders() })
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.ok) {
|
||
btn.textContent = 'Mentés folyamatban...';
|
||
btn.classList.add('loading');
|
||
startBackupPolling();
|
||
} else {
|
||
btn.textContent = data.error || 'Hiba';
|
||
setTimeout(() => { btn.textContent = 'Mentés most'; btn.disabled = false; }, 3000);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
btn.textContent = 'Hiba';
|
||
setTimeout(() => { btn.textContent = 'Mentés most'; btn.disabled = false; }, 3000);
|
||
});
|
||
}
|
||
|
||
function startBackupPolling() {
|
||
const poll = setInterval(() => {
|
||
fetch('/api/backup/status')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.ok && data.data && !data.data.running) {
|
||
clearInterval(poll);
|
||
window.location.reload();
|
||
}
|
||
})
|
||
.catch(() => {});
|
||
}, 3000);
|
||
}
|
||
|
||
// Restic password toggle/copy
|
||
function toggleResticPw() {
|
||
var el = document.getElementById('restic-pw');
|
||
el.type = el.type === 'password' ? 'text' : 'password';
|
||
}
|
||
function copyResticPw() {
|
||
var el = document.getElementById('restic-pw');
|
||
navigator.clipboard.writeText(el.value).then(function() {
|
||
var btn = event.target;
|
||
btn.textContent = 'Másolva!';
|
||
setTimeout(function() { btn.textContent = 'Másolás'; }, 2000);
|
||
});
|
||
}
|
||
|
||
// Restore section
|
||
var huDays = ['vasárnap', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', 'szombat'];
|
||
function formatSnapshot(s) {
|
||
var t = new Date(s.time);
|
||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||
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() {
|
||
var sel = document.getElementById('restore-app');
|
||
var appName = sel.value;
|
||
var snapSel = document.getElementById('restore-snapshot');
|
||
var noSnaps = document.getElementById('restore-no-snapshots');
|
||
var typeInfo = document.getElementById('restore-type-info');
|
||
|
||
document.getElementById('restore-confirm-cb').checked = false;
|
||
document.getElementById('restore-btn').disabled = true;
|
||
noSnaps.style.display = 'none';
|
||
typeInfo.style.display = 'none';
|
||
|
||
if (!appName) {
|
||
snapSel.innerHTML = '<option value="">— Válasszon alkalmazást —</option>';
|
||
return;
|
||
}
|
||
|
||
// Determine restore type from data attributes
|
||
var opt = sel.options[sel.selectedIndex];
|
||
var hasHDD = opt.getAttribute('data-has-hdd') === 'true';
|
||
var hasDB = opt.getAttribute('data-has-db') === 'true';
|
||
var hasVolumes = opt.getAttribute('data-has-volumes') === 'true';
|
||
|
||
if (hasHDD || hasVolumes) {
|
||
typeInfo.innerHTML = '🔄 Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.';
|
||
typeInfo.className = 'restore-info';
|
||
} else if (hasDB) {
|
||
typeInfo.innerHTML = 'ℹ Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.';
|
||
typeInfo.className = 'restore-info restore-info-partial';
|
||
} else {
|
||
typeInfo.innerHTML = 'ℹ Csak konfiguráció visszaállítása (compose fájlok, beállítások).';
|
||
typeInfo.className = 'restore-info restore-info-partial';
|
||
}
|
||
typeInfo.style.display = 'block';
|
||
|
||
snapSel.innerHTML = '<option value="">— Betöltés... —</option>';
|
||
|
||
fetch('/api/backup/snapshots?stack=' + encodeURIComponent(appName))
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
snapSel.innerHTML = '<option value="">— Válasszon —</option>';
|
||
if (data.ok && data.data && data.data.length > 0) {
|
||
// 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';
|
||
}
|
||
});
|
||
}
|
||
|
||
function onRestoreConfirmChange() {
|
||
var cb = document.getElementById('restore-confirm-cb');
|
||
var app = document.getElementById('restore-app').value;
|
||
var snap = document.getElementById('restore-snapshot').value;
|
||
document.getElementById('restore-btn').disabled = !(cb.checked && app && snap);
|
||
}
|
||
|
||
function submitRestore() {
|
||
var app = document.getElementById('restore-app').value;
|
||
var snap = document.getElementById('restore-snapshot').value;
|
||
if (!app || !snap) return;
|
||
|
||
var btn = document.getElementById('restore-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Visszaállítás folyamatban...';
|
||
|
||
var form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = '/backup/restore';
|
||
|
||
var fc = document.createElement('input');
|
||
fc.type = 'hidden'; fc.name = '_csrf';
|
||
fc.value = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
|
||
form.appendChild(fc);
|
||
|
||
var f1 = document.createElement('input');
|
||
f1.type = 'hidden'; f1.name = 'stack_name'; f1.value = app;
|
||
form.appendChild(f1);
|
||
|
||
var f2 = document.createElement('input');
|
||
f2.type = 'hidden'; f2.name = 'snapshot_id'; f2.value = snap;
|
||
form.appendChild(f2);
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
|
||
// Auto-poll if backup is already running on page load
|
||
{{if .Backup}}{{if .Backup.Running}}
|
||
startBackupPolling();
|
||
{{end}}{{end}}
|
||
|
||
|
||
</script>
|
||
|
||
{{template "layout_end" .}}
|
||
{{end}}
|