Medowar 1 mesiac pred
rodič
commit
6c2b98e749

+ 2 - 1
.gitignore

@@ -1,8 +1,9 @@
 config.php
+manage-client/config.php
 .codex
 data/reservations.php
 data/logs/
-data/updates/
+data/manage/
 data/backups/
 data/orders.json
 build/

+ 6 - 2
README.md

@@ -29,7 +29,8 @@ Dieses Projekt ist ein internes Bestellsystem für persönliche Schutzausrüstun
 - Systemeinstellungen: `data/settings.json`
 - Produkte: `data/products.json`
 - Produktbilder: `data/uploads/`
-- Backups: `data/backups/` (wird automatisch erstellt, nicht öffentlich)
+- Update- und Backup-Client: `manage-client/` (Konfiguration in `manage-client/config.php`)
+- Laufzeitdaten des Clients: `data/manage/` (Backups, Update-Sicherungen, Log; nicht öffentlich)
 
 ## Einrichtung
 
@@ -37,12 +38,15 @@ Dieses Projekt ist ein internes Bestellsystem für persönliche Schutzausrüstun
 2. Schreibrechte auf `data/` und `data/ratelimit/` (für Rate-Limits) sicherstellen.
 3. Statische Dateien bereitstellen: `favicon.png` (Document Root), `assets/branding/`, `assets/fonts/`, `assets/no-image.jpg`.
 4. Adminzugänge in `data/admins.json` auf dem Server pflegen (nicht aus dem Repo übernehmen).
-5. Empfängeradresse, PDF-Anhang und Backups im Admin unter `Einstellungen` prüfen.
+5. Empfängeradresse und PDF-Anhang im Admin unter `Einstellungen` prüfen.
 6. Kategorien, FAQ und Organisationen im Admin unter `Einstellungen` pflegen.
 7. Apache: `.htaccess` aktiv (schützt `config.php` und JSON unter `data/`).
+8. `manage-client/config.sample.php` nach `manage-client/config.php` kopieren und Instanz + Token des Manage-Servers eintragen (siehe [docs/UPDATE_AND_BACKUP.md](docs/UPDATE_AND_BACKUP.md)).
 
 Weitere Konstanten: [docs/CONFIG_REFERENCE.md](docs/CONFIG_REFERENCE.md).
 
