Files
deskhub/DeskHub-Webseite/staff/backup.php
T
2026-10-19 18:05:57 +00:00

419 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
require_once __DIR__ . '/../includes/backup.php';
require_once __DIR__ . '/../includes/backup_restore.php';
$staff = require_admin();
$pdo = get_db();
// ── Backup-Setting Helfer ────────────────────────────────────────────────────
function bget(string $key): ?string {
global $pdo;
$s = $pdo->prepare('SELECT value FROM backup_settings WHERE key_name=?');
$s->execute([$key]);
$r = $s->fetch();
return $r ? $r['value'] : null;
}
function bset_save(string $key, ?string $value): void {
global $pdo;
$sql = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite'
? 'INSERT INTO backup_settings (key_name, value, updated_at) VALUES (?,?,?)
ON CONFLICT(key_name) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at'
: 'INSERT INTO backup_settings (key_name, value, updated_at) VALUES (?,?,?)
ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=VALUES(updated_at)';
$pdo->prepare($sql)->execute([$key, $value, db_now()]);
}
// ── POST-Handler ─────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'create_backup') {
$result = create_backup();
if ($result['ok']) {
// FTP-Upload wenn konfiguriert
$ftpHost = bget('ftp_host');
$ftpUser = bget('ftp_user');
$ftpPass = bget('ftp_pass');
$ftpPath = bget('ftp_path') ?: '/';
$ftpPort = (int)(bget('ftp_port') ?: 21);
$ftpTls = bget('ftp_tls') === '1';
$ftpMsg = '';
if ($ftpHost && $ftpUser && function_exists('ftp_connect')) {
$ftp = $ftpTls ? @ftp_ssl_connect($ftpHost, $ftpPort, 10) : @ftp_connect($ftpHost, $ftpPort, 10);
if ($ftp && @ftp_login($ftp, $ftpUser, $ftpPass)) {
ftp_pasv($ftp, true);
$remote = rtrim($ftpPath, '/') . '/' . $result['filename'];
$ftpMsg = ftp_put($ftp, $remote, $result['path'], FTP_BINARY)
? ' – FTP-Upload erfolgreich.' : ' – FTP-Upload fehlgeschlagen.';
ftp_close($ftp);
} else {
$ftpMsg = ' – FTP-Verbindung fehlgeschlagen.';
}
}
// Lokale Rotation
$keep = (int)(bget('rotate_keep') ?: 0);
if ($keep > 0) {
$bdir = realpath(__DIR__ . '/../backups');
$files = array_merge(glob($bdir.'/backup_*.zip')?:[], glob($bdir.'/backup_*.tar.gz')?:[]);
usort($files, fn($a,$b) => filemtime($b)-filemtime($a));
foreach (array_slice($files, $keep) as $old) @unlink($old);
}
$verified = $result['verify'] ?? backup_verify_and_record($result['path']); // create_backup() hat schon automatisch geprüft
audit_log($staff['id'], "Manuelles Backup: {$result['filename']} ({$result['size']} MB)$ftpMsg");
flash($verified['ok'] ? 'success' : 'error', ($verified['ok'] ? "✅ Backup erstellt und geprüft: {$result['filename']} ({$result['size']} MB){$ftpMsg}"
: "Backup erstellt, aber NICHT wiederherstellbar: " . implode(' ', $verified['errors'])));
} else {
flash('error', 'Backup fehlgeschlagen: ' . $result['error']);
}
redirect('/staff/backup.php');
}
if ($action === 'verify_backup') {
$path = backup_path_for((string) ($_POST['filename'] ?? ''));
if (!$path) { flash('error', 'Backup nicht gefunden.'); redirect('/staff/backup.php'); }
$v = backup_verify_and_record($path);
$i = $v['info'];
if ($v['ok']) {
flash('success', '✅ ' . basename($path) . ' ist vollständig und wiederherstellbar (' . strtoupper((string) ($i['driver'] ?? '')) . '-Datenbank, ' . (int) ($i['tables'] ?? 0) . ' Tabellen'
. (isset($i['tickets']) ? ', ' . $i['tickets'] . ' Tickets' : '') . ', ' . (int) ($i['files'] ?? 0) . ' Dateien).' . ($v['warnings'] ? ' Hinweis: ' . implode(' ', $v['warnings']) : ''));
} else {
flash('error', basename($path) . ' ist NICHT wiederherstellbar: ' . implode(' ', $v['errors']));
}
redirect('/staff/backup.php');
}
if ($action === 'restore_backup') {
$path = backup_path_for((string) ($_POST['filename'] ?? ''));
if (!$path) { flash('error', 'Backup nicht gefunden.'); redirect('/staff/backup.php'); }
if (trim((string) ($_POST['confirm'] ?? '')) !== 'WIEDERHERSTELLEN') {
flash('error', 'Zur Bestätigung bitte WIEDERHERSTELLEN eintippen. Es wurde nichts verändert.');
redirect('/staff/backup.php');
}
$dirs = array_values(array_intersect(array_keys(backup_restorable_dirs()), (array) ($_POST['dirs'] ?? [])));
$res = backup_restore($pdo, $path, ['database' => !empty($_POST['database']), 'dirs' => $dirs]);
if ($res['ok']) {
audit_log($staff['id'], 'Backup wiederhergestellt: ' . basename($path) . " ({$res['tables']} Tabellen, {$res['files']} Dateien, Sicherheits-Backup: " . ($res['safety'] ?? '–') . ')');
flash('success', '✅ Wiederhergestellt: ' . basename($path) . " ({$res['tables']} Tabellen, {$res['rows']} Datensätze, {$res['files']} Dateien). Der Stand von vorher steht im Sicherheits-Backup " . ($res['safety'] ?? '–') . '. ' . implode(' ', $res['notes'])
. ' Falls du abgemeldet wirst: Der Mitarbeiter-Stand entspricht jetzt dem Backup, bitte neu anmelden.');
} else {
flash('error', $res['error']);
}
redirect('/staff/backup.php');
}
if ($action === 'delete_backup') {
$file = basename($_POST['filename'] ?? '');
$bdir = realpath(__DIR__ . '/../backups');
$path = $bdir . '/' . $file;
if ($file && $bdir && file_exists($path)
&& strpos(realpath($path), $bdir) === 0
&& preg_match('/^backup_[\d\-_]+\.(zip|tar\.gz)$/', $file)) {
unlink($path);
audit_log($staff['id'], "Backup gelöscht: $file");
flash('success', "Backup $file gelöscht.");
} else {
flash('error', 'Backup nicht gefunden.');
}
redirect('/staff/backup.php');
}
if ($action === 'save_schedule') {
bset_save('schedule', $_POST['schedule'] ?? 'disabled');
bset_save('rotate_keep', (string)(int)($_POST['rotate_keep'] ?? 0));
audit_log($staff['id'], 'Backup-Zeitplan gespeichert');
flash('success', 'Zeitplan gespeichert.');
redirect('/staff/backup.php#zeitplan');
}
if ($action === 'save_ftp') {
bset_save('ftp_host', trim($_POST['ftp_host'] ?? '') ?: null);
bset_save('ftp_port', trim($_POST['ftp_port'] ?? '21'));
bset_save('ftp_user', trim($_POST['ftp_user'] ?? '') ?: null);
bset_save('ftp_tls', !empty($_POST['ftp_tls']) ? '1' : '0');
bset_save('ftp_path', trim($_POST['ftp_path'] ?? '/'));
bset_save('ftp_keep', (string)(int)($_POST['ftp_keep'] ?? 5));
if (!empty($_POST['ftp_pass'])) {
bset_save('ftp_pass', $_POST['ftp_pass']);
}
audit_log($staff['id'], 'FTP-Backup-Einstellungen gespeichert');
flash('success', 'FTP-Einstellungen gespeichert.');
redirect('/staff/backup.php#ftp');
}
if ($action === 'test_ftp') {
$host = bget('ftp_host'); $user = bget('ftp_user'); $pass = bget('ftp_pass');
$port = (int)(bget('ftp_port') ?: 21); $tls = bget('ftp_tls') === '1';
if (!$host || !$user) { flash('error', 'FTP nicht konfiguriert.'); }
elseif (!function_exists('ftp_connect')) { flash('error', 'PHP FTP-Erweiterung nicht verfügbar.'); }
else {
$ftp = $tls ? @ftp_ssl_connect($host, $port, 10) : @ftp_connect($host, $port, 10);
if ($ftp && @ftp_login($ftp, $user, $pass)) {
ftp_close($ftp);
flash('success', "✅ FTP-Verbindung zu $host erfolgreich.");
} else {
flash('error', "FTP-Verbindung zu $host fehlgeschlagen.");
}
}
redirect('/staff/backup.php#ftp');
}
}
// ── Daten laden ───────────────────────────────────────────────────────────────
$backupDir = realpath(__DIR__ . '/../backups') ?: (dirname(__DIR__) . '/backups');
$backupFiles = [];
if (is_dir($backupDir)) {
foreach (array_merge(glob($backupDir.'/backup_*.zip')?:[], glob($backupDir.'/backup_*.tar.gz')?:[]) as $f) {
$backupFiles[] = ['name'=>basename($f),'size'=>round(filesize($f)/1024/1024,2),'bytes'=>filesize($f),'date'=>filemtime($f)];
}
usort($backupFiles, fn($a,$b) => $b['date']-$a['date']);
}
$totalSize = array_sum(array_column($backupFiles, 'size'));
$lastAutoRun = get_setting('cron_process_backups_last_run');
$lastAutoBkp = bget('last_auto_backup');
$checkMap = json_decode((string) get_setting('backup_check_map', ''), true);
$checkMap = is_array($checkMap) ? $checkMap : [];
$rows = [];
foreach ($backupFiles as $b) $rows[] = $b + ['source' => 'local', 'badges' => [], 'actions' => ''];
// Plugins mit Reiter unter „Backup“ (zum Beispiel Synology NAS Backup) können Marken, Knöpfe und weitere Einträge (nur extern vorhandene Backups) ergänzen.
// Zeilen: name, size (MB), bytes, date, source ('local'|'extern'), badges (HTML-Liste), actions (HTML). Die Plugins sind für das Maskieren ihrer HTML-Ausgaben selbst verantwortlich.
if (function_exists('plugin_filter')) $rows = plugin_filter('backup.list.rows', $rows, ['backupDir' => $backupDir]);
usort($rows, fn($a, $b) => ($b['date'] ?? 0) <=> ($a['date'] ?? 0));
$ago = function (int $ts): string {
$d = time() - $ts;
if ($d < 90) return 'gerade eben';
if ($d < 3600) return 'vor ' . round($d / 60) . ' Min.';
if ($d < 86400) return 'vor ' . round($d / 3600) . ' Std.';
return 'vor ' . round($d / 86400) . ' Tagen';
};
$fmtMb = fn($mb) => $mb >= 1024 ? number_format($mb / 1024, 2, ',', '.') . ' GB' : number_format((float) $mb, $mb < 10 ? 2 : 0, ',', '.') . ' MB';
// Zahlen für die Kacheln oben: Backups auf dem Server und (über Plugins, zum Beispiel NAS) extern vorhandene
$externRows = array_values(array_filter($rows, fn($r) => ($r['source'] ?? 'local') !== 'local'));
$sizeAll = array_sum(array_map(fn($r) => (float) ($r['size'] ?? 0), $rows));
$sizeExtern = array_sum(array_map(fn($r) => (float) ($r['size'] ?? 0), $externRows));
$newestTs = $rows ? (int) max(array_map(fn($r) => (int) ($r['date'] ?? 0), $rows)) : 0;
$pageTitle = 'Backup';
$activePage = 'backup';
require __DIR__ . '/_header.php';
?>
<?= plugin_subpage_tabs('backup', $staff, 'parent') ?>
<div class="topbar">
<h1>💾 Backup</h1>
<form method="post" id="backup-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="create_backup">
<button type="submit" class="btn">💾 Backup jetzt erstellen</button>
</form>
</div>
<!-- Statistiken -->
<div class="stat-grid" style="margin-bottom:20px">
<div class="stat-card"><div class="num"><?= count($rows) ?></div><div class="lbl">Backups vorhanden</div>
<div class="lbl" style="text-transform:none;letter-spacing:0;margin-top:3px"><?= count($backupFiles) ?> auf dem Server<?= $externRows ? ' · ' . count($externRows) . ' nur extern (NAS)' : '' ?></div></div>
<div class="stat-card"><div class="num"><?= e($fmtMb($sizeAll)) ?></div><div class="lbl">Gesamtgröße</div>
<div class="lbl" style="text-transform:none;letter-spacing:0;margin-top:3px">Server: <?= e($fmtMb($totalSize)) ?><?= $externRows ? ' · extern: ' . e($fmtMb($sizeExtern)) : '' ?></div></div>
<div class="stat-card"><div class="num"><?= $newestTs ? e(date('d.m.Y', $newestTs)) : '–' ?></div><div class="lbl">Letztes Backup</div>
<div class="lbl" style="text-transform:none;letter-spacing:0;margin-top:3px"><?= $newestTs ? e(date('H:i', $newestTs)) . ' Uhr' : 'noch keins' ?></div></div>
<div class="stat-card"><div class="num"><?= $lastAutoBkp ? date('d.m.Y', strtotime($lastAutoBkp)) : '–' ?></div><div class="lbl">Letztes Auto-Backup</div>
<div class="lbl" style="text-transform:none;letter-spacing:0;margin-top:3px"><?= $lastAutoBkp ? e(date('H:i', strtotime($lastAutoBkp))) . ' Uhr' : ((bget('schedule') ?: 'disabled') === 'disabled' ? 'Zeitplan aus' : 'noch nicht gelaufen') ?></div></div>
</div>
<style>
.bk-list { display:flex; flex-direction:column; }
.bk-row { display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:6px 16px; align-items:center; padding:9px 4px; border-top:1px solid var(--border); }
.bk-row:first-child { border-top:0; }
.bk-name { min-width:0; }
.bk-name code { font-size:.82rem; word-break:break-all; }
.bk-meta { display:block; font-size:.76rem; color:var(--muted); margin-top:1px; }
.bk-badges { display:flex; gap:5px; flex-wrap:wrap; justify-content:flex-end; }
.bk-badge { font-size:.7rem; padding:2px 8px; border-radius:999px; border:1px solid var(--border); color:var(--muted); white-space:nowrap; }
.bk-badge.ok { color:#15803d; border-color:#bbf7d0; background:#f0fdf4; }
.bk-badge.bad { color:#b91c1c; border-color:#fecaca; background:#fef2f2; }
.bk-actions { display:flex; gap:5px; align-items:center; justify-content:flex-end; flex-wrap:nowrap; }
.bk-actions form { margin:0; }
.bk-btn { padding:4px 9px; font-size:.76rem; line-height:1.4; white-space:nowrap; cursor:pointer; list-style:none; }
.bk-btn::-webkit-details-marker { display:none; }
.bk-restore { position:relative; }
.bk-restore form.bk-pop { position:absolute; right:0; z-index:5; width:340px; padding:14px; background:var(--card-bg,#fff); border:1px solid var(--border); border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,.18); text-align:left; }
@media (max-width:820px) { .bk-row { grid-template-columns:1fr; } .bk-badges, .bk-actions { justify-content:flex-start; flex-wrap:wrap; } }
</style>
<!-- Backup-Liste -->
<div class="card" style="margin-bottom:20px">
<div style="display:flex; justify-content:space-between; align-items:baseline; gap:12px; flex-wrap:wrap">
<h3 style="margin:0">Vorhandene Backups</h3>
<?php $lastCheck = json_decode((string) get_setting('backup_last_check', ''), true); if (is_array($lastCheck)): ?>
<span class="muted" style="font-size:.8rem">Letzte Prüfung: <?= e(date('d.m.Y H:i', strtotime($lastCheck['at']))) ?> –
<strong style="color:<?= $lastCheck['ok'] ? '#15803d' : '#dc2626' ?>"><?= $lastCheck['ok'] ? 'wiederherstellbar' : 'nicht wiederherstellbar' ?></strong></span>
<?php endif; ?>
</div>
<?php if (!$rows): ?>
<div class="empty-state">Noch keine Backups vorhanden.</div>
<?php else: ?>
<div class="bk-list" style="margin-top:8px">
<?php foreach ($rows as $b):
$isLocal = ($b['source'] ?? 'local') === 'local';
$chk = $isLocal ? ($checkMap[$b['name']] ?? null) : null;
?>
<div class="bk-row">
<div class="bk-name">
<code><?= e($b['name']) ?></code>
<span class="bk-meta"><?= e($fmtMb($b['size'] ?? 0)) ?> · <?= e(date('d.m.Y H:i', (int) ($b['date'] ?? 0))) ?> (<?= e($ago((int) ($b['date'] ?? 0))) ?>)<?= !$isLocal ? ' · nicht auf diesem Server' : '' ?></span>
</div>
<div class="bk-badges">
<?php if ($isLocal && str_ends_with($b['name'], '.zip')): ?>
<?php if ($chk): ?>
<span class="bk-badge <?= $chk['ok'] ? 'ok' : 'bad' ?>" title="Geprüft am <?= e(date('d.m.Y H:i', strtotime($chk['at']))) ?>"><?= $chk['ok'] ? '✔ geprüft' : '✖ defekt' ?></span>
<?php else: ?>
<span class="bk-badge" title="Noch nicht auf Wiederherstellbarkeit geprüft">nicht geprüft</span>
<?php endif; ?>
<?php endif; ?>
<?php foreach ((array) ($b['badges'] ?? []) as $badge): ?><?= $badge ?><?php endforeach; ?>
</div>
<div class="bk-actions">
<?php if ($isLocal): ?>
<a href="/staff/backup_download.php?file=<?= urlencode($b['name']) ?>" class="btn secondary bk-btn" title="Backup herunterladen">⬇ Download</a>
<?php if (str_ends_with($b['name'], '.zip')): ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="verify_backup"><input type="hidden" name="filename" value="<?= e($b['name']) ?>">
<button type="submit" class="btn secondary bk-btn" title="Prüft, ob sich das Backup wiederherstellen lässt (verändert nichts)">✔ Prüfen</button></form>
<?php endif; ?>
<?php endif; ?>
<?= $b['actions'] ?? '' ?>
<?php if ($isLocal && str_ends_with($b['name'], '.zip')): ?>
<details class="bk-restore"><summary class="btn secondary bk-btn" title="Datenbank und Dateien aus diesem Backup zurückspielen">♻ Wiederherstellen</summary>
<form method="post" class="bk-pop">
<?= csrf_field() ?><input type="hidden" name="action" value="restore_backup"><input type="hidden" name="filename" value="<?= e($b['name']) ?>">
<p style="font-size:.82rem; margin:0 0 8px"><strong>Achtung:</strong> Der aktuelle Stand der Datenbank wird durch den Stand dieses Backups ersetzt. Vorher wird automatisch ein Sicherheits-Backup erstellt. Programmdateien und die Konfiguration bleiben unverändert.</p>
<label style="display:block; font-weight:400"><input type="checkbox" name="database" value="1" checked style="width:auto"> Datenbank</label>
<?php foreach (backup_restorable_dirs() as $dk => $dl): ?><label style="display:block; font-weight:400"><input type="checkbox" name="dirs[]" value="<?= e($dk) ?>" checked style="width:auto"> <?= e($dl) ?></label><?php endforeach; ?>
<label style="margin-top:8px; font-size:.8rem">Zur Bestätigung <code>WIEDERHERSTELLEN</code> eintippen</label>
<input type="text" name="confirm" autocomplete="off" required>
<button type="submit" class="btn" style="margin-top:8px; background:var(--danger)">Jetzt wiederherstellen</button>
</form></details>
<?php endif; ?>
<?php if ($isLocal): ?>
<form method="post" onsubmit="return confirm('Dieses Backup auf dem Server löschen?')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete_backup">
<input type="hidden" name="filename" value="<?= e($b['name']) ?>">
<button type="submit" class="btn secondary bk-btn" style="border-color:var(--danger); color:var(--danger)" title="Auf dem Server löschen">🗑</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-bottom:20px">
<!-- Zeitplan -->
<div class="card" id="zeitplan">
<h3>⏰ Automatischer Zeitplan</h3>
<p class="muted" style="font-size:.85rem; margin-bottom:16px">
Cron-Job einrichten: <code>php <?= e(dirname(__DIR__)) ?>/cron/process_backups.php</code><br>
<?php if ($lastAutoRun): ?><span style="font-size:.78rem">Letzter Lauf: <?= format_dt($lastAutoRun) ?></span><?php endif; ?>
</p>
<p class="muted" style="font-size:.8rem; margin:0 0 12px">✔ Jedes Backup wird nach dem Erstellen automatisch auf Wiederherstellbarkeit geprüft; ältere werden regelmäßig erneut geprüft.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_schedule">
<label>Backup-Intervall</label>
<select name="schedule">
<?php foreach (['disabled'=>'Deaktiviert','daily'=>'Täglich','weekly'=>'Wöchentlich','monthly'=>'Monatlich'] as $v=>$l): ?>
<option value="<?= $v ?>" <?= bget('schedule')===$v?'selected':'' ?>><?= $l ?></option>
<?php endforeach; ?>
</select>
<label style="margin-top:14px">Backup-Rotation (lokale Kopien behalten)</label>
<select name="rotate_keep">
<option value="0" <?= bget('rotate_keep')==='0'?'selected':'' ?>>Alle behalten</option>
<?php foreach ([3,5,7,10,14,30] as $n): ?>
<option value="<?= $n ?>" <?= bget('rotate_keep')===(string)$n?'selected':'' ?>><?= $n ?> Backups behalten</option>
<?php endforeach; ?>
</select>
<p class="muted" style="font-size:.78rem; margin-top:6px">Ältere Backups werden automatisch gelöscht wenn das Limit erreicht wird.</p>
<div class="btn-row" style="margin-top:14px">
<button type="submit" class="btn">Zeitplan speichern</button>
</div>
</form>
</div>
<!-- FTP -->
<div class="card" id="ftp">
<h3>📡 FTP-Upload <span class="muted" style="font-size:.78rem; font-weight:400">(optional)</span></h3>
<p class="muted" style="font-size:.85rem; margin-bottom:16px">Backups werden nach der Erstellung automatisch per FTP übertragen.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_ftp">
<div style="display:grid; grid-template-columns:1fr auto; gap:8px; align-items:flex-end; margin-bottom:10px">
<div>
<label>FTP-Host</label>
<input type="text" name="ftp_host" value="<?= e(bget('ftp_host') ?? '') ?>" placeholder="ftp.beispiel.de">
</div>
<div style="width:80px">
<label>Port</label>
<input type="number" name="ftp_port" value="<?= e(bget('ftp_port') ?? '21') ?>" min="1" max="65535">
</div>
</div>
<label>Benutzername</label>
<input type="text" name="ftp_user" value="<?= e(bget('ftp_user') ?? '') ?>" placeholder="ftp-benutzer" autocomplete="off">
<label style="margin-top:10px">Passwort</label>
<input type="password" name="ftp_pass" placeholder="<?= bget('ftp_pass') ? '••••••••' : 'Passwort eingeben' ?>" autocomplete="new-password">
<label style="margin-top:10px">Zielverzeichnis</label>
<input type="text" name="ftp_path" value="<?= e(bget('ftp_path') ?? '/') ?>" placeholder="/backups/deskhub">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px">
<div>
<label>FTP-Rotation (Kopien auf Server)</label>
<select name="ftp_keep">
<option value="0" <?= bget('ftp_keep')==='0'?'selected':'' ?>>Alle behalten</option>
<?php foreach ([3,5,7,10] as $n): ?>
<option value="<?= $n ?>" <?= bget('ftp_keep')===(string)$n?'selected':'' ?>><?= $n ?> behalten</option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex; align-items:flex-end; padding-bottom:2px">
<label style="display:flex; align-items:center; gap:8px; font-weight:400; cursor:pointer">
<input type="checkbox" name="ftp_tls" value="1" <?= bget('ftp_tls')==='1'?'checked':'' ?> style="width:auto">
FTPS (TLS)
</label>
</div>
</div>
<div class="btn-row" style="margin-top:16px; gap:8px">
<button type="submit" class="btn">FTP speichern</button>
<button type="submit" form="ftp-test-form" class="btn secondary">Verbindung testen</button>
</div>
</form>
<form method="post" id="ftp-test-form" style="display:none">
<?= csrf_field() ?>
<input type="hidden" name="action" value="test_ftp">
</form>
</div>
</div>
<!-- Hinweise -->
<div class="card">
<h3>ℹ️ Hinweise</h3>
<ul style="padding-left:20px; line-height:2; font-size:.88rem; color:var(--muted)">
<li>Backups enthalten alle Dateien inkl. <strong style="color:var(--ink)">Datenbank</strong> und <strong style="color:var(--ink)">Konfiguration</strong></li>
<li>Backups werden automatisch vor jedem Update erstellt</li>
<li>Für automatische Backups den Cron einrichten:<br><code>0 2 * * * php <?= e(dirname(__DIR__)) ?>/cron/process_backups.php</code></li>
<li>Backups liegen in <code>backups/</code> und sind von außen nicht zugänglich</li>
</ul>
</div>
<script>
document.getElementById('backup-form').addEventListener('submit', function(e) {
e.preventDefault();
var btn = this.querySelector('button[type=submit]');
btn.disabled = true;
btn.textContent = '⏳ Backup wird erstellt...';
this.submit();
});
</script>
<?php require __DIR__ . '/_footer.php'; ?>