# Architektur ## Überblick `manage` besteht aus zwei Hälften: dem Server in diesem Repository und dem Client-Paket, das in jedes betreute Projekt kopiert wird. Eine Server-Installation betreut **ein Produkt** mit einer überschaubaren Zahl von Instanzen. Für ein weiteres Produkt wird `manage` erneut ausgerollt. Relevante Verzeichnisse: - `admin/` – Oberfläche - `api/v1/` – Schnittstelle für Clients - `includes/` – gemeinsame Bibliothek - `storage/` – sämtlicher Zustand, nicht über das Web erreichbar - `client-package/` – der weitergebbare Ordner für Projekte ## Datenfluss ```text Projekt (Instanz) Manage-Server ───────────────── ───────────── manage-client/ bin/manage-client.php ──── check ───► api/v1/manifest.php ──► storage/releases/manifest.json ui/panel.php ──── update ──► api/v1/package.php ──► storage/releases/packages/ lib/*.php ──── backup ──► api/v1/backup.php ──► storage/backups// ──── status ──► api/v1/heartbeat.php ──► storage/instances.json │ ▼ admin/ (Anmeldung mit Passwort) ``` Der Client zieht; der Server schiebt nie. Es gibt keine Verbindung vom Server zur Instanz, was den Betrieb hinter NAT und Firewalls unkompliziert macht. ## Authentifizierung Zwei getrennte Wege: | Weg | Wer | Mittel | |---|---|---| | `admin/` | Menschen | ein Passwort, Sitzung, CSRF, Ratenbegrenzung | | `api/v1/` | Instanzen | Instanz-Kennung + Token in zwei Headern, zustandslos | Tokens werden als SHA-256-Hash gespeichert und in konstanter Zeit verglichen. Das Klartext-Token erscheint genau einmal, beim Anlegen oder Erneuern. Details: [client-package/docs/08_PROTOCOL.md](../client-package/docs/08_PROTOCOL.md). ## Instanzregister `storage/instances.json` ist das Bindeglied zwischen beiden Modulen. Es ersetzt zwei getrennte Mechanismen der Vorgängerlösung: die Namensliste des Backup-Servers und die vollständig fehlende Client-Identität des Update-Servers. ```json { "instances": [ { "id": "example-prod", "label": "Stadt Freising Produktiv", "enabled": true, "token_hash": "…", "created_at": "…", "token_rotated_at": "…", "last_seen_at": "…", "last_ip": "…", "version": "v1.3.14", "php_version": "8.3.6", "disk_free": 12884901888, "pending_migrations": 0, "last_backup_at": "…", "backup_count": 3, "notes": "" } ] } ``` Jede erfolgreich authentifizierte Anfrage aktualisiert `last_seen_at` und `last_ip`. Die Übersicht bleibt dadurch aktuell, auch ohne eigenen Heartbeat. Eine gelöschte Instanz kann nichts mehr hochladen; ihre bereits gespeicherten Backups bleiben aber erhalten und in der Oberfläche sichtbar. ## Releases `storage/releases/manifest.json` ist die Release-Datenbank, die Pakete liegen daneben in `packages/`: ```json { "latest": "v1.3.0", "releases": { "v1.3.0": { "version": "v1.3.0", "package": "packages/example-orderform-v1.3.0.zip", "sha256": "…", "size": 2199, "published_at": "…" } } } ``` Prüfsumme und Größe berechnet immer der Server nach dem Upload; sie werden nie vom Hochladenden übernommen. Ein Upload setzt das Release automatisch als `latest`. Die Download-URL wird aus `MANAGE_PUBLIC_URL` gebildet, **nicht** aus dem `Host`-Header. Die Vorgängerlösung leitete sie aus `HTTP_HOST` ab, also aus einem vom Client kontrollierten Wert. ## Backups ```text storage/backups/ index.json Metadaten aller Backups /backup-YYYYmmdd-HHMMSS[-N].zip ``` Der Server prüft nach dem Speichern die Prüfsumme erneut und löscht die Datei bei Abweichung. Ein vorhandener Dateiname wird nie überschrieben. ### Zwei Aufbewahrungsstufen Bei aktivem S3-Archiv arbeitet die lokale Platte als schneller Zwischenspeicher und der Bucket als vollständiges Archiv: - **S3**: behält die neuesten `s3_retention` Sicherungen je Instanz (Standard 365). - **Lokal**: behält die neuesten `retention` Sicherungen (Standard 30), löscht eine Datei aber **nie**, solange ihr S3-Upload noch aussteht. Ist S3 nicht erreichbar, wachsen die lokalen Kopien also über die Aufbewahrung hinaus, statt die einzige Kopie zu verlieren. Fehlgeschlagene S3-Uploads werden beim nächsten Upload derselben Instanz oder über die Schaltfläche in der Oberfläche nachgeholt. S3-Fehler lassen einen Client-Upload nie fehlschlagen: Die lokale Kopie liegt bereits vor. Protokolliert werden sie in `storage/logs/s3.log`. ## Speicherung Ausschließlich flache Dateien, kein Datenbankserver. Alle Schreibvorgänge laufen über `manageWriteJsonFile()`: erst in eine `.tmp`-Datei, dann `rename()`. Damit kann ein abgebrochener Request keinen halb geschriebenen Index hinterlassen. Bekannte Grenze: Gleichzeitige Uploads derselben Instanz können sich beim Schreiben von `index.json` überschneiden. Bei einer Handvoll Instanzen mit nächtlichen Backups ist das praktisch ausgeschlossen; bei vielen gleichzeitigen Uploads wäre eine Sperre nötig. ## Protokolle | Datei | Inhalt | |---|---| | `storage/logs/access.log` | JSONL: Anmeldungen, Releases, empfangene Backups, Downloads | | `storage/logs/error.log` | JSONL: fehlgeschlagene Anmeldungen, abgelehnte Uploads, interne Fehler | | `storage/logs/s3.log` | Klartext: S3-Diagnose mit Status, Umleitungen und Request-ID | Die JSONL-Protokolle rotieren nach `MANAGE_LOG_MAX_BYTES` und werden nach `MANAGE_LOG_MAX_AGE_SECONDS` entfernt. ## Weiter - [SERVER_SETUP](SERVER_SETUP.md) – Installation - [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – Instanzen und Tokens - [RELEASING](RELEASING.md) – Releases veröffentlichen