+Updates und Backups: [docs/UPDATE_AND_BACKUP.md](docs/UPDATE_AND_BACKUP.md).
+
 Technische Dokumentation im Browser: [docs/index.php](docs/index.php) (Markdown wird mit [marked](https://marked.js.org/) gerendert).
 
 ## Hinweise

+ 2 - 2
admin/index.php

@@ -1,7 +1,7 @@
 <?php
 require_once __DIR__ . '/../config.php';
 require_once __DIR__ . '/../includes/functions.php';
-require_once __DIR__ . '/../includes/backup.php';
+require_once __DIR__ . '/../manage-client/lib/client.php';
 
 if (empty($_SESSION['admin_logged_in'])) {
     header('Location: login.php');
@@ -13,7 +13,7 @@ $backupAutoMessage = '';
 $backupAutoMessageType = '';
 
 try {
-    $backup = backupCreateAutomaticIfDue();
+    $backup = manageBackupCreateAutomaticIfDue();
     if ($backup !== null) {
         $backupAutoMessage =
             'Automatisches Backup wurde erstellt: ' .

+ 24 - 246
admin/settings.php

@@ -2,79 +2,7 @@
 require_once __DIR__ . "/../config.php";
 require_once __DIR__ . "/../includes/functions.php";
 require_once __DIR__ . "/../includes/version.php";
-require_once __DIR__ . "/../includes/backup.php";
-
-if (!defined("UPDATE_MANIFEST_URL")) {
-    define("UPDATE_MANIFEST_URL", "");
-}
-
-function settingsUpdaterVersionCompareValue(string $version): string
-{
-    return ltrim(trim($version), "vV");
-}
-
-function settingsGetUpdaterStatus(): array
-{
-    $manifestUrl = trim((string) UPDATE_MANIFEST_URL);
-    if ($manifestUrl === "") {
-        return [
-            "label" => "Update-Ziel ist nicht konfiguriert.",
-            "available" => false,
-            "version" => "",
-        ];
-    }
-
-    $context = stream_context_create([
-        "http" => [
-            "method" => "GET",
-            "timeout" => 3,
-            "ignore_errors" => true,
-            "header" => "User-Agent: PSA-Orderform-Settings/" . APP_VERSION . "\r\n",
-        ],
-    ]);
-
-    $body = @file_get_contents($manifestUrl, false, $context);
-    if ($body === false) {
-        return [
-            "label" => "Update-Status konnte nicht geladen werden.",
-            "available" => false,
-            "version" => "",
-        ];
-    }
-
-    $manifest = json_decode($body, true);
-    if (!is_array($manifest)) {
-        return [
-            "label" => "Update-Status ist ungültig.",
-            "available" => false,
-            "version" => "",
-        ];
-    }
-
-    $version = trim((string) ($manifest["version"] ?? $manifest["latest"] ?? ""));
-    if (!preg_match('/^v\d+\.\d+\.\d+$/', $version)) {
-        return [
-            "label" => "Update-Version ist ungültig.",
-            "available" => false,
-            "version" => "",
-        ];
-    }
-
-    $available =
-        version_compare(
-            settingsUpdaterVersionCompareValue($version),
-            settingsUpdaterVersionCompareValue(APP_VERSION),
-            ">",
-        );
-
-    return [
-        "label" => $available
-            ? "Update verfügbar: " . $version
-            : "Kein Update verfügbar.",
-        "available" => $available,
-        "version" => $version,
-    ];
-}
+require_once __DIR__ . "/../manage-client/lib/client.php";
 
 function settingsFormatBackupDate(string $date): string
 {
@@ -86,43 +14,6 @@ function settingsFormatBackupDate(string $date): string
     return date("d.m.Y H:i", $timestamp);
 }
 
-function settingsGetBackupUploadLabel(array $backup): string
-{
-    $uploads =
-        isset($backup["remote_uploads"]) && is_array($backup["remote_uploads"])
-            ? $backup["remote_uploads"]
-            : [];
-
-    if (empty($uploads)) {
-        return "Nur lokal";
-    }
-
-    $successful = 0;
-    foreach ($uploads as $upload) {
-        if (is_array($upload) && !empty($upload["success"])) {
-            $successful++;
-        }
-    }
-
-    if ($successful === count($uploads)) {
-        return "Remote erfolgreich (" . $successful . ")";
-    }
-    if ($successful > 0) {
-        return "Teilweise erfolgreich (" . $successful . "/" . count($uploads) . ")";
-    }
-
-    return "Remote fehlgeschlagen";
-}
-
-function settingsGetBackupCapabilityLabel(array $capability): string
-{
-    if (empty($capability["configured"])) {
-        return "nicht konfiguriert";
-    }
-
-    return !empty($capability["available"]) ? "bereit" : "nicht verfügbar";
-}
-
 function settingsIsSuperAdmin(): bool
 {
     return normalizeAdminUsername($_SESSION['admin_username'] ?? "") === "admin";
@@ -190,47 +81,6 @@ function settingsWriteLocalConfig(string $content): void
     }
 }
 
-function settingsFindBackupByFilename(string $filename): ?array
-{
-    if ($filename === "" || basename($filename) !== $filename) {
-        return null;
-    }
-
-    foreach (backupListBackups() as $backup) {
-        if (($backup["filename"] ?? "") === $filename) {
-            return $backup;
-        }
-    }
-
-    return null;
-}
-
-function settingsSendBackupDownload(array $backup): void
-{
-    $filename = basename((string) ($backup["filename"] ?? ""));
-    $path = backupGetDirectory() . $filename;
-
-    if ($filename === "" || !is_file($path) || !is_readable($path)) {
-        throw new RuntimeException("Backup-Datei wurde nicht gefunden.");
-    }
-    $size = filesize($path);
-    if ($size === false) {
-        throw new RuntimeException("Backup-Dateigröße konnte nicht gelesen werden.");
-    }
-
-    logAccess("Backup downloaded", [
-        "filename" => $filename,
-    ]);
-
-    header("Content-Type: application/zip");
-    header('Content-Disposition: attachment; filename="' . $filename . '"');
-    header("Content-Length: " . (string) $size);
-    header("X-Content-Type-Options: nosniff");
-
-    readfile($path);
-    exit();
-}
-
 if (empty($_SESSION['admin_logged_in'])) {
     header("Location: login.php");
     exit();
@@ -264,41 +114,6 @@ if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_settings'])) {
             $messageType = "error";
         }
     }
-} elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['create_backup'])) {
-    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
-        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
-        $messageType = "error";
-    } else {
-        try {
-            $backup = backupCreate("manual");
-            $message =
-                "Backup wurde erstellt: " .
-                $backup["filename"] .
-                " (" .
-                backupFormatBytes((int) $backup["size"]) .
-                ").";
-            $messageType = "success";
-        } catch (Throwable $exception) {
-            $message = "Backup konnte nicht erstellt werden: " . $exception->getMessage();
-            $messageType = "error";
-        }
-    }
-} elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['download_backup'])) {
-    if (!validateCsrfToken($_POST['csrf_token'] ?? "")) {
-        $message = "Ungültiges Token. Bitte versuchen Sie es erneut.";
-        $messageType = "error";
-    } else {
-        try {
-            $backup = settingsFindBackupByFilename((string) ($_POST["backup_filename"] ?? ""));
-            if ($backup === null) {
-                throw new RuntimeException("Backup wurde nicht gefunden.");
-            }
-            settingsSendBackupDownload($backup);
-        } catch (Throwable $exception) {
-            $message = "Backup konnte nicht heruntergeladen werden: " . $exception->getMessage();
-            $messageType = "error";
-        }
-    }
 } elseif ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_local_config'])) {
     if (!$isSuperAdmin) {
         http_response_code(403);
@@ -567,9 +382,7 @@ if ($_SERVER['REQUEST_METHOD'] === "POST" && isset($_POST['save_settings'])) {
 }
 
 $settings = getSystemSettings();
-$updaterStatus = settingsGetUpdaterStatus();
-$backupCapabilities = backupRemoteCapabilities();
-$backups = backupListBackups();
+$manageStatus = manageClientStatus();
 $localConfigContent = $isSuperAdmin ? settingsReadLocalConfig() : "";
 
 $categories = getCategories();
@@ -855,66 +668,31 @@ include __DIR__ . "/../includes/header.php";
     </form>
 </div>
 
-<div class="panel panel-lg mt-4" id="backups">
-    <h3>Backups</h3>
-    <p>Lokale Aufbewahrung: <?php echo (int) backupGetRetentionLimit(); ?> Backups</p>
-    <p>Automatisches Intervall: <?php echo (int) floor(((int) BACKUP_AUTO_INTERVAL_SECONDS) / 86400); ?> Tage</p>
+<div class="panel panel-lg mt-4" id="update-backup">
+    <h3>Update &amp; Backup</h3>
+    <p>Installierte Version: <?php echo escape($manageStatus["version"] !== "" ? $manageStatus["version"] : "unbekannt"); ?></p>
+    <?php if (!$manageStatus["configured"]): ?>
+        <p>Der Manage-Client ist nicht konfiguriert. <code>manage-client/config.php</code> fehlt oder ist unvollständig.</p>
+    <?php elseif ($manageStatus["update"] !== null && $manageStatus["update"]["available"]): ?>
+        <p>Update verfügbar: <?php echo escape($manageStatus["update"]["latest"]); ?></p>
+    <?php elseif ($manageStatus["update"] !== null): ?>
+        <p>Kein Update verfügbar.</p>
+    <?php else: ?>
+        <p>Update-Status konnte nicht geladen werden<?php echo $manageStatus["update_error"] !== null
+            ? ": " . escape($manageStatus["update_error"])
+            : "."; ?></p>
+    <?php endif; ?>
     <p>
-        S3: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["s3"])); ?> ·
-        SFTP: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["sftp"])); ?> ·
-        Custom: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["custom"])); ?> ·
-        Managed: <?php echo escape(settingsGetBackupCapabilityLabel($backupCapabilities["managed"])); ?>
+        Letztes Backup:
+        <?php echo $manageStatus["last_backup_at"] !== null
+            ? escape(settingsFormatBackupDate($manageStatus["last_backup_at"]))
+            : "noch keines"; ?>
+        (<?php echo count($manageStatus["backups"]); ?> lokal)
     </p>
