419 lines
25 KiB
PHP
419 lines
25 KiB
PHP
<?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'; ?>
|