= 8; } // Every message on this page, rendered as .alert blocks in source order. // A single action can produce several: an update reports deployment, migrations // and hook failure separately. $notices = []; $adminAccounts = getAdminAccounts(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!settingsCsrfValid($_POST['csrf_token'] ?? '')) { $notices[] = ['type' => 'error', 'text' => 'Ungültiges Sicherheitstoken. Bitte die Seite neu laden.']; } elseif (isset($_POST['create_backup'])) { try { $record = manageBackupCreate('manual'); $notices[] = ['type' => 'success', 'text' => sprintf( 'Backup erstellt: %s (%d Dateien, %s).', $record['filename'], $record['file_count'], manageFormatBytes((int) $record['size']) )]; // A failed upload is a warning, never an error: the local archive // is complete and valid either way. foreach ($record['remote_uploads'] as $upload) { if (empty($upload['success'])) { $notices[] = ['type' => 'warning', 'text' => 'Upload an ' . $upload['target'] . ' fehlgeschlagen: ' . ($upload['error'] ?? 'unbekannter Fehler')]; } } manageHeartbeatSendQuietly(); } catch (Throwable $exception) { $notices[] = ['type' => 'error', 'text' => 'Backup fehlgeschlagen: ' . $exception->getMessage()]; } } elseif (isset($_POST['download_backup'])) { try { $path = manageBackupPath($_POST['filename'] ?? ''); $handle = fopen($path, 'rb'); $size = filesize($path); if ($handle === false || $size === false) { throw new RuntimeException('Das Backup konnte nicht geöffnet werden.'); } header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . addcslashes(basename($path), '"\\') . '"'); header('Content-Length: ' . $size); header('Cache-Control: private, no-store'); header('X-Content-Type-Options: nosniff'); fpassthru($handle); fclose($handle); exit; } catch (Throwable $exception) { $notices[] = ['type' => 'error', 'text' => 'Download fehlgeschlagen: ' . $exception->getMessage()]; } } elseif (isset($_POST['apply_update'])) { try { $result = manageUpdateApply(['force' => !empty($_POST['force'])]); $notices[] = ['type' => 'success', 'text' => sprintf( 'Update ausgerollt: %s → %s. %d Dateien kopiert, %d gesichert, %d übersprungen.', $result['from_version'] !== '' ? $result['from_version'] : 'unbekannt', $result['to_version'], $result['copied'], $result['backed_up'], $result['skipped'] )]; $notices[] = ['type' => 'info', 'text' => 'Die überschriebenen Dateien liegen unter ' . $result['backup_dir'] . ' - nur für eine manuelle Wiederherstellung, es gibt kein Rollback.']; // The files are live at this point. A failing post-update step is // therefore reported on its own, not as "update failed". $hook = $result['hook']; if (is_array($hook)) { $applied = $hook['migrations']['applied'] ?? []; if ($applied !== []) { $notices[] = ['type' => 'success', 'text' => 'Migrationen ausgeführt: ' . implode(', ', $applied)]; } if (empty($hook['success'])) { if (!empty($hook['failed_migration'])) { $notices[] = ['type' => 'error', 'text' => 'Die Dateien wurden ausgerollt, aber die Migration "' . $hook['failed_migration'] . '" ist fehlgeschlagen: ' . ($hook['error'] ?? '')]; $notices[] = ['type' => 'error', 'text' => 'Die restlichen Migrationen wurden nicht ausgeführt. ' . 'Nach Behebung der Ursache unten "Migrationen ausführen" verwenden.']; } else { $notices[] = ['type' => 'error', 'text' => 'Die Dateien wurden ausgerollt, aber der ' . 'Post-Update-Hook ist fehlgeschlagen: ' . ($hook['error'] ?? '')]; } } } manageHeartbeatSendQuietly(); } catch (Throwable $exception) { $notices[] = ['type' => 'error', 'text' => 'Update fehlgeschlagen: ' . $exception->getMessage()]; } } elseif (isset($_POST['run_migrations'])) { $report = manageUpdateRunMigrations(); if ($report['applied'] !== []) { $notices[] = ['type' => 'success', 'text' => 'Migrationen ausgeführt: ' . implode(', ', $report['applied'])]; } if (!$report['success']) { $notices[] = ['type' => 'error', 'text' => 'Migration "' . $report['failed'] . '" ist fehlgeschlagen: ' . $report['error']]; } elseif ($report['applied'] === []) { $notices[] = ['type' => 'info', 'text' => 'Es gibt keine offenen Migrationen.']; } } elseif (isset($_POST['send_heartbeat'])) { try { $result = manageHeartbeatSend(); $notices[] = ['type' => 'success', 'text' => 'Status an den Manage-Server gemeldet. Aktuelles Release: ' . ($result['latest'] !== '' ? $result['latest'] : 'keines') . '.']; } catch (Throwable $exception) { $notices[] = ['type' => 'error', 'text' => 'Statusmeldung fehlgeschlagen: ' . $exception->getMessage()]; } } elseif (isset($_POST['add_admin'])) { $username = normalizeAdminUsername($_POST['username'] ?? ''); $description = normalizeAdminDescription($_POST['description'] ?? ''); $email = normalizeAdminEmail($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; $passwordConfirm = $_POST['password_confirm'] ?? ''; if (!isValidAdminUsername($username)) { $notices[] = ['type' => 'error', 'text' => 'Ungültiger Benutzername. Erlaubt: 3-50 Zeichen (Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich).']; } elseif (isset($adminAccounts[$username])) { $notices[] = ['type' => 'error', 'text' => 'Dieser Benutzername existiert bereits.']; } elseif (!isValidAdminDescription($description)) { $notices[] = ['type' => 'error', 'text' => 'Beschreibung ist erforderlich (max. 120 Zeichen).']; } elseif (!isValidAdminEmail($email)) { $notices[] = ['type' => 'error', 'text' => 'Gültige E-Mail ist erforderlich.']; } elseif (!isValidAdminPasswordInput($password)) { $notices[] = ['type' => 'error', 'text' => 'Passwort muss mindestens 8 Zeichen lang sein.']; } elseif ($password !== $passwordConfirm) { $notices[] = ['type' => 'error', 'text' => 'Passwort und Bestätigung stimmen nicht überein.']; } else { $adminAccounts[$username] = [ 'password_hash' => password_hash($password, PASSWORD_BCRYPT), 'description' => $description, 'email' => $email ]; saveAdminAccounts($adminAccounts); $notices[] = ['type' => 'success', 'text' => 'Admin wurde erfolgreich angelegt.']; } } elseif (isset($_POST['update_description'])) { $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? ''); $description = normalizeAdminDescription($_POST['description'] ?? ''); $email = normalizeAdminEmail($_POST['email'] ?? ''); if (!isset($adminAccounts[$targetUsername])) { $notices[] = ['type' => 'error', 'text' => 'Admin nicht gefunden.']; } elseif (!isValidAdminDescription($description)) { $notices[] = ['type' => 'error', 'text' => 'Beschreibung ist erforderlich (max. 120 Zeichen).']; } elseif (!isValidAdminEmail($email)) { $notices[] = ['type' => 'error', 'text' => 'Gültige E-Mail ist erforderlich.']; } else { $adminAccounts[$targetUsername]['description'] = $description; $adminAccounts[$targetUsername]['email'] = $email; saveAdminAccounts($adminAccounts); $notices[] = ['type' => 'success', 'text' => 'Beschreibung und E-Mail wurden aktualisiert.']; } } elseif (isset($_POST['change_password'])) { $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? ''); $newPassword = $_POST['new_password'] ?? ''; $newPasswordConfirm = $_POST['new_password_confirm'] ?? ''; if (!isset($adminAccounts[$targetUsername])) { $notices[] = ['type' => 'error', 'text' => 'Admin nicht gefunden.']; } elseif (!isValidAdminPasswordInput($newPassword)) { $notices[] = ['type' => 'error', 'text' => 'Passwort muss mindestens 8 Zeichen lang sein.']; } elseif ($newPassword !== $newPasswordConfirm) { $notices[] = ['type' => 'error', 'text' => 'Passwort und Bestätigung stimmen nicht überein.']; } else { $adminAccounts[$targetUsername]['password_hash'] = password_hash($newPassword, PASSWORD_BCRYPT); saveAdminAccounts($adminAccounts); $notices[] = ['type' => 'success', 'text' => 'Passwort wurde aktualisiert.']; } } elseif (isset($_POST['delete_admin'])) { $targetUsername = normalizeAdminUsername($_POST['target_username'] ?? ''); if (!isset($adminAccounts[$targetUsername])) { $notices[] = ['type' => 'error', 'text' => 'Admin nicht gefunden.']; } else { unset($adminAccounts[$targetUsername]); saveAdminAccounts($adminAccounts); if (isset($_SESSION['admin_username']) && $_SESSION['admin_username'] === $targetUsername) { $_SESSION['admin_logged_in'] = false; unset($_SESSION['admin_username']); session_destroy(); header('Location: login.php'); exit; } $notices[] = ['type' => 'success', 'text' => 'Admin wurde gelöscht.']; } } $adminAccounts = getAdminAccounts(); } // Collected after the actions, so the page shows the state they produced. // Never throws: remote failures come back inside the array. $status = manageClientStatus(); // No cron on this host, so the report to the Manage server rides along with // this page load - at most once an hour. manageHeartbeatSendIfDue(); $updateAvailable = $status['update'] !== null && !empty($status['update']['available']); $latestVersion = $status['update']['latest'] ?? ''; $currentAdmin = isset($_SESSION['admin_username']) ? normalizeAdminUsername($_SESSION['admin_username']) : ''; $changeUsername = normalizeAdminUsername($_GET['change'] ?? ''); $selectedChangeUser = null; $editDescriptionUsername = normalizeAdminUsername($_GET['edit_description'] ?? ''); $selectedDescriptionUser = null; if ($changeUsername !== '') { if (!isset($adminAccounts[$changeUsername])) { $notices[] = ['type' => 'error', 'text' => 'Ausgewählter Admin wurde nicht gefunden.']; } else { $selectedChangeUser = $changeUsername; } } if ($editDescriptionUsername !== '') { if (!isset($adminAccounts[$editDescriptionUsername])) { $notices[] = ['type' => 'error', 'text' => 'Ausgewählter Admin wurde nicht gefunden.']; } else { $selectedDescriptionUser = $editDescriptionUsername; } } ksort($adminAccounts); $csrfToken = settingsCsrfToken(); $bodyClass = 'admin-page'; include __DIR__ . '/../includes/header.php'; ?>
MANAGE_SERVER_URL, MANAGE_INSTANCE
und MANAGE_TOKEN in config.php funktionieren Update-Prüfung und Backup-Upload nicht.
Lokale Backups lassen sich trotzdem erstellen.
| Instanz | |
| Manage-Server | |
| PHP-Version | |
| Offene Migrationen |
Gesichert werden die Daten unter data/ und die Produktbilder unter
assets/images/. Lokal bleiben die letzten
Archive erhalten, jedes wird zusätzlich an den
Manage-Server übertragen.
0): ?>
Zusätzlich erstellt das Dashboard automatisch alle
Tage ein Backup.
Es wurde noch kein Backup erstellt.
| Datei | Erstellt | Auslöser | Dateien | Größe | Upload | Aktionen |
|---|---|---|---|---|---|---|
| nicht übertragen'; } else { foreach ($uploads as $upload) { if (!empty($upload['success'])) { echo '' . htmlspecialchars($upload['target']) . ': OK'; } else { echo '' . htmlspecialchars($upload['target']) . ': Fehler'; } } } ?> |
Diese Migrationen wurden noch nicht ausgeführt:
Eingeloggt als:
| Benutzername | Beschreibung | Aktionen | |
|---|---|---|---|
| Du | Profil ändern Passwort ändern |