-
-    <form method="POST" action="settings.php#backups" class="inline-form">
-        <?php echo csrfField(); ?>
-        <button type="submit" name="create_backup" class="btn">Backup erstellen</button>
-    </form>
-
-    <h4 class="mt-4">Letzte Backups</h4>
-    <?php if (empty($backups)): ?>
-        <p>Es wurden noch keine Backups erstellt.</p>
-    <?php else: ?>
-        <div class="table-responsive">
-            <table class="responsive-table">
-                <thead>
-                    <tr>
-                        <th>Erstellt</th>
-                        <th>Auslöser</th>
-                        <th>Größe</th>
-                        <th>Dateien</th>
-                        <th>Remote</th>
-                        <th>Aktionen</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <?php foreach ($backups as $backup): ?>
-                        <tr>
-                            <td data-label="Erstellt"><?php echo escape(settingsFormatBackupDate((string) ($backup["created_at"] ?? ""))); ?></td>
-                            <td data-label="Auslöser"><?php echo (($backup["trigger"] ?? "") === "automatic") ? "Automatisch" : "Manuell"; ?></td>
-                            <td data-label="Größe"><?php echo escape(backupFormatBytes((int) ($backup["size"] ?? 0))); ?></td>
-                            <td data-label="Dateien"><?php echo (int) ($backup["file_count"] ?? 0); ?></td>
-                            <td data-label="Remote"><?php echo escape(settingsGetBackupUploadLabel($backup)); ?></td>
-                            <td data-label="Aktionen">
-                                <form method="POST" action="settings.php#backups" class="inline-form">
-                                    <?php echo csrfField(); ?>
-                                    <input type="hidden" name="backup_filename" value="<?php echo escape($backup["filename"] ?? ""); ?>">
-                                    <button type="submit" name="download_backup" class="btn btn-secondary btn-small">Download</button>
-                                </form>
-                            </td>
-                        </tr>
-                    <?php endforeach; ?>
-                </tbody>
-            </table>
-        </div>
+    <?php if ($manageStatus["pending_migrations"] !== []): ?>
+        <p><?php echo count($manageStatus["pending_migrations"]); ?> offene Migration(en).</p>
     <?php endif; ?>
