v0.11.8 — Per-App Cross-Drive Backup (3-2-1 rule)
New feature: backup app data to a secondary storage drive to satisfy
the "different media" requirement of the 3-2-1 backup rule.
- settings.go: CrossDriveBackup struct, AppBackupPrefs.CrossDrive field,
getter/setter methods, GetOrCreateCrossDrivePassword, preserves
cross-drive config when toggling nightly backup
- crossdrive.go (new): CrossDriveRunner with rsync and restic backends.
Validates destination (mount point, writable), prevents source/dest
overlap, per-app concurrency lock, persists last_run/status/size.
- main.go: wire CrossDriveRunner, register cross-drive-daily (03:30)
and cross-drive-weekly (04:30 Sundays) scheduler jobs
- router.go: 4 new API endpoints — save config, trigger run, get status,
run-all. Router now accepts Settings and CrossDriveRunner.
- server.go: Server struct accepts CrossDriveRunner, new web route
POST /settings/cross-backup/{name}
- handlers.go: deployHandler populates CrossDriveConfig, BackupDestPaths,
BackupDestWarning, AppBackupEnabled. settingsCrossBackupHandler saves
config. backupsHandler builds CrossDriveSummary, UnconfiguredApps,
CrossDriveWarnings for backup page.
- deploy.html: "Biztonsági mentés" card with destination/method/schedule
dropdowns, last-run status, manual trigger button, flash messages.
- backups.html: "Másolatok másik meghajtóra" section with per-app
status rows, unconfigured app warnings, "Összes futtatása most" button.
- style.css: margin-bottom fix for .deploy-stale-data, new cross-drive
card and list styles.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
</div>
|
||||
|
||||
<div class="deploy-container">
|
||||
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
|
||||
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
|
||||
<div class="deploy-info">
|
||||
<img class="deploy-logo" src="{{.LogoURL}}" alt="" onerror="this.onerror=function(){this.style.display='none'};this.src='{{.LogoPNGURL}}'">
|
||||
<div>
|
||||
@@ -90,6 +92,115 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .AlreadyDeployed}}
|
||||
{{if .StorageInfo}}
|
||||
<div class="deploy-cross-drive">
|
||||
<h4>🔒 Biztonsági mentés</h4>
|
||||
|
||||
<div class="cross-drive-nightly">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="app-backup-enabled" {{if .AppBackupEnabled}}checked{{end}} disabled>
|
||||
<span class="toggle-label">Napi mentésbe foglalás (restic, helyi)</span>
|
||||
</label>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{{if .BackupDestWarning}}
|
||||
<div class="alert alert-warning" style="margin-bottom:1rem">⚠️ {{.BackupDestWarning}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if not .BackupDestPaths}}
|
||||
<div class="alert alert-info">
|
||||
Másik adattároló szükséges a másolat készítéséhez.
|
||||
<a href="/settings" style="color:var(--accent-blue)">Csatlakoztass egy külső meghajtót a Beállítások oldalon.</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<form method="post" action="/settings/cross-backup/{{.Meta.Slug}}">
|
||||
<div class="settings-grid" style="margin-bottom:1rem">
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Engedélyezve</span>
|
||||
<label class="toggle" style="margin:0">
|
||||
<input type="checkbox" name="cross_drive_enabled" id="cross-drive-enabled"
|
||||
{{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}checked{{end}}>
|
||||
<span class="toggle-label">Igen</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Cél tárhely</span>
|
||||
<select name="cross_drive_dest" class="form-control" style="max-width:20rem">
|
||||
{{range .BackupDestPaths}}
|
||||
<option value="{{.Path}}"
|
||||
{{if and $.CrossDriveConfig (eq $.CrossDriveConfig.DestinationPath .Path)}}selected{{end}}>
|
||||
{{.Label}} ({{.Path}}){{if .IsDefault}} ★{{end}}
|
||||
{{if .FreeHuman}} — {{.FreeHuman}} szabad{{end}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Módszer</span>
|
||||
<select name="cross_drive_method" class="form-control" style="max-width:20rem">
|
||||
<option value="rsync" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "rsync")}}selected{{end}}>
|
||||
Egyszerű másolat (rsync)
|
||||
</option>
|
||||
<option value="restic" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "restic")}}selected{{end}}>
|
||||
Verziózott mentés (restic)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span class="settings-label">Ütemezés</span>
|
||||
<select name="cross_drive_schedule" class="form-control" style="max-width:20rem">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{{if .CrossDriveConfig}}
|
||||
{{if .CrossDriveConfig.LastRun}}
|
||||
<div class="form-hint" style="margin-bottom:.75rem">
|
||||
Utolsó futás: {{.CrossDriveConfig.LastRun}}
|
||||
{{if eq .CrossDriveConfig.LastStatus "ok"}}✅ Sikeres{{else if eq .CrossDriveConfig.LastStatus "error"}}❌ Hiba: {{.CrossDriveConfig.LastError}}{{else if eq .CrossDriveConfig.LastStatus "running"}}⏳ Fut...{{end}}
|
||||
{{if .CrossDriveConfig.LastDuration}} ({{.CrossDriveConfig.LastDuration}}){{end}}
|
||||
{{if .CrossDriveConfig.LastSizeHuman}} — {{.CrossDriveConfig.LastSizeHuman}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Beállítások mentése</button>
|
||||
{{if and .CrossDriveConfig .CrossDriveConfig.Enabled}}
|
||||
<button type="button" class="btn btn-sm btn-outline"
|
||||
onclick="triggerCrossDriveBackup('{{.Meta.Slug}}', this)">
|
||||
Mentés most
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
|
||||
⚠️ A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója.
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
||||
{{with .MemoryInfo}}
|
||||
{{if .Available}}
|
||||
@@ -242,6 +353,46 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function triggerCrossDriveBackup(stackName, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Mentés folyamatban...';
|
||||
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 = 'Mentés most';
|
||||
return;
|
||||
}
|
||||
btn.textContent = '⏳ Mentés folyamatban...';
|
||||
// Poll status
|
||||
var poll = setInterval(function() {
|
||||
fetch('/api/stacks/' + stackName + '/cross-backup/status')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(s) {
|
||||
if (!s.ok || !s.data) return;
|
||||
if (!s.data.running) {
|
||||
clearInterval(poll);
|
||||
var status = s.data.last_status;
|
||||
if (status === 'ok') {
|
||||
btn.textContent = '✅ Mentés kész';
|
||||
} else {
|
||||
btn.textContent = '❌ Hiba';
|
||||
alert('Hiba: ' + (s.data.last_error || 'Ismeretlen hiba'));
|
||||
}
|
||||
setTimeout(function() { location.reload(); }, 2000);
|
||||
}
|
||||
}).catch(function(){});
|
||||
}, 3000);
|
||||
})
|
||||
.catch(function(e) {
|
||||
alert('Hálózati hiba: ' + e.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Mentés most';
|
||||
});
|
||||
}
|
||||
|
||||
function checkStorageSpace(sel) {
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
var warn = document.getElementById('storage-space-warn');
|
||||
|
||||
Reference in New Issue
Block a user