feat: automatische geplante Backups mit Rotation

Phase 1.3 - Automatische Backups:
- Cron-basierter Scheduler (Standard: täglich 2 Uhr)
- Konfigurierbar über .env (Zeitplan, Verzeichnis, Anzahl)
- Automatische Rotation: behält nur letzte N Backups (Standard: 7)
- UI in Settings → Backup: Status-Anzeige und manueller Trigger
- Tests: 7 erfolgreiche Tests für Scheduler-Funktionalität

Neue Umgebungsvariablen:
- BACKUP_ENABLED (Standard: true)
- BACKUP_SCHEDULE (Standard: 0 2 * * *)
- BACKUP_DIR (Standard: ./backups)
- BACKUP_KEEP (Standard: 7)
- TZ (für Zeitzone)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-05-04 07:02:38 +02:00
parent 99a2280c02
commit 9b29d1847c
11 changed files with 484 additions and 3 deletions
+84
View File
@@ -798,6 +798,14 @@ export async function render(container, { user }) {
</div>
</div>
<div class="settings-card" id="backup-scheduler-card">
<h3 class="settings-card__title">${t('settings.backupSchedulerTitle')}</h3>
<p class="form-hint">${t('settings.backupSchedulerHint')}</p>
<div class="settings-info-grid" id="backup-scheduler-info">
<!-- Populated by JavaScript -->
</div>
</div>
<div class="settings-card">
<h3 class="settings-card__title">${t('settings.backupCliTitle')}</h3>
<p class="form-hint">${t('settings.backupCliHint')}</p>
@@ -1535,7 +1543,83 @@ function bindApiTokenEvents(container, initialTokens) {
});
}
async function loadBackupSchedulerStatus(container) {
const infoContainer = container.querySelector('#backup-scheduler-info');
if (!infoContainer) return;
try {
const res = await api.get('/backup/status');
const scheduler = res.data?.scheduler;
if (!scheduler) return;
const { enabled, schedule, keepCount, lastBackup } = scheduler;
let lastBackupText = t('settings.backupSchedulerNever');
if (lastBackup?.timestamp) {
const date = formatDate(lastBackup.timestamp) + ' ' + formatTime(lastBackup.timestamp);
lastBackupText = lastBackup.success
? t('settings.backupSchedulerLastSuccess', { date })
: t('settings.backupSchedulerLastFail', { date });
}
const html = `
<div class="settings-info-row">
<span class="settings-info-label">${t('settings.backupSchedulerStatus')}</span>
<span class="settings-info-value ${enabled ? 'settings-info-value--success' : ''}">
${enabled ? t('settings.backupSchedulerEnabled') : t('settings.backupSchedulerDisabled')}
</span>
</div>
${enabled ? `
<div class="settings-info-row">
<span class="settings-info-label">${t('settings.backupSchedulerSchedule')}</span>
<span class="settings-info-value"><code>${esc(schedule)}</code></span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">${t('settings.backupSchedulerKeep')}</span>
<span class="settings-info-value">${t('settings.backupSchedulerKeepCount', { count: keepCount })}</span>
</div>
<div class="settings-info-row">
<span class="settings-info-label">${t('settings.backupSchedulerLastBackup')}</span>
<span class="settings-info-value">${esc(lastBackupText)}</span>
</div>
<div class="settings-form-actions">
<button class="btn btn--secondary" id="backup-trigger-btn">${t('settings.backupSchedulerTrigger')}</button>
</div>
` : ''}
`;
infoContainer.replaceChildren();
infoContainer.insertAdjacentHTML('beforeend', html);
if (window.lucide) window.lucide.createIcons();
// Event-Handler für manuellen Trigger
const triggerBtn = infoContainer.querySelector('#backup-trigger-btn');
if (triggerBtn) {
triggerBtn.addEventListener('click', async () => {
triggerBtn.disabled = true;
triggerBtn.textContent = t('settings.backupSchedulerTriggering');
try {
await api.post('/backup/trigger');
window.oikos?.showToast(t('settings.backupSchedulerTriggeredToast'), 'success');
// Status neu laden
loadBackupSchedulerStatus(container);
} catch (err) {
window.oikos?.showToast(err.message ?? t('common.errorGeneric'), 'danger');
triggerBtn.disabled = false;
triggerBtn.textContent = t('settings.backupSchedulerTrigger');
}
});
}
} catch (err) {
console.error('Failed to load backup scheduler status:', err);
}
}
function bindBackupEvents(container) {
// Scheduler-Status laden und anzeigen
loadBackupSchedulerStatus(container);
const form = container.querySelector('#backup-restore-form');
const fileInput = container.querySelector('#backup-restore-file');
const selectedFile = container.querySelector('#backup-selected-file');