-</div>
-
-<div class="panel panel-lg mt-4">
-    <h3>Updater</h3>
-    <p>Installierte Version: <?php echo escape(APP_VERSION); ?></p>
-    <p>Update-Status: <?php echo escape($updaterStatus["label"]); ?></p>
-    <p><a href="updater.php" class="btn btn-secondary">Updater öffnen</a></p>
+    <p><a href="manage.php" class="btn btn-secondary">Update &amp; Backup öffnen</a></p>
 </div>
 
 <?php if ($isSuperAdmin): ?>

+ 4 - 50
config.sample.php

@@ -55,56 +55,10 @@ define('CATEGORIES_FILE', DATA_DIR . 'categories.json');
 define('FAQ_FILE', DATA_DIR . 'faq.json');
 define('UPLOADS_URL', SITE_URL . '/data/uploads');
 
-// Manual update settings
-// Point this to the central update server's manifest.php endpoint.
-// Note: the bundled `update-server/` component is deprecated and unmaintained.
-define('UPDATE_MANIFEST_URL', 'https://dev.med0.de/psa/updater/manifest.php');
-define('UPDATE_WORK_DIR', DATA_DIR . 'updates/work/');
-define('UPDATE_BACKUP_DIR', DATA_DIR . 'updates/backups/');
-
-// Data backup settings
-define('BACKUP_DIR', DATA_DIR . 'backups/');
-define('BACKUP_LOCAL_RETENTION', 4);
-define('BACKUP_AUTO_INTERVAL_SECONDS', 604800);
-define('BACKUP_REMOTE_TARGETS', [
-    // [
-    //     'name' => 'S3 Backup',
-    //     'type' => 's3',
-    //     'bucket' => 'example-bucket',
-    //     'region' => 'eu-central-1',
-    //     'prefix' => 'psa-orderform',
-    //     'access_key' => 'AKIA...',
-    //     'secret_key' => '...',
-    //     // Optional for S3-compatible storage:
-    //     // 'endpoint' => 'https://s3.example.org',
-    // ],
-    // [
-    //     'name' => 'SFTP Backup',
-    //     'type' => 'sftp',
-    //     'host' => 'backup.example.org',
-    //     'port' => 22,
-    //     'username' => 'backup-user',
-    //     'password' => '...',
-    //     'path' => '/backups/psa-orderform',
-    // ],
-    // [
-    //     'name' => 'Custom Backup',
-    //     'type' => 'custom',
-    //     'file' => __DIR__ . '/custom-backup-uploader.php',
-    //     'callback' => 'uploadPsaOrderformBackup',
-    // ],
-    // [
-    //     'name' => 'Managed Backup Server',
-    //     'type' => 'managed',
-    //     'url' => 'https://backup.example.org/upload.php',
-    //     'instance' => 'stadt-freising-prod',
-    // ],
-]);
-
-// Log retention settings
-define('LOG_MAX_BYTES', 1048576);
-define('LOG_KEEP_FILES', 5);
-define('LOG_MAX_AGE_SECONDS', 2592000);
+// Update and backup
+// Handled by the manage client in manage-client/. Its settings - server URL,
+// instance, token, backup sources - live in manage-client/config.php, not here.
+// See docs/UPDATE_AND_BACKUP.md.
 
 // Session settings
 if (session_status() === PHP_SESSION_NONE) {

+ 1 - 1
docs/ADMIN_BUSINESS_LOGIC.md

@@ -292,7 +292,7 @@ Unter **Einstellungen** (`admin/settings.php`) im Abschnitt **Allgemein**:
 - **PDF an interne Bestell-E-Mails anhängen**
 - **Artikelname für Namensschilder**
 
-Die Seite ist in Abschnitte gegliedert (in dieser Reihenfolge): **Kategorien**, **FAQ**, **Organisationen**, **Allgemein**, **Backups**, **Updater** und — nur für das Konto `admin` — **Lokale Konfiguration**. Kategorien, FAQ und Organisationen haben keine eigenen Seiten mehr.
+Die Seite ist in Abschnitte gegliedert (in dieser Reihenfolge): **Kategorien**, **FAQ**, **Organisationen**, **Allgemein**, **Update & Backup** und — nur für das Konto `admin` — **Lokale Konfiguration**. Kategorien, FAQ und Organisationen haben keine eigenen Seiten mehr. Der Abschnitt **Update & Backup** zeigt nur den Status und verlinkt auf `admin/manage.php`; die Aktionen selbst liegen dort (siehe [UPDATE_AND_BACKUP.md](UPDATE_AND_BACKUP.md)).
 
 ---
 

+ 1 - 0
docs/ADMIN_SYSTEM.md

@@ -7,6 +7,7 @@ Das Admin-System nutzt einen klassischen Session-Login für den Bereich unter `a
 - Login-Seite: `admin/login.php`
 - Admin-Dashboard: `admin/index.php`
 - Admin-Verwaltung: `admin/admins.php`
+- Update & Backup: `admin/manage.php` (bindet `manage-client/ui/panel.php` ein, siehe [UPDATE_AND_BACKUP.md](UPDATE_AND_BACKUP.md))
 - Backend-Helfer: `includes/functions.php`
 - Persistenz: `data/admins.json`
 

+ 3 - 11
docs/CONFIG_REFERENCE.md

@@ -38,10 +38,6 @@
 | `CATEGORIES_FILE` | JSON-Datei für Kategorien |
 | `FAQ_FILE` | JSON-Datei für FAQ-Inhalte |
 | `MANUAL_BACKORDERS_FILE` | JSON-Datei für manuelle Nachbestell-Einträge (ohne Bestellbezug) |
-| `BACKUP_DIR` | Lokales Verzeichnis für Daten-Backups (Standard: `DATA_DIR . 'backups/'`) |
-| `BACKUP_LOCAL_RETENTION` | Anzahl lokal aufzubewahrender Backup-ZIPs (Standard: 4) |
-| `BACKUP_AUTO_INTERVAL_SECONDS` | Intervall für Backups durch Admin-Aktivität (Standard: 604800 = wöchentlich; 0 deaktiviert) |
-| `BACKUP_REMOTE_TARGETS` | Optionale Remote-Ziele für Backup-Uploads (`s3`, `sftp`, `custom`, `managed`) |
 
 ## Bearbeitung im Admin
 
@@ -66,14 +62,10 @@ Der Startseiten-Introtext wird unter **Einstellungen > FAQ** gepflegt (`startpag
 - Zugriffs- und Fehlerprotokolle: `data/logs/` (siehe `logAccess` / `logError` in `includes/functions.php`).
 - Logs werden ab `LOG_MAX_BYTES` rotiert, es bleiben `LOG_KEEP_FILES` rotierte Dateien erhalten, und rotierte Logs älter als `LOG_MAX_AGE_SECONDS` werden entfernt.
 
-## Backups
+## Update und Backup
 
-- Manuelle Backups werden im Admin unter **Einstellungen** erstellt.
-- Automatische Backups werden nur durch Admin-Aktivität auf der Einstellungsseite ausgelöst, wenn das konfigurierte Intervall abgelaufen ist.
-- Backups enthalten `data/*.json` und `data/uploads/**`; App-Dateien, Logs, Updates, Rate-Limits und bestehende Backups werden ausgeschlossen.
-- SFTP benötigt die optionale PHP-SSH2-Erweiterung. Ohne Erweiterung bleibt das lokale Backup gültig, der Remote-Upload wird als fehlgeschlagen protokolliert.
-- Zugangsdaten für Remote-Ziele gehören in `config.php`, nicht in `data/settings.json`.
-- Details und Beispiele: [Backup-Konfiguration](BACKUP_CONFIGURATION.md).
+Update- und Backup-Konstanten stehen **nicht** in `config.php`, sondern in
+`manage-client/config.php`. Details: [Update und Backup](UPDATE_AND_BACKUP.md).
 
 ## Hinweis
 

+ 1 - 1
docs/SHOP_LOGIC.md

@@ -187,7 +187,7 @@ Unter **Einstellungen** können Sie anpassen:
 - **FAQ** — FAQ-Inhalt (Markdown) und Text auf der Startseite
 - **Organisationen** — auswählbare Organisationen inkl. Sortierung und Aktiv-Status
 - **Allgemein** — Empfängeradresse für interne Bestellmails, PDF-Anhang ja/nein, Artikelname für Namensschilder
-- **Backups**, **Updater** und (nur für das Konto `admin`) **Lokale Konfiguration**
+- **Update & Backup** und (nur für das Konto `admin`) **Lokale Konfiguration**
 
 ---
 

+ 1 - 1
docs/UPDATE_AND_BACKUP.md

@@ -92,7 +92,7 @@ werden bewusst **nicht** automatisch installiert.
 
 Das Skript schreibt die Version nach `includes/version.php`, packt alle von Git
 verwalteten Dateien abzüglich der Ausschlussliste (`config.php`,
-`manage-client/config.php`, `data/`, `build/`, `scripts/`, `client-package/`)
+`manage-client/config.php`, `data/`, `build/`, `scripts/`)
 nach `build/releases/psa-orderform-vX.Y.Z.zip` und gibt SHA-256 und Größe aus.
 Das ZIP wird im Manage-Server unter **Releases** hochgeladen; Prüfsumme und
 Größe berechnet der Server selbst.

+ 0 - 1
scripts/create-release-zip.sh

@@ -41,7 +41,6 @@ EXCLUDES=(
     "build/"
     "scripts/"
     ".codex/"
-    "client-package/"
 )
 
 # --- END CONFIGURATION ------------------------------------------------------