Sfoglia il codice sorgente

changed docs to english

Medowar 1 mese fa
parent
commit
d2870b6772
33 ha cambiato i file con 1857 aggiunte e 1725 eliminazioni
  1. 110 102
      README.md
  2. 15 15
      client-docs/api.php
  3. 101 0
      client-docs/content/00_OVERVIEW.md
  4. 0 90
      client-docs/content/00_UEBERBLICK.md
  5. 28 27
      client-docs/content/50_API.md
  6. 20 19
      client-docs/content/llms-intro.md
  7. 14 14
      client-docs/inc/handbook.php
  8. 44 44
      client-docs/index.php
  9. 18 18
      client-docs/llms.php
  10. 54 54
      client-docs/openapi.json
  11. 89 88
      client-package/README.md
  12. 46 41
      client-package/docs/01_QUICKSTART.md
  13. 73 70
      client-package/docs/02_INTEGRATION.md
  14. 77 75
      client-package/docs/03_CONFIG_REFERENCE.md
  15. 79 75
      client-package/docs/04_FUNCTION_API.md
  16. 103 99
      client-package/docs/05_BACKUP_SOURCES.md
  17. 85 80
      client-package/docs/06_UPDATE_PACKAGING.md
  18. 82 80
      client-package/docs/07_POST_UPDATE_HOOKS.md
  19. 64 60
      client-package/docs/08_PROTOCOL.md
  20. 202 137
      client-package/docs/09_TROUBLESHOOTING.md
  21. 99 94
      client-package/docs/10_SECURITY.md
  22. 10 10
      client-package/docs/index.php
  23. 15 16
      client-package/examples/after-update.php
  24. 19 19
      client-package/examples/cron/manage-client.cron
  25. 8 8
      client-package/examples/flat-file-project/migrations/2026-08-20-01-add-category-id.php
  26. 13 13
      client-package/examples/integration-snippet.php
  27. 5 5
      client-package/examples/mysql-project/migrations/2026-08-20-01-add-orders-index.php
  28. 79 77
      docs/ARCHITECTURE.md
  29. 76 74
      docs/CONFIG_REFERENCE.md
  30. 75 71
      docs/INSTANCE_MANAGEMENT.md
  31. 61 59
      docs/RELEASING.md
  32. 83 81
      docs/SERVER_SETUP.md
  33. 10 10
      docs/index.php

+ 110 - 102
README.md

@@ -1,132 +1,140 @@
 # Manage
 
-Update- und Backup-Server für PHP-Projekte, plus ein weitergebbares Client-Paket.
+Update and backup server for PHP projects, plus a redistributable client package.
 
-## Zweck
+## Purpose
 
-Mehrere eigenständige PHP-Projekte brauchen dieselben zwei Dinge: eine Möglichkeit,
-neue Versionen einzuspielen, und regelmäßige Sicherungen der Betriebsdaten. `manage`
-stellt beides zentral bereit, statt es in jedem Projekt erneut zu bauen.
+Multiple independent PHP projects need the same two things: a way to roll out
+new versions, and regular backups of operational data. `manage` provides both
+centrally instead of building them into each project again.
 
-Eine Installation betreut **ein Produkt** mit einer überschaubaren Zahl von
-Instanzen. Für ein weiteres Produkt wird `manage` erneut ausgerollt.
+One installation serves **one product** with a manageable number of
+instances. For another product, `manage` is deployed again.
 
-## Kernfunktionen
+## Core features
 
-- Release-Verwaltung: Pakete hochladen, Prüfsummen serverseitig berechnen, aktuelles
-  Release festlegen
-- Backup-Empfang je Instanz, mit zwei Aufbewahrungsstufen und optionalem S3-Archiv
-- Instanzregister mit Token-Authentifizierung, Deaktivierung und Token-Erneuerung
-- Übersicht über installierte Versionen, letzte Sicherungen und offene Migrationen
-- Client-Paket mit Kommandozeile, fertiger Adminoberfläche und Funktions-API
+- Release management: upload packages, checksums computed server-side, set
+  the current release
+- Backup reception per instance, with two retention tiers and an optional S3
+  archive
+- Instance registry with token authentication, deactivation and token
+  rotation
+- Overview of installed versions, latest backups and pending migrations
+- Client package with a command line, a ready-made admin UI and a function
+  API
 
-## Voraussetzungen
+## Requirements
 
 - PHP 8.x
-- ein Webserver (Apache mit aktiver `.htaccess` oder nginx mit gesetzten Sperren)
-- Schreibrechte auf `storage/`
+- a web server (Apache with `.htaccess` enabled, or nginx with the
+  equivalent locks set)
+- write access to `storage/`
 
-Keine Datenbank, kein Composer, kein Build-Schritt, keine externen Bibliotheken.
+No database, no Composer, no build step, no external libraries.
 
-## Wichtige Dateien
+## Key files
 
-- Konfiguration: `config.php` (aus `config.sample.php`)
-- Oberfläche: `admin/`
-- Schnittstelle für Clients: `api/v1/`
-- Gemeinsame Bibliothek: `includes/`
-- Zustand: `storage/` (nicht öffentlich)
-- Weitergebbares Client-Paket: `client-package/`
+- Configuration: `config.php` (from `config.sample.php`)
+- UI: `admin/`
+- Client interface: `api/v1/`
+- Shared library: `includes/`
+- State: `storage/` (not public)
+- Redistributable client package: `client-package/`
 
-## Einrichtung
+## Setup
 
-1. `config.sample.php` nach `config.php` kopieren.
-2. Passwort-Hash erzeugen und als `MANAGE_ADMIN_PASSWORD_HASH` eintragen:
+1. Copy `config.sample.php` to `config.php`.
+2. Generate a password hash and enter it as `MANAGE_ADMIN_PASSWORD_HASH`:
    `php -r 'echo password_hash("…", PASSWORD_DEFAULT), PHP_EOL;'`
-3. `MANAGE_PUBLIC_URL` auf die absolute Adresse dieser Installation setzen.
-4. `MANAGE_PRODUCT_NAME` und `MANAGE_PACKAGE_PREFIX` auf das betreute Produkt setzen.
-5. Schreibrechte auf `storage/` sicherstellen.
-6. `admin/login.php` öffnen und unter **Einstellungen → Diagnose** prüfen, dass alles
-   Wesentliche stimmt.
+3. Set `MANAGE_PUBLIC_URL` to the absolute address of this installation.
+4. Set `MANAGE_PRODUCT_NAME` and `MANAGE_PACKAGE_PREFIX` to the product being
+   served.
+5. Make sure `storage/` is writable.
+6. Open `admin/login.php` and check under **Settings → Diagnostics** that
+   everything essential checks out.
 
-Ausführlich: [docs/SERVER_SETUP.md](docs/SERVER_SETUP.md).
+Details: [docs/SERVER_SETUP.md](docs/SERVER_SETUP.md).
 
-## Ein Projekt anbinden
+## Connecting a project
 
-1. Im Manage-Server unter **Instanzen** eine Instanz anlegen und das einmalig
-   angezeigte Token notieren.
-2. Client-Paket bauen und übergeben:
+1. In the Manage server under **Instances**, create an instance and note the
+   token shown once.
+2. Build and hand over the client package:
    `./scripts/build-client-package.sh --server-url https://manage.example.org`
-3. Im Projekt: `manage-client/` einkopieren, `config.php` anlegen,
-   `php manage-client/bin/manage-client.php status` ausführen.
+3. In the project: copy in `manage-client/`, create `config.php`, run
+   `php manage-client/bin/manage-client.php status`.
 
-Die vollständige Anleitung dafür liegt **im Paket selbst**
-([client-package/README.md](client-package/README.md)), damit die empfangende Seite
-keinen Zugriff auf dieses Repository braucht.
+The complete guide for this lives **inside the package itself**
+([client-package/README.md](client-package/README.md)), so the receiving side
+needs no access to this repository.
 
-## Dokumentation
+## Documentation
 
-Serverseitig, in `docs/`:
+Server-side, in `docs/`:
 
-- [ARCHITECTURE.md](docs/ARCHITECTURE.md) – Aufbau, Datenfluss, Speicherformate
-- [SERVER_SETUP.md](docs/SERVER_SETUP.md) – Installation, Webserver, Limits, S3
-- [INSTANCE_MANAGEMENT.md](docs/INSTANCE_MANAGEMENT.md) – Instanzen, Tokens, Übergabe des Client-Pakets
-- [RELEASING.md](docs/RELEASING.md) – Pakete bauen und veröffentlichen
-- [CONFIG_REFERENCE.md](docs/CONFIG_REFERENCE.md) – alle Serverkonstanten
+- [ARCHITECTURE.md](docs/ARCHITECTURE.md) – structure, data flow, storage formats
+- [SERVER_SETUP.md](docs/SERVER_SETUP.md) – installation, web server, limits, S3
+- [INSTANCE_MANAGEMENT.md](docs/INSTANCE_MANAGEMENT.md) – instances, tokens, handing over the client package
+- [RELEASING.md](docs/RELEASING.md) – building and publishing packages
+- [CONFIG_REFERENCE.md](docs/CONFIG_REFERENCE.md) – every server constant
 
-Für Projekte, in `client-package/docs/`: Quickstart, Integration, Konfiguration,
-Funktions-API, Backup-Quellen, Paketbau, Post-Update-Hooks, Protokoll, Fehlersuche,
-Sicherheit.
+For projects, in `client-package/docs/`: quickstart, integration,
+configuration, function API, backup sources, packaging, post-update hooks,
+protocol, troubleshooting, security.
 
-Im Browser lesbar über `docs/index.php` beziehungsweise `client-package/docs/index.php`
-(Markdown wird mit dem mitgelieferten [marked](https://marked.js.org/) gerendert).
+Readable in the browser via `docs/index.php` and
+`client-package/docs/index.php` respectively (Markdown rendered with the
+bundled [marked](https://marked.js.org/)).
 
-### Öffentliches Client-Handbuch
+### Public client handbook
 
-`client-docs/` veröffentlicht den Inhalt von `client-package/` über HTTP: jedes Kapitel
-und jede Quelldatei als eigene Seite, dazu die Schnittstelle als OpenAPI. Anders als
-`admin/` und `api/v1/` ist dieser Ordner ohne Anmeldung erreichbar – er ist dafür da,
-weitergegeben zu werden, damit ein Projekt eingebunden werden kann, ohne vorher ein ZIP
-zu verschicken.
+`client-docs/` publishes the content of `client-package/` over HTTP: every
+chapter and every source file as its own page, plus the interface as
+OpenAPI. Unlike `admin/` and `api/v1/`, this folder is reachable without
+logging in — it exists to be handed out, so a project can be integrated
+without shipping a ZIP first.
 
-| Adresse | Inhalt |
+| Address | Content |
 |---|---|
-| `client-docs/` | Übersicht, von dort ein Kapitel oder eine Quelldatei |
-| `client-docs/index.php?doc=01_QUICKSTART` | ein Kapitel |
-| `client-docs/index.php?code=manage-client/lib/updater.php` | eine Quelldatei |
-| `client-docs/api.php` | Protokoll v1 in Swagger UI |
-| `client-docs/openapi.php` | das OpenAPI-Dokument allein, für Codegeneratoren |
-
-Für Programme gibt es dieselben Seiten als reines Markdown unter `llms.php`, mit
-`llms.php?doc=…` und `llms.php?code=…` daneben. `client-docs/llms.php` ist das
-Verzeichnis: es nennt jede Seite mit einem Satz Beschreibung und ihrer Größe und führt
-für die üblichen Aufgaben – Client einbauen, nur Backups, nur Updater, eigenen Client
-schreiben, Fehlersuche – die kurze Liste der Seiten auf, die dafür reicht. Das ist der
-Zweck der Aufteilung: nur laden, was gebraucht wird. Diese Adresse bekommt ein LLM.
-Apache beantwortet zusätzlich `llms.txt`; kanonisch ist `llms.php`, weil das ohne
-`mod_rewrite` auskommt.
-
-Die Seiten für Menschen rendern ihr Markdown im Browser und tragen im Quelltext einen
-Kommentar und ein unsichtbares Element, die auf die Markdown-Fassung derselben Seite
-verweisen – ein Programm, das versehentlich dort landet, findet den Weg.
-
-Der Inhalt wird bei jedem Aufruf aus `client-package/` gelesen; es gibt nichts zu bauen
-und nichts nachzuziehen. `manage-client/config.php` ist von der Veröffentlichung
-ausgenommen, damit ein lokal angelegtes Token nicht öffentlich wird.
-
-marked und Swagger UI liegen unter `client-docs/assets/` im Repository. Es wird kein
-fremder Server kontaktiert, auch nicht für Schriften oder Symbole.
-
-## Was bewusst fehlt
-
-- **Keine Wiederherstellung.** Backups werden erstellt, übertragen und zum Download
-  bereitgestellt, aber nie automatisch zurückgespielt. Ein Update sichert die
-  überschriebenen Dateien, kann sie aber nicht zurückholen.
-- **Keine automatischen Updates.** Der Server bietet an, die Instanz entscheidet.
-- **Keine Signatur der Pakete.** Prüfsumme und Paket kommen vom selben Server; die
-  Absicherung ist TLS plus Token. Der Manage-Server muss entsprechend geschützt sein.
-- **Keine Kanäle pro Instanz.** Es gibt ein aktuelles Release für alle Instanzen
-  eines Servers.
-
-## Hinweise
-
-- Kein automatisiertes Test-/CI-Setup vorgesehen.
+| `client-docs/` | overview, from there a chapter or a source file |
+| `client-docs/index.php?doc=01_QUICKSTART` | one chapter |
+| `client-docs/index.php?code=manage-client/lib/updater.php` | one source file |
+| `client-docs/api.php` | protocol v1 in Swagger UI |
+| `client-docs/openapi.php` | the OpenAPI document alone, for code generators |
+
+For programs, the same pages exist as plain Markdown under `llms.php`, with
+`llms.php?doc=…` and `llms.php?code=…` alongside. `client-docs/llms.php` is
+the index: it names every page with a one-sentence description and its size,
+and for the usual tasks — integrate the client, backups only, updater only,
+write your own client, troubleshooting — lists the short set of pages that
+suffices. That is the point of the split: load only what is needed. This is
+the address to hand to an LLM. Apache additionally answers `llms.txt`;
+`llms.php` is canonical, because that works without `mod_rewrite`.
+
+The pages for humans render their Markdown in the browser and carry a
+comment and an invisible element in the source that point to the Markdown
+version of the same page — a program that lands there by accident finds its
+way.
+
+The content is read from `client-package/` on every request; there is
+nothing to build and nothing to regenerate. `manage-client/config.php` is
+excluded from publication, so a locally created token never becomes public.
+
+marked and Swagger UI live under `client-docs/assets/` in the repository. No
+external server is contacted, not even for fonts or icons.
+
+## What is deliberately missing
+
+- **No restore.** Backups are created, transferred and made available for
+  download, but never played back automatically. An update backs up the
+  files it overwrites, but cannot bring them back.
+- **No automatic updates.** The server offers; the instance decides.
+- **No package signing.** The checksum and the package come from the same
+  server; the safeguard is TLS plus the token. The Manage server must be
+  secured accordingly.
+- **No per-instance channels.** There is one current release for all
+  instances of a server.
+
+## Notes
+
+- No automated test/CI setup is provided.

+ 15 - 15
client-docs/api.php

@@ -16,50 +16,50 @@ $base = handbookBaseUrl();
 $self = handbookSelfUrl();
 ?>
 <!DOCTYPE html>
-<html lang="de">
+<html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>API-Referenz – Manage Client</title>
+    <title>API Reference – Manage Client</title>
     <link rel="stylesheet" href="assets/docs.css">
     <link rel="stylesheet" href="assets/swagger-ui.css">
     <link rel="alternate" type="application/json" href="openapi.php"
-          title="Dieselbe Beschreibung als OpenAPI-Dokument">
+          title="The same description as an OpenAPI document">
 </head>
 <body>
 <!--
-    Hinweis für LLMs und andere Programme / note for LLMs and other programs:
+    Note for LLMs and other programs:
 
-    Diese Seite ist Swagger UI und baut sich erst im Browser auf; im HTML steht
-    nichts Verwertbares. Dieselbe Beschreibung als OpenAPI 3.1:
+    This page is Swagger UI and only builds itself in the browser; there is
+    nothing usable in the HTML. The same description as OpenAPI 3.1:
 
         <?php echo $self . "/openapi.php\n"; ?>
 
-    Verzeichnis aller Seiten der Dokumentation, mit Kurzbeschreibung und Größe:
+    Index of every documentation page, with a short description and size:
 
         <?php echo $self . "/llms.php\n"; ?>
 -->
 <p class="llm-only">
-    Für LLMs: die Schnittstelle als OpenAPI-Dokument unter <?php echo handbookEscape($self); ?>/openapi.php –
-    Verzeichnis aller Seiten unter <?php echo handbookEscape($self); ?>/llms.php
+    For LLMs: the interface as an OpenAPI document at <?php echo handbookEscape($self); ?>/openapi.php –
+    index of every page at <?php echo handbookEscape($self); ?>/llms.php
 </p>
 <header class="docs-header">
     <div class="docs-header-inner">
         <a class="docs-brand" href="index.php">Manage Client</a>
         <nav class="docs-header-links">
-            <a href="index.php">Dokumentation</a>
+            <a href="index.php">Documentation</a>
             <a href="openapi.php">OpenAPI</a>
-            <a href="llms.php">Für LLMs</a>
+            <a href="llms.php">For LLMs</a>
         </nav>
     </div>
 </header>
 <div class="swagger-frame">
     <div class="docs-note">
-        <p><strong>Protokoll v1</strong> – die Schnittstelle zwischen einer Projektinstanz und
+        <p><strong>Protocol v1</strong> – the interface between a project instance and
             <code><?php echo handbookEscape($base); ?></code>.</p>
-        <p>„Try it out“ spricht diese Installation an und braucht eine gültige Instanz samt
-            Token; ohne beides antwortet jeder Endpunkt mit <code>401</code>. Die Kennung wird
-            über <em>Authorize</em> gesetzt.</p>
+        <p>"Try it out" talks to this installation and needs a valid instance plus
+            token; without both, every endpoint responds with <code>401</code>. The id is
+            set via <em>Authorize</em>.</p>
     </div>
     <div id="swagger-ui"></div>
 </div>

+ 101 - 0
client-docs/content/00_OVERVIEW.md

@@ -0,0 +1,101 @@
+# Overview
+
+The Manage client brings two things into a standalone PHP project: a way
+to install new versions, and regular backups of operational data.
+
+This is the published version of the `client-package/` folder from the
+Manage repository: {{DOC_COUNT}} chapters and {{CODE_COUNT}} source files,
+each on its own page, plus the interface to the server as OpenAPI.
+Everything is read fresh from the repository on every request; what's here
+is the current state.
+
+## What the client does
+
+Three operations, all through the same functions, whether they're
+triggered from the command line, from the bundled UI, or directly from the
+project.
+
+**Update.** `manageUpdateCheck()` fetches the manifest from the server and
+compares versions. `manageUpdateApply()` downloads the package, checks size
+and SHA-256, extracts it into a working directory while checking every ZIP
+entry against path traversal, requires at least one sanity path in the
+archive, then copies file by file into the project and saves every
+overwritten file first into a timestamped folder. Protected paths are
+skipped. After that, the migrations from the package run, along with an
+optional callback.
+
+**Backup.** `manageBackupCreate()` collects the configured sources —
+globs, directories, individual files, each with a target prefix in the
+archive —, optionally appends a MySQL dump, writes a ZIP, applies local
+retention, and uploads the archive to the Manage server as well as to
+optional extra targets (S3, SFTP, a custom endpoint). A lock file prevents
+concurrent runs. A failed upload does not invalidate the local archive.
+
+**Heartbeat.** `manageHeartbeatSend()` reports version, PHP version, free
+disk space, pending migrations and the time of the last backup. The
+response includes the update information as a side effect.
+
+## Integrate, don't rebuild
+
+The client is cut so it can be copied as a folder into a foreign project.
+It has no dependencies, no build step, and no assumptions about the target
+project beyond PHP. Rebuilding it costs time and loses details already
+solved here — checksums, protected paths, lock files, resuming after
+partial failure.
+
+1. Take `manage-client/` from the source part into the target project,
+   paths and content unchanged.
+2. Copy `manage-client/config.sample.php` to `manage-client/config.php` and
+   set `MANAGE_SERVER_URL`, `MANAGE_INSTANCE`, `MANAGE_TOKEN` and
+   `MANAGE_BACKUP_SOURCES`. The first three values come from the Manage
+   server.
+3. Check `MANAGE_APP_ROOT`, `MANAGE_VERSION_FILE` and
+   `MANAGE_VERSION_CONSTANT` against the target project's actual layout.
+   These are the only places where the client has to know anything about
+   the project.
+4. Extend `MANAGE_UPDATE_PROTECTED_PATHS` with anything an update must
+   never overwrite, and `MANAGE_UPDATE_SANITY_PATHS` with a file present in
+   every valid release.
+5. Wire in one of the three entry points: the command line for cron,
+   `ui/panel.php` for the admin area, or the function API for custom pages.
+   See [02_INTEGRATION.md](02_INTEGRATION.md).
+6. Check with `php manage-client/bin/manage-client.php status`.
+
+Details in [01_QUICKSTART.md](01_QUICKSTART.md).
+
+## Writing your own client
+
+Anyone who has to implement this in a different language will find the
+binding description of the interface in [08_PROTOCOL.md](08_PROTOCOL.md)
+and the OpenAPI document. Three points decide whether the implementation is
+safe:
+
+- Check SHA-256 **and** size of every package against the manifest and
+  abort on mismatch, before anything is extracted.
+- When extracting, check every entry against path traversal (`..`,
+  absolute paths).
+- When deploying, skip the protected paths — otherwise the first update
+  overwrites the configuration and the operational data.
+
+## Limits
+
+These points are missing deliberately. Anyone expecting them is building
+on a false assumption.
+
+- **No restore.** Backups are created, transferred and made available for
+  download, but never played back automatically. An update backs up the
+  files it overwrites, but cannot bring them back.
+- **No automatic updates.** The server offers; the instance decides.
+- **No package signing.** The checksum and the package come from the same
+  server; the safeguard is TLS plus the token.
+- **No maintenance page.** Deployment overwrites files while the
+  application is live.
+- **No backup before the update.** That's deliberately a visible line in
+  the project rather than something built in; see
+  [04_FUNCTION_API.md](04_FUNCTION_API.md).
+
+## Requirements
+
+PHP 8.0 or newer, the `zip` extension for updates, write access to the
+project's data directory. No Composer, no build step, no external
+libraries.

+ 0 - 90
client-docs/content/00_UEBERBLICK.md

@@ -1,90 +0,0 @@
-# Überblick
-
-Der Manage-Client bringt zwei Dinge in ein eigenständiges PHP-Projekt: eine Möglichkeit,
-neue Versionen einzuspielen, und regelmäßige Sicherungen der Betriebsdaten.
-
-Dies ist die veröffentlichte Fassung des Ordners `client-package/` aus dem
-Manage-Repository: {{DOC_COUNT}} Kapitel und {{CODE_COUNT}} Quelldateien, jede auf einer
-eigenen Seite, dazu die Schnittstelle zum Server als OpenAPI. Alles wird bei jedem Aufruf
-frisch aus dem Repository gelesen; was hier steht, ist der aktuelle Stand.
-
-## Was der Client tut
-
-Drei Vorgänge, alle über dieselben Funktionen, egal ob sie aus der Kommandozeile, aus der
-mitgelieferten Oberfläche oder direkt aus dem Projekt ausgelöst werden.
-
-**Update.** `manageUpdateCheck()` holt das Manifest vom Server und vergleicht die
-Versionen. `manageUpdateApply()` lädt das Paket, prüft Größe und SHA-256, entpackt es in
-ein Arbeitsverzeichnis, prüft dabei jeden ZIP-Eintrag gegen Pfadausbruch, verlangt
-mindestens einen Sanity-Pfad im Archiv, kopiert dann Datei für Datei in das Projekt und
-legt jede überschriebene Datei vorher in einem Zeitstempel-Ordner ab. Geschützte Pfade
-werden übersprungen. Danach laufen die Migrationen aus dem Paket und ein optionaler
-Callback.
-
-**Backup.** `manageBackupCreate()` sammelt die konfigurierten Quellen – Globs,
-Verzeichnisse, Einzeldateien, jeweils mit Zielpräfix im Archiv –, hängt optional einen
-MySQL-Dump an, schreibt ein ZIP, wendet die lokale Aufbewahrung an und lädt das Archiv
-zum Manage-Server sowie zu optionalen Zusatzzielen (S3, SFTP, eigener Endpunkt). Eine
-Sperrdatei verhindert gleichzeitige Läufe. Ein fehlgeschlagener Upload macht das lokale
-Archiv nicht ungültig.
-
-**Heartbeat.** `manageHeartbeatSend()` meldet Version, PHP-Version, freien Speicher,
-offene Migrationen und den Zeitpunkt des letzten Backups. Die Antwort enthält nebenbei
-die Update-Information.
-
-## Einbinden statt nachbauen
-
-Der Client ist so geschnitten, dass er als Ordner in ein fremdes Projekt kopiert wird. Er
-hat keine Abhängigkeiten, keinen Build-Schritt und keine Annahmen über das Zielprojekt
-außer PHP. Nachbauen kostet Zeit und verliert Details, die hier bereits gelöst sind –
-Prüfsummen, geschützte Pfade, Sperrdateien, Wiederanlauf nach Teilfehlern.
-
-1. `manage-client/` aus dem Quellcode-Teil in das Zielprojekt übernehmen, Pfade und
-   Inhalte unverändert.
-2. `manage-client/config.sample.php` nach `manage-client/config.php` kopieren und
-   `MANAGE_SERVER_URL`, `MANAGE_INSTANCE`, `MANAGE_TOKEN` sowie `MANAGE_BACKUP_SOURCES`
-   setzen. Die ersten drei Werte kommen aus dem Manage-Server.
-3. `MANAGE_APP_ROOT`, `MANAGE_VERSION_FILE` und `MANAGE_VERSION_CONSTANT` gegen den
-   tatsächlichen Aufbau des Zielprojekts prüfen. Das sind die einzigen Stellen, an denen
-   der Client etwas über das Projekt wissen muss.
-4. `MANAGE_UPDATE_PROTECTED_PATHS` um alles ergänzen, was ein Update niemals überschreiben
-   darf, und `MANAGE_UPDATE_SANITY_PATHS` um eine Datei, die in jedem gültigen Release
-   vorkommt.
-5. Einen der drei Einstiegspunkte einbinden: Kommandozeile für Cron, `ui/panel.php` für
-   den Adminbereich, oder die Funktions-API für eigene Seiten. Siehe
-   [02_INTEGRATION.md](02_INTEGRATION.md).
-6. Mit `php manage-client/bin/manage-client.php status` prüfen.
-
-Ausführlich in [01_QUICKSTART.md](01_QUICKSTART.md).
-
-## Einen eigenen Client schreiben
-
-Wer in einer anderen Sprache implementieren muss, findet in
-[08_PROTOCOL.md](08_PROTOCOL.md) und im OpenAPI-Dokument die verbindliche Beschreibung
-der Schnittstelle. Drei Punkte entscheiden darüber, ob die Implementierung sicher ist:
-
-- SHA-256 **und** Größe jedes Pakets gegen das Manifest prüfen und bei Abweichung
-  abbrechen, bevor irgendetwas entpackt wird.
-- Beim Entpacken jeden Eintrag gegen Pfadausbruch prüfen (`..`, absolute Pfade).
-- Beim Ausrollen die geschützten Pfade auslassen, sonst überschreibt das erste Update die
-  Konfiguration und die Betriebsdaten.
-
-## Grenzen
-
-Diese Punkte fehlen bewusst. Wer sie erwartet, baut auf einer falschen Annahme auf.
-
-- **Keine Wiederherstellung.** Backups werden erstellt, übertragen und zum Download
-  bereitgestellt, aber nie automatisch zurückgespielt. Ein Update sichert die
-  überschriebenen Dateien, kann sie aber nicht zurückholen.
-- **Keine automatischen Updates.** Der Server bietet an, die Instanz entscheidet.
-- **Keine Signatur der Pakete.** Prüfsumme und Paket kommen vom selben Server; die
-  Absicherung ist TLS plus Token.
-- **Keine Wartungsseite.** Das Ausrollen überschreibt Dateien im laufenden Betrieb.
-- **Kein Backup vor dem Update.** Das ist bewusst eine sichtbare Zeile im Projekt und
-  nicht eingebaut, siehe [04_FUNCTION_API.md](04_FUNCTION_API.md).
-
-## Voraussetzungen
-
-PHP 8.0 oder neuer, die Erweiterung `zip` für Updates, Schreibrechte auf dem
-Datenverzeichnis des Projekts. Kein Composer, kein Build-Schritt, keine externen
-Bibliotheken.

+ 28 - 27
client-docs/content/50_API.md

@@ -1,38 +1,39 @@
-# HTTP-API
+# HTTP API
 
-Die Schnittstelle zwischen Client und Manage-Server, Protokoll v1. Wer den
-mitgelieferten Client übernimmt, braucht dieses Kapitel nicht – es ist für eigene
-Clients, für Debugging und für die Fehlersuche mit `curl` gedacht.
+The interface between client and Manage server, protocol v1. Anyone using
+the bundled client doesn't need this chapter — it's for custom clients, for
+debugging, and for troubleshooting with `curl`.
 
-Basis-URL dieser Installation: `{{BASE_URL}}/api/v1`
+Base URL of this installation: `{{BASE_URL}}/api/v1`
 
-## Die vier Endpunkte
+## The four endpoints
 
-| Methode | Pfad | Zweck |
+| Method | Path | Purpose |
 |---|---|---|
-| `GET` | `/manifest.php` | Welches Release soll installiert werden |
-| `GET` | `/package.php?version=vX.Y.Z` | Das Release-ZIP |
-| `POST` | `/backup.php` | Backup-Archiv hochladen (`multipart/form-data`) |
-| `POST` | `/heartbeat.php` | Status melden, Update-Information erhalten |
+| `GET` | `/manifest.php` | which release should be installed |
+| `GET` | `/package.php?version=vX.Y.Z` | the release ZIP |
+| `POST` | `/backup.php` | upload a backup archive (`multipart/form-data`) |
+| `POST` | `/heartbeat.php` | report status, receive update information |
 
-Jede Anfrage trägt zwei Header:
+Every request carries two headers:
 
 ```http
-X-Manage-Instance: meinprojekt-prod
+X-Manage-Instance: myproject-prod
 X-Manage-Token:    e4032c4dc51e9100…
 ```
 
-Es gibt keine Sitzung, kein Cookie und kein gemeinsames Passwort. Der Server speichert
-nur den SHA-256-Hash des Tokens und vergleicht in konstanter Zeit.
-
-## Wo was steht
-
-- **Beschreibung mit Beispielaufrufen**, Fehlerfällen und den Regeln, an die sich ein
-  eigener Client halten muss: [08_PROTOCOL.md](08_PROTOCOL.md).
-- **Maschinenlesbar**, als OpenAPI 3.1: <{{SELF_URL}}/openapi.php>. Enthält Schemata für
-  alle Antworten, die Fehlercodes und die Muster für Version, Dateiname und Prüfsumme.
-- **Zum Nachschlagen und Ausprobieren** im Browser:
-  [Swagger UI]({{SELF_URL}}/api.php). „Try it out“ spricht diese Installation an und
-  braucht eine gültige Instanz samt Token.
-- **Als Referenzimplementierung**: `manage-client/lib/client.php` baut die Anfragen,
-  `lib/updater.php` und `lib/backup.php` benutzen sie.
+There is no session, no cookie and no shared password. The server stores
+only the SHA-256 hash of the token and compares it in constant time.
+
+## Where to find what
+
+- **Description with example calls**, error cases, and the rules a custom
+  client must follow: [08_PROTOCOL.md](08_PROTOCOL.md).
+- **Machine-readable**, as OpenAPI 3.1: <{{SELF_URL}}/openapi.php>.
+  Contains schemas for every response, the error codes, and the patterns
+  for version, filename and checksum.
+- **To browse and try out** in the browser:
+  [Swagger UI]({{SELF_URL}}/api.php). "Try it out" talks to this
+  installation and needs a valid instance plus token.
+- **As a reference implementation**: `manage-client/lib/client.php` builds
+  the requests; `lib/updater.php` and `lib/backup.php` use them.

+ 20 - 19
client-docs/content/llms-intro.md

@@ -1,26 +1,27 @@
-# Manage Client – Verzeichnis
+# Manage Client – Index
 
-Update- und Backup-Funktionalität für eigenständige PHP-Projekte: ein PHP-Client, der
-Releases von einem zentralen Server holt, prüft und einspielt, Backups der Betriebsdaten
-erstellt und hochlädt und den Zustand der Installation meldet. Keine Abhängigkeiten, kein
-Composer, kein Build-Schritt.
+Update and backup functionality for standalone PHP projects: a PHP client
+that fetches, verifies and installs releases from a central server, creates
+and uploads backups of operational data, and reports the installation's
+status. No dependencies, no Composer, no build step.
 
-Dies ist das Verzeichnis für Programme. Jede Seite ist reines Markdown und einzeln
-abrufbar. Insgesamt {{PAGE_COUNT}} Seiten, zusammen {{TOTAL_SIZE}} – **laden Sie nicht
-alles.** Der Abschnitt *Wofür welche Seiten* nennt für die üblichen Aufgaben die kurze
-Liste, die dafür reicht; die Größenangabe hinter jeder Seite hilft beim Abschätzen.
+This is the index for programs. Every page is plain Markdown and
+individually retrievable. {{PAGE_COUNT}} pages in total, {{TOTAL_SIZE}}
+combined — **don't load all of it.** The *What each page is for* section
+names, for the usual tasks, the short list that suffices; the size shown
+after each page helps with estimating.
 
-Adressen:
+Addresses:
 
-- Verzeichnis (diese Seite): {{SELF_URL}}/llms.php (auf Apache auch {{SELF_URL}}/llms.txt)
-- Eine Seite: {{SELF_URL}}/llms.php?doc=KEY beziehungsweise {{SELF_URL}}/llms.php?code=PFAD
-- Dieselben Inhalte für Menschen: {{SELF_URL}}/index.php
-- Der Manage-Server, den der Client anspricht: {{BASE_URL}}
+- Index (this page): {{SELF_URL}}/llms.php (also {{SELF_URL}}/llms.txt on Apache)
+- One page: {{SELF_URL}}/llms.php?doc=KEY or {{SELF_URL}}/llms.php?code=PATH
+- The same content for humans: {{SELF_URL}}/index.php
+- The Manage server this client talks to: {{BASE_URL}}
 
-Die Quellcode-Seiten geben den jeweiligen Dateiinhalt vollständig und unverändert wieder,
-in einem Codeblock unter dem Pfad, unter dem die Datei im Projekt liegen muss. Sie sind
-zum Übernehmen gedacht, nicht als Vorlage zum Umschreiben.
+The source-code pages reproduce each file's content completely and
+unchanged, in a code block under the path where the file must live in the
+project. They're meant to be adopted, not used as a template to rewrite.
 
-Nicht enthalten: `manage-client/config.php`. Diese Datei trägt das Zugangstoken einer
-konkreten Instanz und wird nicht veröffentlicht. Die Vorlage dafür ist
+Not included: `manage-client/config.php`. This file carries a specific
+instance's access token and is not published. Its template is
 `manage-client/config.sample.php`.

+ 14 - 14
client-docs/inc/handbook.php

@@ -160,7 +160,7 @@ function handbookPages(): array
     $pages = [];
 
     // The two authored chapters frame the material that comes from the package.
-    foreach (["00_UEBERBLICK", "50_API"] as $key) {
+    foreach (["00_OVERVIEW", "50_API"] as $key) {
         $path = HANDBOOK_CONTENT_DIR . "/" . $key . ".md";
         if (!is_file($path)) {
             continue;
@@ -248,7 +248,7 @@ function handbookWeight(array $page): int
 {
     if ($page["kind"] === "doc") {
         return match (true) {
-            $page["key"] === "00_UEBERBLICK" => 0,
+            $page["key"] === "00_OVERVIEW" => 0,
             $page["key"] === "README" => 1,
             $page["key"] === "50_API" => 3,
             default => 2,
@@ -579,10 +579,10 @@ function handbookRecipes(): array
 {
     return [
         [
-            "title" => "Update- und Backup-Funktion in ein Projekt einbauen",
-            "note" => "Der übliche Fall. Das Paket wird übernommen, nicht nachgebaut.",
+            "title" => "Adding update and backup functionality to a project",
+            "note" => "The usual case. The package gets adopted, not rebuilt.",
             "pages" => [
-                ["doc", "00_UEBERBLICK"],
+                ["doc", "00_OVERVIEW"],
                 ["doc", "01_QUICKSTART"],
                 ["doc", "02_INTEGRATION"],
                 ["doc", "03_CONFIG_REFERENCE"],
@@ -591,8 +591,8 @@ function handbookRecipes(): array
             ],
         ],
         [
-            "title" => "Nur Backups einrichten",
-            "note" => "Ohne Updater. Quellen festlegen, Ziele wählen, Cron eintragen.",
+            "title" => "Setting up backups only",
+            "note" => "Without the updater. Define sources, choose targets, add cron.",
             "pages" => [
                 ["doc", "05_BACKUP_SOURCES"],
                 ["doc", "03_CONFIG_REFERENCE"],
@@ -602,8 +602,8 @@ function handbookRecipes(): array
             ],
         ],
         [
-            "title" => "Nur den Updater einrichten",
-            "note" => "Geschützte Pfade, Paketbau, Migrationen und Post-Update-Hook.",
+            "title" => "Setting up the updater only",
+            "note" => "Protected paths, packaging, migrations and the post-update hook.",
             "pages" => [
                 ["doc", "06_UPDATE_PACKAGING"],
                 ["doc", "07_POST_UPDATE_HOOKS"],
@@ -613,9 +613,9 @@ function handbookRecipes(): array
             ],
         ],
         [
-            "title" => "Einen eigenen Client schreiben",
-            "note" => "Andere Sprache oder anderes Framework. Das Protokoll ist verbindlich, "
-                . "die PHP-Fassung ist die Referenz.",
+            "title" => "Writing your own client",
+            "note" => "A different language or framework. The protocol is binding, "
+                . "the PHP version is the reference.",
             "pages" => [
                 ["doc", "08_PROTOCOL"],
                 ["doc", "50_API"],
@@ -626,8 +626,8 @@ function handbookRecipes(): array
             ],
         ],
         [
-            "title" => "Einen Fehler im laufenden Betrieb suchen",
-            "note" => "Meldungen, Exit-Codes und ihre Ursachen.",
+            "title" => "Debugging a running installation",
+            "note" => "Messages, exit codes and their causes.",
             "pages" => [
                 ["doc", "09_TROUBLESHOOTING"],
                 ["doc", "03_CONFIG_REFERENCE"],

+ 44 - 44
client-docs/index.php

@@ -27,7 +27,7 @@ $requestedCode = isset($_GET["code"]) ? (string) $_GET["code"] : "";
 
 $page = null;
 $markdown = null;
-$pageTitle = "Übersicht";
+$pageTitle = "Overview";
 $notFound = false;
 
 if ($requestedDoc !== "" || $requestedCode !== "") {
@@ -37,7 +37,7 @@ if ($requestedDoc !== "" || $requestedCode !== "") {
 
     if ($page === null) {
         http_response_code(404);
-        $pageTitle = "Nicht gefunden";
+        $pageTitle = "Not Found";
         $notFound = true;
     } else {
         $markdown = handbookPageMarkdown($page);
@@ -56,45 +56,45 @@ $indexUrl = handbookSelfUrl() . "/llms.php";
 $rawUrl = $page !== null ? handbookRawUrl($page) : $indexUrl;
 ?>
 <!DOCTYPE html>
-<html lang="de">
+<html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title><?php echo handbookEscape($pageTitle); ?> – Manage Client</title>
     <link rel="stylesheet" href="<?php echo handbookEscape($baseHref); ?>assets/docs.css">
     <link rel="alternate" type="text/markdown" href="<?php echo handbookEscape($rawUrl); ?>"
-          title="Diese Seite als Markdown">
+          title="This page as Markdown">
 </head>
 <body>
 <!--
-    Hinweis für LLMs und andere Programme / note for LLMs and other programs:
+    Note for LLMs and other programs:
 
-    Diese Seite rendert ihren Inhalt erst im Browser. Der Text steht hier daher
-    nicht im HTML. Dieselbe Seite als reines Markdown, ohne Auszeichnung:
+    This page renders its content only in the browser. The text is therefore
+    not in this HTML. The same page as plain Markdown, unstyled:
 
         <?php echo $rawUrl . "\n"; ?>
 
-    Verzeichnis aller Seiten, mit Kurzbeschreibung und Größe, um gezielt nur das
-    Nötige zu laden:
+    Index of every page, with a short description and size, so you can load
+    only what you need:
 
         <?php echo $indexUrl . "\n"; ?>
 -->
 <p class="llm-only">
-    Für LLMs: diese Seite als Markdown unter <?php echo handbookEscape($rawUrl); ?> –
-    Verzeichnis aller Seiten unter <?php echo handbookEscape($indexUrl); ?>
+    For LLMs: this page as Markdown at <?php echo handbookEscape($rawUrl); ?> –
+    index of every page at <?php echo handbookEscape($indexUrl); ?>
 </p>
 <header class="docs-header">
     <div class="docs-header-inner">
         <a class="docs-brand" href="<?php echo handbookEscape($baseHref); ?>index.php">Manage Client</a>
         <nav class="docs-header-links">
-            <a href="<?php echo handbookEscape($baseHref); ?>api.php">API-Referenz</a>
-            <a href="<?php echo handbookEscape($baseHref); ?>llms.php">Für LLMs</a>
+            <a href="<?php echo handbookEscape($baseHref); ?>api.php">API Reference</a>
+            <a href="<?php echo handbookEscape($baseHref); ?>llms.php">For LLMs</a>
         </nav>
     </div>
 </header>
 <div class="docs-layout">
-    <nav class="docs-nav" aria-label="Dokumentation">
-        <p class="docs-nav-title">Dokumentation</p>
+    <nav class="docs-nav" aria-label="Documentation">
+        <p class="docs-nav-title">Documentation</p>
         <ul>
             <?php foreach ($docs as $entry): ?>
                 <li>
@@ -106,13 +106,13 @@ $rawUrl = $page !== null ? handbookRawUrl($page) : $indexUrl;
             <?php endforeach; ?>
         </ul>
 
-        <p class="docs-nav-title">Schnittstelle</p>
+        <p class="docs-nav-title">Interface</p>
         <ul>
             <li><a href="<?php echo handbookEscape($baseHref); ?>api.php">Swagger UI</a></li>
-            <li><a href="<?php echo handbookEscape($baseHref); ?>openapi.php">OpenAPI-Dokument</a></li>
+            <li><a href="<?php echo handbookEscape($baseHref); ?>openapi.php">OpenAPI document</a></li>
         </ul>
 
-        <p class="docs-nav-title">Quellcode</p>
+        <p class="docs-nav-title">Source Code</p>
         <ul class="docs-nav-code">
             <?php foreach ($code as $entry): ?>
                 <li>
@@ -127,18 +127,18 @@ $rawUrl = $page !== null ? handbookRawUrl($page) : $indexUrl;
     <main class="docs-main">
         <?php if ($page === null && !$notFound): ?>
             <h1>Manage Client</h1>
-            <p>Update- und Backup-Funktionalität für eigenständige PHP-Projekte: die
-                vollständige Dokumentation des Client-Pakets, sein gesamter Quellcode und
-                die Schnittstelle zum Manage-Server.</p>
-            <p>Jedes Kapitel und jede Quelldatei hat eine eigene Seite; die Liste steht links
-                und noch einmal hier darunter, jeweils mit einem Satz dazu, was darauf steht.</p>
-            <p>Für Programme gibt es dieselben Seiten als reines Markdown:
-                <a href="<?php echo handbookEscape($baseHref); ?>llms.php">llms.php</a> ist das
-                Verzeichnis dafür und nennt zusätzlich, welche Seiten für welche Aufgabe
-                gebraucht werden.</p>
-
-            <h2>Dokumentation</h2>
-            <p>In empfohlener Lesereihenfolge.</p>
+            <p>Update and backup functionality for standalone PHP projects: the
+                complete documentation of the client package, its entire source code,
+                and the interface to the Manage server.</p>
+            <p>Every chapter and every source file has its own page; the list is on
+                the left and again below, each with one sentence about what's on it.</p>
+            <p>For programs, the same pages exist as plain Markdown:
+                <a href="<?php echo handbookEscape($baseHref); ?>llms.php">llms.php</a> is the
+                index for that, and additionally names which pages are needed for which
+                task.</p>
+
+            <h2>Documentation</h2>
+            <p>In the recommended reading order.</p>
             <dl class="docs-index-list">
                 <?php foreach ($docs as $entry): ?>
                     <dt>
@@ -150,18 +150,18 @@ $rawUrl = $page !== null ? handbookRawUrl($page) : $indexUrl;
                 <?php endforeach; ?>
             </dl>
 
-            <h2>Schnittstelle</h2>
+            <h2>Interface</h2>
             <dl class="docs-index-list">
-                <dt><a href="<?php echo handbookEscape($baseHref); ?>api.php">API-Referenz</a></dt>
-                <dd>Protokoll v1 in Swagger UI, zum Nachschlagen und Ausprobieren</dd>
-                <dt><a href="<?php echo handbookEscape($baseHref); ?>openapi.php">OpenAPI-Dokument</a></dt>
-                <dd>Dieselbe Beschreibung als OpenAPI 3.1, für Codegeneratoren</dd>
+                <dt><a href="<?php echo handbookEscape($baseHref); ?>api.php">API reference</a></dt>
+                <dd>Protocol v1 in Swagger UI, to look up and try out</dd>
+                <dt><a href="<?php echo handbookEscape($baseHref); ?>openapi.php">OpenAPI document</a></dt>
+                <dd>The same description as OpenAPI 3.1, for code generators</dd>
             </dl>
 
-            <h2>Quellcode</h2>
-            <p>Das vollständige Client-Paket. Für eine Einbindung wird der Ordner
-                <code>manage-client/</code> gebraucht; <code>examples/</code>,
-                <code>docs/</code> und <code>scripts/</code> gehören nicht dazu.</p>
+            <h2>Source Code</h2>
+            <p>The complete client package. Integrating it needs the folder
+                <code>manage-client/</code>; <code>examples/</code>,
+                <code>docs/</code> and <code>scripts/</code> are not part of that.</p>
             <dl class="docs-index-list docs-index-code">
                 <?php foreach ($code as $entry): ?>
                     <dt>
@@ -174,14 +174,14 @@ $rawUrl = $page !== null ? handbookRawUrl($page) : $indexUrl;
                 <?php endforeach; ?>
             </dl>
         <?php elseif ($notFound): ?>
-            <h1>Nicht gefunden</h1>
-            <p>Die angeforderte Seite gibt es nicht.</p>
-            <p><a href="<?php echo handbookEscape($baseHref); ?>index.php">Zur Übersicht</a></p>
+            <h1>Not Found</h1>
+            <p>The requested page doesn't exist.</p>
+            <p><a href="<?php echo handbookEscape($baseHref); ?>index.php">Back to the overview</a></p>
         <?php else: ?>
             <p class="docs-source-note">
-                Quelle: <code><?php echo handbookEscape($page["source"]); ?></code>
+                Source: <code><?php echo handbookEscape($page["source"]); ?></code>
                 · <?php echo handbookEscape(handbookFormatBytes($page["bytes"])); ?>
-                · <a href="<?php echo handbookEscape($rawUrl); ?>">als Markdown</a>
+                · <a href="<?php echo handbookEscape($rawUrl); ?>">as Markdown</a>
             </p>
             <article id="doc-content" class="markdown-body"></article>
             <script type="application/json" id="doc-source"><?php

+ 18 - 18
client-docs/llms.php

@@ -35,8 +35,8 @@ $page = $requestedDoc !== ""
 if ($page === null) {
     http_response_code(404);
     handbookSendText(
-        "# Nicht gefunden\n\n"
-        . "Diese Seite gibt es nicht. Das Verzeichnis aller Seiten steht unter\n"
+        "# Not Found\n\n"
+        . "This page doesn't exist. The index of every page is at\n"
         . handbookSelfUrl() . "/llms.php\n",
     );
 }
@@ -46,8 +46,8 @@ if ($page === null) {
 handbookSendText(
     rtrim(handbookPageMarkdown($page, "llms.php")) . "\n\n"
     . "---\n\n"
-    . "Verzeichnis aller Seiten: " . handbookSelfUrl() . "/llms.php\n"
-    . "Diese Seite für Menschen: " . handbookPageUrl($page) . "\n",
+    . "Index of every page: " . handbookSelfUrl() . "/llms.php\n"
+    . "This page for humans: " . handbookPageUrl($page) . "\n",
 );
 
 /** Emits a Markdown document and ends the request. */
@@ -87,7 +87,7 @@ function handbookRenderIndex(): string
     ]);
 
     // ---- Recipes: the shortlist per job ------------------------------------
-    $out[] = "## Wofür welche Seiten\n";
+    $out[] = "## What Each Page Is For\n";
     foreach (handbookRecipes() as $recipe) {
         $lines = ["### " . $recipe["title"], "", $recipe["note"], ""];
 
@@ -104,29 +104,29 @@ function handbookRenderIndex(): string
 
     // ---- Every page --------------------------------------------------------
     $out[] = handbookRenderList(
-        "## Dokumentation",
-        "Kapitel in empfohlener Lesereihenfolge.",
+        "## Documentation",
+        "Chapters in the recommended reading order.",
         $docs,
     );
 
-    $out[] = "## Schnittstelle\n\n"
-        . "- " . $self . "/openapi.php – OpenAPI 3.1 der vier Endpunkte des Manage-Servers, "
-        . "als JSON. Für einen eigenen Client zusammen mit dem Kapitel *Protokoll v1* lesen.\n"
-        . "- " . $self . "/api.php – dieselbe Beschreibung in Swagger UI. Eine Seite für "
-        . "Menschen, für ein Programm ohne Nutzen.\n";
+    $out[] = "## Interface\n\n"
+        . "- " . $self . "/openapi.php – OpenAPI 3.1 of the Manage server's four endpoints, "
+        . "as JSON. Read together with the *Protocol v1* chapter for a custom client.\n"
+        . "- " . $self . "/api.php – the same description in Swagger UI. A page for "
+        . "humans, no use to a program.\n";
 
     $out[] = handbookRenderList(
-        "## Quellcode",
-        "Das vollständige Client-Paket, jede Datei einzeln abrufbar. Für eine Einbindung "
-        . "wird der Ordner `manage-client/` gebraucht; `examples/`, `docs/` und `scripts/` "
-        . "gehören nicht dazu.",
+        "## Source Code",
+        "The complete client package, every file individually retrievable. Integrating it "
+        . "needs the folder `manage-client/`; `examples/`, `docs/` and `scripts/` "
+        . "are not part of that.",
         $code,
     );
 
     $vendored = handbookVendoredFiles();
     if ($vendored !== []) {
-        $lines = ["## Nicht abgedruckt", "", "Fremdbibliotheken, die zum Paket gehören, aber "
-            . "hier nicht als Seite stehen:", ""];
+        $lines = ["## Not Reproduced Here", "", "Third-party libraries that are part of the "
+            . "package but don't have a page here:", ""];
         foreach ($vendored as $relative => $bytes) {
             $lines[] = "- `client-package/" . $relative . "` (" . handbookFormatBytes($bytes) . ")";
         }

+ 54 - 54
client-docs/openapi.json

@@ -1,32 +1,32 @@
 {
     "openapi": "3.1.0",
     "info": {
-        "title": "Manage – Protokoll v1",
+        "title": "Manage – Protocol v1",
         "version": "1.0.0",
-        "summary": "Update- und Backup-Schnittstelle zwischen einer Projektinstanz und dem Manage-Server.",
-        "description": "Die vier Endpunkte, die ein Client benötigt: Release-Manifest abrufen, Release-Paket herunterladen, Backup hochladen, Status melden.\n\nJede Anfrage authentifiziert sich mit zwei Headern (`X-Manage-Instance`, `X-Manage-Token`). Es gibt keine Sitzung, kein Cookie und kein gemeinsames Passwort. Der Server speichert nur den SHA-256-Hash des Tokens.\n\nJede erfolgreich authentifizierte Anfrage aktualisiert nebenbei `last_seen_at` und die letzte IP der Instanz.\n\nDie Referenzimplementierung des Clients steht vollständig im Handbuch, siehe `lib/remote.php`, `lib/updater.php` und `lib/backup.php`.",
+        "summary": "Update and backup interface between a project instance and the Manage server.",
+        "description": "The four endpoints a client needs: fetch the release manifest, download the release package, upload a backup, report status.\n\nEvery request authenticates with two headers (`X-Manage-Instance`, `X-Manage-Token`). There is no session, no cookie and no shared password. The server stores only the SHA-256 hash of the token.\n\nEvery successfully authenticated request additionally updates `last_seen_at` and the instance's latest IP.\n\nThe reference implementation of the client is fully documented in the handbook; see `lib/remote.php`, `lib/updater.php` and `lib/backup.php`.\n\nNote: the server's actual `error` strings in the response examples below are still German text — this document's descriptions are in English, the API's literal responses are not.",
         "license": {
-            "name": "Siehe Handbuch"
+            "name": "See the handbook"
         }
     },
     "servers": [
         {
             "url": "/api/v1",
-            "description": "Diese Manage-Installation"
+            "description": "This Manage installation"
         }
     ],
     "tags": [
         {
             "name": "Update",
-            "description": "Release ermitteln und Paket beziehen."
+            "description": "Determine the release and fetch the package."
         },
         {
             "name": "Backup",
-            "description": "Sicherungsarchive an den Server übertragen."
+            "description": "Transfer backup archives to the server."
         },
         {
             "name": "Status",
-            "description": "Zustand der Instanz melden."
+            "description": "Report the instance's state."
         }
     ],
     "security": [
@@ -40,11 +40,11 @@
             "get": {
                 "tags": ["Update"],
                 "operationId": "getManifest",
-                "summary": "Release abrufen, das die Instanz installieren soll",
-                "description": "Liefert Version, Download-Adresse, Größe und SHA-256 des aktuellen Release.\n\n`package_url` wird aus der Serverkonfiguration (`MANAGE_PUBLIC_URL`) gebildet, nicht aus dem `Host`-Header der Anfrage. Ein gefälschter Header kann einen Client daher nicht auf einen fremden Server umlenken.\n\nRelease-Metadaten sind nicht öffentlich: Der Endpunkt verlangt ein gültiges Token.",
+                "summary": "Fetch the release the instance should install",
+                "description": "Returns version, download address, size and SHA-256 of the current release.\n\n`package_url` is built from the server configuration (`MANAGE_PUBLIC_URL`), not from the request's `Host` header. A spoofed header therefore can't redirect a client to a foreign server.\n\nRelease metadata is not public: the endpoint requires a valid token.",
                 "responses": {
                     "200": {
-                        "description": "Aktuelles Release",
+                        "description": "Current release",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/Manifest" },
@@ -63,7 +63,7 @@
                     "401": { "$ref": "#/components/responses/Unauthorized" },
                     "403": { "$ref": "#/components/responses/Disabled" },
                     "404": {
-                        "description": "Es ist kein gültiges Release veröffentlicht.",
+                        "description": "No valid release is published.",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/Error" },
@@ -81,32 +81,32 @@
             "get": {
                 "tags": ["Update"],
                 "operationId": "getPackage",
-                "summary": "Release-ZIP herunterladen",
-                "description": "Streamt das Release-Archiv. Antwort ist `application/zip` mit `Content-Length` und `Cache-Control: private, no-store`; bei Erfolg kein JSON.\n\nDer Client **muss** Größe und SHA-256 gegen das Manifest prüfen und die Datei bei Abweichung löschen. Ohne diese Prüfung wird beliebiger Code ausgerollt.",
+                "summary": "Download the release ZIP",
+                "description": "Streams the release archive. The response is `application/zip` with `Content-Length` and `Cache-Control: private, no-store`; no JSON on success.\n\nThe client **must** check size and SHA-256 against the manifest and delete the file on mismatch. Without this check, arbitrary code gets deployed.",
                 "parameters": [
                     {
                         "name": "version",
                         "in": "query",
                         "required": true,
-                        "description": "Version im Format `vMAJOR.MINOR.PATCH`.",
+                        "description": "Version in `vMAJOR.MINOR.PATCH` format.",
                         "schema": { "$ref": "#/components/schemas/Version" },
                         "example": "v1.3.0"
                     }
                 ],
                 "responses": {
                     "200": {
-                        "description": "Das Release-Archiv",
+                        "description": "The release archive",
                         "headers": {
                             "Content-Disposition": {
                                 "description": "attachment; filename=\"…zip\"",
                                 "schema": { "type": "string" }
                             },
                             "Content-Length": {
-                                "description": "Größe in Bytes, identisch mit `size` aus dem Manifest.",
+                                "description": "Size in bytes, identical to `size` from the manifest.",
                                 "schema": { "type": "integer" }
                             },
                             "Cache-Control": {
-                                "description": "Immer `private, no-store`.",
+                                "description": "Always `private, no-store`.",
                                 "schema": { "type": "string" }
                             }
                         },
@@ -117,7 +117,7 @@
                         }
                     },
                     "400": {
-                        "description": "Ungültiges Versionsformat",
+                        "description": "Invalid version format",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/Error" },
@@ -128,7 +128,7 @@
                     "401": { "$ref": "#/components/responses/Unauthorized" },
                     "403": { "$ref": "#/components/responses/Disabled" },
                     "404": {
-                        "description": "Release existiert nicht",
+                        "description": "Release does not exist",
                         "content": {
                             "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
                         }
@@ -143,8 +143,8 @@
             "post": {
                 "tags": ["Backup"],
                 "operationId": "uploadBackup",
-                "summary": "Backup-Archiv hochladen",
-                "description": "Nimmt ein ZIP entgegen und legt es unter der Instanz ab.\n\nGeprüft wird in dieser Reihenfolge: Upload-Fehlercode, `is_uploaded_file`, Größenlimit (`MANAGE_BACKUP_MAX_UPLOAD_BYTES`), ZIP-Signatur, Dateinamensmuster, Prüfsumme nach dem Speichern. Weicht die Prüfsumme ab, wird die Datei wieder gelöscht und `400` gemeldet.\n\nEin vorhandener Dateiname wird nie überschrieben: Der Server hängt `-2`, `-3` an und meldet den tatsächlich verwendeten Namen zurück.\n\nFehler beim S3-Archivieren lassen den Upload **nicht** fehlschlagen – die lokale Kopie ist gespeichert und wird später nachgezogen.",
+                "summary": "Upload a backup archive",
+                "description": "Accepts a ZIP and stores it under the instance.\n\nChecked in this order: upload error code, `is_uploaded_file`, size limit (`MANAGE_BACKUP_MAX_UPLOAD_BYTES`), ZIP signature, filename pattern, checksum after storing. If the checksum doesn't match, the file is deleted again and `400` is reported.\n\nAn existing filename is never overwritten: the server appends `-2`, `-3` and reports the name actually used.\n\nErrors during S3 archiving do **not** fail the upload – the local copy is stored and gets caught up later.",
                 "requestBody": {
                     "required": true,
                     "content": {
@@ -156,22 +156,22 @@
                                     "backup": {
                                         "type": "string",
                                         "format": "binary",
-                                        "description": "Das ZIP-Archiv."
+                                        "description": "The ZIP archive."
                                     },
                                     "filename": {
                                         "type": "string",
                                         "pattern": "^backup-\\d{8}-\\d{6}(?:-\\d+)?\\.zip$",
-                                        "description": "Gewünschter Name. Ohne Angabe vergibt der Server `backup-YYYYmmdd-HHMMSS.zip`.",
+                                        "description": "Desired name. The server assigns `backup-YYYYmmdd-HHMMSS.zip` if omitted.",
                                         "example": "backup-20260820-092104.zip"
                                     },
                                     "sha256": {
                                         "type": "string",
                                         "pattern": "^[a-f0-9]{64}$",
-                                        "description": "Prüfsumme des Archivs. Wird serverseitig neu berechnet und verglichen."
+                                        "description": "Checksum of the archive. Recomputed and compared server-side."
                                     },
                                     "meta": {
                                         "type": "string",
-                                        "description": "JSON-Objekt mit `trigger`, `file_count`, `source_bytes`, `app_version`.",
+                                        "description": "JSON object with `trigger`, `file_count`, `source_bytes`, `app_version`.",
                                         "example": "{\"trigger\":\"cron\",\"file_count\":3,\"source_bytes\":63,\"app_version\":\"v1.3.0\"}"
                                     }
                                 }
@@ -184,13 +184,13 @@
                 },
                 "responses": {
                     "200": {
-                        "description": "Backup gespeichert",
+                        "description": "Backup stored",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/BackupResult" },
                                 "example": {
                                     "success": true,
-                                    "instance": "meinprojekt-prod",
+                                    "instance": "myproject-prod",
                                     "filename": "backup-20260820-092104.zip",
                                     "size": 427,
                                     "sha256": "824f3f8000000000000000000000000000000000000000000000000000000000",
@@ -201,7 +201,7 @@
                         }
                     },
                     "400": {
-                        "description": "Upload abgelehnt: Datei fehlt, Limit überschritten, kein ZIP, Name oder Prüfsumme falsch.",
+                        "description": "Upload rejected: file missing, limit exceeded, not a ZIP, wrong name or checksum.",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/Error" },
@@ -220,8 +220,8 @@
             "post": {
                 "tags": ["Status"],
                 "operationId": "sendHeartbeat",
-                "summary": "Status melden und Update-Information erhalten",
-                "description": "Meldet den Zustand der Instanz. Alle Felder sind optional; fehlende Felder lassen den bisherigen Wert auf dem Server unverändert.\n\nDie Antwort enthält die Update-Information, ersetzt für einfache Überwachung also einen zusätzlichen Aufruf von `manifest.php`.",
+                "summary": "Report status and receive update information",
+                "description": "Reports the instance's state. All fields are optional; missing fields leave the server's current value unchanged.\n\nThe response includes the update information, so it replaces a separate call to `manifest.php` for simple monitoring.",
                 "requestBody": {
                     "required": false,
                     "content": {
@@ -239,13 +239,13 @@
                 },
                 "responses": {
                     "200": {
-                        "description": "Status übernommen",
+                        "description": "Status accepted",
                         "content": {
                             "application/json": {
                                 "schema": { "$ref": "#/components/schemas/HeartbeatResponse" },
                                 "example": {
                                     "success": true,
-                                    "instance": "meinprojekt-prod",
+                                    "instance": "myproject-prod",
                                     "latest": "v1.3.0",
                                     "update_available": false,
                                     "server_time": "2026-08-20T09:23:11+00:00"
@@ -254,7 +254,7 @@
                         }
                     },
                     "400": {
-                        "description": "Anfrage-Body ist kein gültiges JSON.",
+                        "description": "Request body is not valid JSON.",
                         "content": {
                             "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
                         }
@@ -273,18 +273,18 @@
                 "type": "apiKey",
                 "in": "header",
                 "name": "X-Manage-Instance",
-                "description": "Kennung der Instanz, zum Beispiel `meinprojekt-prod`."
+                "description": "The instance id, for example `myproject-prod`."
             },
             "instanceToken": {
                 "type": "apiKey",
                 "in": "header",
                 "name": "X-Manage-Token",
-                "description": "Das beim Anlegen der Instanz einmalig angezeigte Token."
+                "description": "The token shown once when the instance was created."
             }
         },
         "responses": {
             "Unauthorized": {
-                "description": "Header fehlen, Instanz unbekannt oder Token falsch – bewusst nicht unterscheidbar, damit Instanz-Kennungen nicht durchprobiert werden können.",
+                "description": "Headers missing, instance unknown, or token wrong – deliberately indistinguishable, so instance ids can't be brute-forced.",
                 "content": {
                     "application/json": {
                         "schema": { "$ref": "#/components/schemas/Error" },
@@ -293,7 +293,7 @@
                 }
             },
             "Disabled": {
-                "description": "Instanz existiert, ist aber deaktiviert.",
+                "description": "Instance exists but is deactivated.",
                 "content": {
                     "application/json": {
                         "schema": { "$ref": "#/components/schemas/Error" },
@@ -302,7 +302,7 @@
                 }
             },
             "MethodNotAllowed": {
-                "description": "Falsche HTTP-Methode. Die Antwort trägt einen `Allow`-Header.",
+                "description": "Wrong HTTP method. The response carries an `Allow` header.",
                 "headers": {
                     "Allow": { "schema": { "type": "string" } }
                 },
@@ -311,7 +311,7 @@
                 }
             },
             "RateLimited": {
-                "description": "Zu viele fehlgeschlagene Authentifizierungen von dieser IP (`MANAGE_API_RATE_LIMIT_MAX` je `MANAGE_API_RATE_LIMIT_WINDOW` Sekunden, ab Werk 240 je 300 s). Eine erfolgreiche Anmeldung setzt den Zähler zurück.",
+                "description": "Too many failed authentications from this IP (`MANAGE_API_RATE_LIMIT_MAX` per `MANAGE_API_RATE_LIMIT_WINDOW` seconds, 240 per 300s out of the box). A successful login resets the counter.",
                 "content": {
                     "application/json": {
                         "schema": { "$ref": "#/components/schemas/Error" },
@@ -320,7 +320,7 @@
                 }
             },
             "ServerError": {
-                "description": "Unerwarteter Serverfehler; Einzelheiten stehen im Serverprotokoll.",
+                "description": "Unexpected server error; details are in the server log.",
                 "content": {
                     "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
                 }
@@ -334,11 +334,11 @@
             },
             "Error": {
                 "type": "object",
-                "description": "Aufbau **aller** Fehlerantworten.",
+                "description": "Shape of **every** error response.",
                 "required": ["success", "error"],
                 "properties": {
                     "success": { "type": "boolean", "const": false },
-                    "error": { "type": "string", "description": "Meldung in Klartext, für Protokoll und Anzeige." }
+                    "error": { "type": "string", "description": "Plain-text message, for logging and display." }
                 }
             },
             "Manifest": {
@@ -349,19 +349,19 @@
                     "latest": { "$ref": "#/components/schemas/Version" },
                     "version": {
                         "allOf": [{ "$ref": "#/components/schemas/Version" }],
-                        "description": "Gleichbedeutend mit `latest`; beide Felder existieren aus Kompatibilitätsgründen."
+                        "description": "Equivalent to `latest`; both fields exist for compatibility reasons."
                     },
                     "package_url": {
                         "type": "string",
                         "format": "uri",
-                        "description": "Absolute Adresse für den Download, aus `MANAGE_PUBLIC_URL` gebildet."
+                        "description": "Absolute download address, built from `MANAGE_PUBLIC_URL`."
                     },
                     "sha256": {
                         "type": "string",
                         "pattern": "^[a-f0-9]{64}$",
-                        "description": "Prüfsumme des Pakets. Vom Client zwingend zu verifizieren."
+                        "description": "Checksum of the package. Must be verified by the client."
                     },
-                    "size": { "type": "integer", "description": "Paketgröße in Bytes." },
+                    "size": { "type": "integer", "description": "Package size in bytes." },
                     "published_at": { "type": "string", "format": "date-time" }
                 }
             },
@@ -373,17 +373,17 @@
                     "instance": { "type": "string" },
                     "filename": {
                         "type": "string",
-                        "description": "Der tatsächlich vergebene Name – kann vom gewünschten abweichen, wenn er bereits belegt war."
+                        "description": "The name actually assigned – may differ from the one requested if it was already taken."
                     },
                     "size": { "type": "integer" },
                     "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
                     "retention": {
                         "type": "integer",
-                        "description": "Wie viele Tage der Server dieses Archiv aufbewahrt."
+                        "description": "How many days the server keeps this archive."
                     },
                     "s3": {
                         "type": "object",
-                        "description": "Zustand des optionalen S3-Archivs. Nie ein Grund für einen Fehlschlag.",
+                        "description": "State of the optional S3 archive. Never a reason for failure.",
                         "properties": {
                             "enabled": { "type": "boolean" },
                             "uploaded": { "type": "boolean" },
@@ -397,10 +397,10 @@
                 "properties": {
                     "version": {
                         "type": "string",
-                        "description": "Installierte Version der Anwendung. Leer, wenn nicht ermittelbar."
+                        "description": "Installed version of the application. Empty if it can't be determined."
                     },
                     "php_version": { "type": "string", "examples": ["8.3.6"] },
-                    "disk_free": { "type": "integer", "description": "Freier Speicher in Bytes." },
+                    "disk_free": { "type": "integer", "description": "Free disk space in bytes." },
                     "pending_migrations": { "type": "integer", "minimum": 0 },
                     "last_backup_at": { "type": "string", "format": "date-time" }
                 }
@@ -413,11 +413,11 @@
                     "instance": { "type": "string" },
                     "latest": {
                         "type": "string",
-                        "description": "Aktuelles Release auf dem Server, oder leer, wenn keines veröffentlicht ist."
+                        "description": "Current release on the server, or empty if none is published."
                     },
                     "update_available": {
                         "type": "boolean",
-                        "description": "Ergebnis eines `version_compare` zwischen `latest` und der gemeldeten Version. `false`, solange die Instanz keine Version meldet."
+                        "description": "Result of a `version_compare` between `latest` and the reported version. `false` as long as the instance reports no version."
                     },
                     "server_time": { "type": "string", "format": "date-time" }
                 }

+ 89 - 88
client-package/README.md

@@ -1,136 +1,137 @@
-# Manage Client-Paket
+# Manage Client Package
 
-Update- und Backup-Client für PHP-Projekte, zusammen mit der vollständigen
-Dokumentation zur Einbindung. Dieser Ordner ist so gebaut, dass er als einzelnes
-ZIP weitergegeben werden kann: Wer ihn erhält, braucht weder Zugriff auf das
-Repository des Manage-Servers noch weitere Erklärungen.
+Update and backup client for PHP projects, together with the complete
+documentation for integrating it. This folder is built so it can be handed
+over as a single ZIP: whoever receives it needs neither access to the
+Manage server's repository nor further explanation.
 
-## Was das ist
+## What this is
 
-Ein PHP-Client, der
+A PHP client that
 
-- Releases von einem zentralen Manage-Server holt, prüft und einspielt,
-- Backups der Betriebsdaten erstellt und dorthin hochlädt,
-- den Status der Installation an den Server meldet.
+- fetches, verifies and installs releases from a central Manage server,
+- creates backups of operational data and uploads them there,
+- reports the installation's status to the server.
 
-Der Client läuft eigenständig über die Kommandozeile, bringt eine fertige
-Oberfläche für den Adminbereich mit und stellt alle Funktionen zum direkten
-Aufruf aus dem Projekt bereit. Alle drei Wege rufen dieselben Funktionen auf, ein
-Vorgang verhält sich also überall gleich.
+The client runs standalone from the command line, brings a ready-made UI
+for the admin area, and exposes every function for direct calls from the
+project. All three paths call the same functions, so a given operation
+behaves identically no matter how it's triggered.
 
-Keine Abhängigkeiten: kein Composer, kein Build-Schritt, keine externen
-Bibliotheken. PHP 8.0 oder neuer, `ext-zip` für Updates, Schreibrechte auf dem
-Datenverzeichnis.
+No dependencies: no Composer, no build step, no external libraries. PHP 8.0
+or newer, `ext-zip` for updates, write access to the data directory.
 
-## In fünf Minuten
+## In five minutes
 
 ```bash
-# 1. Ordner ins Projekt kopieren
-cp -r manage-client /pfad/zum/projekt/manage-client
+# 1. Copy the folder into the project
+cp -r manage-client /path/to/project/manage-client
 
-# 2. Instanz im Manage-Server anlegen und das einmalig angezeigte Token kopieren
+# 2. Create an instance on the Manage server and copy the token shown once
 
-# 3. Konfiguration anlegen
-cd /pfad/zum/projekt/manage-client
+# 3. Create the configuration
+cd /path/to/project/manage-client
 cp config.sample.php config.php
-# MANAGE_SERVER_URL, MANAGE_INSTANCE, MANAGE_TOKEN und MANAGE_BACKUP_SOURCES setzen
+# Set MANAGE_SERVER_URL, MANAGE_INSTANCE, MANAGE_TOKEN and MANAGE_BACKUP_SOURCES
 
-# 4. Verbindung prüfen
+# 4. Check the connection
 php bin/manage-client.php status
 
-# 5. Erstes Backup
+# 5. First backup
 php bin/manage-client.php backup
 ```
 
-Ausführlich: [docs/01_QUICKSTART.md](docs/01_QUICKSTART.md).
+Details: [docs/01_QUICKSTART.md](docs/01_QUICKSTART.md).
 
-## Inhalt
+## Contents
 
 ```text
-manage-client/            <- dieser Ordner wird ins Projekt kopiert
-  config.sample.php       Vorlage, wird zu config.php
-  lib/                    die Bibliothek; client.php ist der einzige Einstiegspunkt
-  bin/manage-client.php   Kommandozeile für Cron und Shell
-  ui/panel.php            fertige Seite für den Adminbereich
-  ui/status-partial.php   kleiner Statusblock für eine bestehende Seite
-
-docs/                     die Dokumentation, siehe unten
-examples/                 lauffähige Beispiele zum Abschreiben
-scripts/                  Build-Skript für Release-Pakete, siehe unten
+manage-client/            <- this folder gets copied into the project
+  config.sample.php       template, becomes config.php
+  lib/                    the library; client.php is the only entry point
+  bin/manage-client.php   command line for cron and shell
+  ui/panel.php            ready-made page for the admin area
+  ui/status-partial.php   small status block for an existing page
+
+docs/                     the documentation, see below
+examples/                 working examples to copy from
+scripts/                  build script for release packages, see below
 ```
 
-Im Browser lesbar: `docs/index.php` über einen beliebigen PHP-Server öffnen, zum
-Beispiel `php -S localhost:8080 -t docs`. Die Dateien sind auch als reines
-Markdown lesbar.
+Readable in the browser: open `docs/index.php` through any PHP server, for
+example `php -S localhost:8080 -t docs`. The files are also readable as
+plain Markdown.
 
-## Dokumentation
+## Documentation
 
-| Dokument | Inhalt |
+| Document | Content |
 |---|---|
-| [01_QUICKSTART](docs/01_QUICKSTART.md) | Von diesem Ordner zum ersten Backup |
-| [02_INTEGRATION](docs/02_INTEGRATION.md) | Einbindung in ein bestehendes Projekt, Cron, Rechte |
-| [03_CONFIG_REFERENCE](docs/03_CONFIG_REFERENCE.md) | Jede Konstante mit Standardwert und Bedeutung |
-| [04_FUNCTION_API](docs/04_FUNCTION_API.md) | Die aufrufbaren Funktionen mit Rückgabewerten |
-| [05_BACKUP_SOURCES](docs/05_BACKUP_SOURCES.md) | Was gesichert wird, Datenbank-Dump, zusätzliche Ziele |
-| [06_UPDATE_PACKAGING](docs/06_UPDATE_PACKAGING.md) | Wie ein Release-Paket gebaut werden muss |
-| [07_POST_UPDATE_HOOKS](docs/07_POST_UPDATE_HOOKS.md) | Migrationen und Projekt-Callback nach dem Update |
-| [08_PROTOCOL](docs/08_PROTOCOL.md) | Die HTTP-Schnittstelle, für eigene Clients und Debugging |
-| [09_TROUBLESHOOTING](docs/09_TROUBLESHOOTING.md) | Jede Fehlermeldung mit Ursache und Behebung |
-| [10_SECURITY](docs/10_SECURITY.md) | Token, Berechtigungen, Checkliste vor dem Produktivgang |
-
-Empfohlene Reihenfolge beim ersten Mal: 01, 02, 05, 06. Der Rest ist Nachschlagewerk.
-
-## Beispiele
-
-| Datei | Inhalt |
+| [01_QUICKSTART](docs/01_QUICKSTART.md) | from this folder to the first backup |
+| [02_INTEGRATION](docs/02_INTEGRATION.md) | integration into an existing project, cron, permissions |
+| [03_CONFIG_REFERENCE](docs/03_CONFIG_REFERENCE.md) | every constant with its default value and meaning |
+| [04_FUNCTION_API](docs/04_FUNCTION_API.md) | the callable functions with return values |
+| [05_BACKUP_SOURCES](docs/05_BACKUP_SOURCES.md) | what gets backed up, database dumps, extra targets |
+| [06_UPDATE_PACKAGING](docs/06_UPDATE_PACKAGING.md) | how a release package must be built |
+| [07_POST_UPDATE_HOOKS](docs/07_POST_UPDATE_HOOKS.md) | migrations and the project callback after an update |
+| [08_PROTOCOL](docs/08_PROTOCOL.md) | the HTTP interface, for custom clients and debugging |
+| [09_TROUBLESHOOTING](docs/09_TROUBLESHOOTING.md) | every error message with cause and fix |
+| [10_SECURITY](docs/10_SECURITY.md) | token, permissions, checklist before going live |
+
+Recommended order for the first read: 01, 02, 05, 06. The rest is reference
+material.
+
+## Examples
+
+| File | Content |
 |---|---|
-| `examples/flat-file-project/` | Projekt mit JSON-Dateien, inklusive Beispielmigration |
-| `examples/mysql-project/` | Projekt mit MySQL, Datenbank-Dump und `ALTER TABLE`-Migration |
-| `examples/after-update.php` | Vorlage für den Post-Update-Callback |
-| `examples/integration-snippet.php` | Die Zeilen, die ein bestehendes Projekt braucht |
-| `examples/cron/manage-client.cron` | Fertige Crontab-Zeilen |
+| `examples/flat-file-project/` | project with JSON files, including an example migration |
+| `examples/mysql-project/` | project with MySQL, database dump and an `ALTER TABLE` migration |
+| `examples/after-update.php` | template for the post-update callback |
+| `examples/integration-snippet.php` | the lines an existing project needs |
+| `examples/cron/manage-client.cron` | ready-made crontab lines |
 
-## Release-Pakete bauen
+## Building release packages
 
-`scripts/create-release-zip.sh` baut aus einem Projekt das ZIP, das im
-Manage-Server als Release hochgeladen wird. Es wird nach `scripts/` des Projekts
-kopiert, am Kopf einmal angepasst (Produktname, Versionsdatei, Ausschlüsse) und
-dann im Projektverzeichnis aufgerufen:
+`scripts/create-release-zip.sh` builds the ZIP from a project that gets
+uploaded to the Manage server as a release. It gets copied to `scripts/` of
+the project, adjusted once at the top (product name, version file,
+exclusions), and then run from the project directory:
 
 ```bash
 ./scripts/create-release-zip.sh v1.3.0
 ```
 
-Einzelheiten: [docs/06_UPDATE_PACKAGING.md](docs/06_UPDATE_PACKAGING.md).
+Details: [docs/06_UPDATE_PACKAGING.md](docs/06_UPDATE_PACKAGING.md).
 
-## Befehle
+## Commands
 
 ```text
 php manage-client/bin/manage-client.php status
-                                        check                      Exit 2 = Update verfügbar
+                                        check                      exit 2 = update available
                                         update [--force] [--yes] [--skip-hook]
                                         migrate [--dry-run]
                                         backup [--trigger=cron]
                                         heartbeat
 ```
 
-Exit-Codes: `0` Erfolg, `1` Fehler, `2` Update verfügbar (nur bei `check`).
-`--quiet` unterdrückt die normale Ausgabe, Fehler gehen weiterhin auf STDERR.
+Exit codes: `0` success, `1` error, `2` update available (`check` only).
+`--quiet` suppresses normal output; errors still go to STDERR.
 
-## Was der Client bewusst nicht tut
+## What the client deliberately does not do
 
-- **Keine Wiederherstellung.** Backups werden erstellt und übertragen, aber nie
-  zurückgespielt. Ein Update sichert die überschriebenen Dateien, kann sie aber
-  nicht zurückholen. Beides ist bewusst Handarbeit.
-- **Keine automatischen Updates.** `update` ist immer eine bewusste Entscheidung.
-- **Kein Entfernen gelöschter Dateien.** Ein Update überlagert den Bestand; was im
-  neuen Release fehlt, bleibt liegen. Soll es verschwinden, gehört das in eine
-  Migration.
-- **Kein Wartungsmodus.** Die Anwendung bleibt während des Ausrollens erreichbar.
+- **No restore.** Backups are created and transferred, but never played
+  back. An update backs up the files it overwrites, but cannot bring them
+  back. Both are deliberately manual work.
+- **No automatic updates.** `update` is always a deliberate decision.
+- **No removal of deleted files.** An update overlays what's there; a file
+  missing from the new release stays behind. To make it disappear, that
+  belongs in a migration.
+- **No maintenance mode.** The application stays reachable while rolling
+  out.
 
-## Anpassen
+## Customizing
 
-Der Ordner wird pro Projekt gepflegt. Anpassungen an `ui/panel.php` – etwa an die
-Anmeldung oder das Aussehen des Projekts – sind vorgesehen und ausdrücklich
-erwünscht. Änderungen in `lib/` sollten sparsam bleiben, damit eine neuere Fassung
-des Client-Pakets noch übernommen werden kann.
+This folder is maintained per project. Adjustments to `ui/panel.php` — for
+example to the login or the project's look — are expected and explicitly
+welcome. Changes inside `lib/` should stay sparse, so a newer version of
+the client package can still be adopted.

+ 46 - 41
client-package/docs/01_QUICKSTART.md

@@ -1,51 +1,54 @@
 # Quickstart
 
-## Überblick
+## Overview
 
-Fünf Schritte von diesem Ordner bis zum ersten Backup. Ausführliche Erklärungen
-stehen in [02_INTEGRATION](02_INTEGRATION.md) und [03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md).
+Five steps from this folder to the first backup. Detailed explanations are
+in [02_INTEGRATION](02_INTEGRATION.md) and
+[03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md).
 
-Relevante Dateien:
+Relevant files:
 
-- `manage-client/` – der Ordner, der in das Projekt kopiert wird
-- `manage-client/config.sample.php` – Vorlage für die Konfiguration
-- `manage-client/bin/manage-client.php` – Kommandozeile
-- `manage-client/ui/panel.php` – fertige Oberfläche für den Adminbereich
+- `manage-client/` – the folder that gets copied into the project
+- `manage-client/config.sample.php` – template for the configuration
+- `manage-client/bin/manage-client.php` – command line
+- `manage-client/ui/panel.php` – ready-made UI for the admin area
 
-Voraussetzungen: PHP 8.0 oder neuer, die Erweiterung `zip` (für Updates), Schreibrechte
-auf dem Datenverzeichnis des Projekts. Weder Composer noch ein Build-Schritt werden benötigt.
+Requirements: PHP 8.0 or newer, the `zip` extension (for updates), write
+access to the project's data directory. Neither Composer nor a build step
+is needed.
 
-## 1. Ordner kopieren
+## 1. Copy the folder
 
 ```bash
-cp -r manage-client /pfad/zum/projekt/manage-client
+cp -r manage-client /path/to/project/manage-client
 ```
 
-Der Ordner liegt üblicherweise direkt im Projektstamm, also neben `index.php`.
-Liegt er woanders, muss `MANAGE_APP_ROOT` angepasst werden.
+The folder usually sits directly in the project root, next to `index.php`.
+If it lives elsewhere, `MANAGE_APP_ROOT` needs to be adjusted.
 
-## 2. Instanz auf dem Server anlegen
+## 2. Create an instance on the server
 
-Im Manage-Server unter **Instanzen** eine neue Instanz anlegen. Direkt danach wird
-das Token **einmalig** angezeigt – zusammen mit einem fertigen Konfigurationsblock
-zum Kopieren. Danach ist das Token nicht mehr abrufbar; es kann nur neu erzeugt werden.
+In the Manage server, under **Instances**, create a new instance. Right
+after that, the token is shown **once** — together with a ready-made
+configuration block to copy. After that the token can no longer be
+retrieved; it can only be regenerated.
 
-## 3. Konfiguration anlegen
+## 3. Create the configuration
 
 ```bash
-cd /pfad/zum/projekt/manage-client
+cd /path/to/project/manage-client
 cp config.sample.php config.php
 ```
 
-Mindestens diese Werte eintragen:
+Enter at least these values:
 
 ```php
 define("MANAGE_SERVER_URL", "https://manage.example.org");
-define("MANAGE_INSTANCE",   "mein-projekt-prod");
+define("MANAGE_INSTANCE",   "my-project-prod");
 define("MANAGE_TOKEN",      "…");
 ```
 
-Danach festlegen, was gesichert werden soll:
+Then decide what should be backed up:
 
 ```php
 define("MANAGE_BACKUP_SOURCES", [
@@ -54,40 +57,42 @@ define("MANAGE_BACKUP_SOURCES", [
 ]);
 ```
 
-`config.php` gehört **nicht** ins Repository des Projekts. Siehe [10_SECURITY](10_SECURITY.md).
+`config.php` does **not** belong in the project's repository. See
+[10_SECURITY](10_SECURITY.md).
 
-## 4. Verbindung prüfen
+## 4. Check the connection
 
 ```bash
 php manage-client/bin/manage-client.php status
 ```
 
-Erwartete Ausgabe:
+Expected output (the command's actual output stays in German; shown here
+with an English gloss on the right):
 
 ```text
-Instanz:           mein-projekt-prod
+Instanz:           my-project-prod      # Instance:
 Server:            https://manage.example.org
-Konfiguriert:      ja
-Installierte Ver.: v1.0.0
-Aktuelles Release: v1.0.0
-Update verfügbar:  nein
-Lokale Backups:    0
+Konfiguriert:      ja                   # Configured: yes
+Installierte Ver.: v1.0.0               # Installed version:
+Aktuelles Release: v1.0.0               # Current release:
+Update verfügbar:  nein                 # Update available: no
+Lokale Backups:    0                    # Local backups:
 ```
 
-Steht dort `Konfiguriert: NEIN` oder erscheint ein Fehler, hilft
-[09_TROUBLESHOOTING](09_TROUBLESHOOTING.md).
+If it shows `Konfiguriert: NEIN` ("Configured: NO") or an error appears,
+[09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) helps.
 
-## 5. Erstes Backup
+## 5. First backup
 
 ```bash
 php manage-client/bin/manage-client.php backup
 ```
 
-Das Archiv liegt danach lokal unter `data/manage/backups/` und ist im Manage-Server
-unter **Backups** sichtbar.
+The archive then sits locally under `data/manage/backups/` and is visible
+in the Manage server under **Backups**.
 
-## Weiter
+## Next
 
-- Oberfläche in den Adminbereich einbinden: [02_INTEGRATION](02_INTEGRATION.md)
-- Regelmäßige Backups per Cron: [02_INTEGRATION](02_INTEGRATION.md)
-- Release-Pakete richtig bauen: [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md)
+- Add the UI to the admin area: [02_INTEGRATION](02_INTEGRATION.md)
+- Regular backups via cron: [02_INTEGRATION](02_INTEGRATION.md)
+- Building release packages correctly: [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md)

+ 73 - 70
client-package/docs/02_INTEGRATION.md

@@ -1,31 +1,32 @@
-# Integration in ein bestehendes Projekt
+# Integration Into an Existing Project
 
-## Überblick
+## Overview
 
-Der Client funktioniert eigenständig über die Kommandozeile. Zusätzlich lässt er
-sich in drei Stufen in das Projekt einbinden – von "gar nicht" bis "vollständig".
+The client works standalone from the command line. It can additionally be
+wired into the project in three stages — from "not at all" to "fully".
 
-Relevante Dateien:
+Relevant files:
 
-- `manage-client/lib/client.php` – einziger Einstiegspunkt, lädt alles Weitere
-- `manage-client/ui/panel.php` – vollständige Oberfläche
-- `manage-client/ui/status-partial.php` – kleiner Statusblock
-- `manage-client/bin/manage-client.php` – Kommandozeile für Cron
+- `manage-client/lib/client.php` – the only entry point, loads everything else
+- `manage-client/ui/panel.php` – the complete UI
+- `manage-client/ui/status-partial.php` – a small status block
+- `manage-client/bin/manage-client.php` – command line for cron
 
-## Stufe 1: Standalone
+## Stage 1: standalone
 
-Nichts zu tun. Der Client wird ausschließlich über die Kommandozeile bedient und
-das Projekt weiß nichts von ihm. Nur der Ordner muss vorhanden und konfiguriert sein.
+Nothing to do. The client is operated exclusively from the command line and
+the project knows nothing about it. Only the folder needs to exist and be
+configured.
 
-Diese Stufe genügt, wenn Updates und Backups von einer Person mit Shell-Zugang
-gepflegt werden.
+This stage is enough when updates and backups are maintained by one person
+with shell access.
 
-## Stufe 2: Oberfläche im Adminbereich
+## Stage 2: UI in the admin area
 
-`ui/panel.php` ist eine fertige Seite. Sie erwartet, dass das Projekt seine
-**eigene Anmeldung bereits geprüft hat**, bevor die Datei eingebunden wird.
+`ui/panel.php` is a ready-made page. It expects the project to have
+**already checked its own login** before the file is included.
 
-Neue Datei `admin/manage.php` im Projekt:
+New file `admin/manage.php` in the project:
 
 ```php
 <?php
@@ -33,7 +34,7 @@ Neue Datei `admin/manage.php` im Projekt:
 require_once __DIR__ . "/../config.php";
 require_once __DIR__ . "/../includes/functions.php";
 
-// Anmeldung des Projekts – hier steht, was das Projekt ohnehin verwendet.
+// The project's own login check - whatever the project already uses.
 if (empty($_SESSION["admin_logged_in"])) {
     header("Location: login.php");
     exit;
@@ -42,21 +43,22 @@ if (empty($_SESSION["admin_logged_in"])) {
 require __DIR__ . "/../manage-client/ui/panel.php";
 ```
 
-Das Panel bringt eine eigene Prüfung auf `$_SESSION["admin_logged_in"]` mit, damit
-ein direkter Aufruf nicht ungeschützt ist. Verwendet das Projekt ein anderes
-Session-Flag, gibt es zwei Möglichkeiten:
+The panel brings its own check on `$_SESSION["admin_logged_in"]`, so a
+direct call isn't left unprotected. If the project uses a different session
+flag, there are two options:
 
-1. den Guard in `ui/panel.php` anpassen (der Ordner wird ohnehin pro Projekt gepflegt), oder
-2. vor dem Einbinden `define("MANAGE_PANEL_SKIP_AUTH_GUARD", true);` setzen, wenn die
-   Anmeldung im aufrufenden Skript bereits sichergestellt ist.
+1. adjust the guard in `ui/panel.php` (this folder is maintained per project
+   anyway), or
+2. set `define("MANAGE_PANEL_SKIP_AUTH_GUARD", true);` before including it,
+   if the login is already guaranteed in the calling script.
 
-Die zweite Variante deaktiviert **nur** die zusätzliche Prüfung des Panels. Wer sie
-setzt, ohne vorher selbst zu prüfen, veröffentlicht Update- und Backup-Funktionen
-im Internet.
+The second option **only** disables the panel's extra check. Setting it
+without checking the login yourself publishes update and backup functions
+to the internet.
 
-### Statusblock auf einer bestehenden Seite
+### Status block on an existing page
 
-Für eine Einstellungsseite genügt oft der kleine Block:
+For a settings page, the small block is often enough:
 
 ```php
 <?php
@@ -65,77 +67,78 @@ include __DIR__ . "/../manage-client/ui/status-partial.php";
 ?>
 ```
 
-Er rendert nur ein Fragment, wirft nie eine Exception und zeigt Version,
-Update-Verfügbarkeit, letztes Backup und offene Migrationen.
+It renders only a fragment, never throws an exception, and shows version,
+update availability, last backup and pending migrations.
 
-## Stufe 3: Funktionsaufrufe im Projekt
+## Stage 3: function calls in the project
 
-Alle Funktionen aus [04_FUNCTION_API](04_FUNCTION_API.md) können direkt aufgerufen
-werden:
+All functions from [04_FUNCTION_API](04_FUNCTION_API.md) can be called
+directly:
 
 ```php
 require_once __DIR__ . "/manage-client/lib/client.php";
 
-// z. B. auf dem Admin-Dashboard: automatisches Backup, wenn fällig
+// e.g. on the admin dashboard: automatic backup when due
 manageBackupCreateAutomaticIfDue();
 ```
 
-Dieser Aufruf ist die Variante für Hosting ohne Cron: Er erstellt ein Backup, wenn
-seit dem letzten automatischen Backup `MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` vergangen
-sind, und gibt sonst sofort `null` zurück. Ein Backup dauert je nach Datenmenge
-mehrere Sekunden – deshalb gehört der Aufruf auf eine selten geladene Adminseite,
-nicht auf jede Seite des Projekts.
+This call is the option for hosting without cron: it creates a backup once
+`MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` has passed since the last automatic
+backup, and otherwise returns `null` immediately. A backup takes several
+seconds depending on the amount of data — so the call belongs on a rarely
+loaded admin page, not on every page of the project.
 
 ## Cron
 
-Cron ist der empfohlene Weg. Beispielzeilen liegen in
+Cron is the recommended path. Example lines live in
 `examples/cron/manage-client.cron`:
 
 ```cron
-# Backup, jede Nacht um 03:20 Uhr
-20 3 * * * /usr/bin/php /pfad/zum/projekt/manage-client/bin/manage-client.php backup --trigger=cron --quiet
+# Backup, every night at 03:20.
+20 3 * * * /usr/bin/php /path/to/project/manage-client/bin/manage-client.php backup --trigger=cron --quiet
 
-# Statusmeldung an den Manage-Server, stündlich
-7 * * * * /usr/bin/php /pfad/zum/projekt/manage-client/bin/manage-client.php heartbeat --quiet
+# Status report to the Manage server, hourly.
+7 * * * * /usr/bin/php /path/to/project/manage-client/bin/manage-client.php heartbeat --quiet
 
-# Update-Prüfung, werktags um 08:00 Uhr (Exit 2 = Update verfügbar)
-0 8 * * 1-5 /usr/bin/php /pfad/zum/projekt/manage-client/bin/manage-client.php check --quiet
+# Update check, weekdays at 08:00 (exit 2 = update available).
+0 8 * * 1-5 /usr/bin/php /path/to/project/manage-client/bin/manage-client.php check --quiet
 ```
 
-`--quiet` unterdrückt die normale Ausgabe; Fehler gehen weiterhin auf STDERR und
-werden von Cron per Mail zugestellt. Updates werden bewusst **nicht** automatisch
-eingespielt: `update` bleibt eine bewusste Entscheidung.
+`--quiet` suppresses normal output; errors still go to STDERR and are
+delivered by cron via mail. Updates are deliberately **not** installed
+automatically: `update` remains a deliberate decision.
 
-## .gitignore des Projekts
+## The project's .gitignore
 
-Ins `.gitignore` des Projekts gehören:
+The project's `.gitignore` should contain:
 
 ```gitignore
 manage-client/config.php
 data/manage/
 ```
 
-Der übrige Inhalt von `manage-client/` **soll** eingecheckt werden, damit er Teil
-des Release-Pakets ist und mit ausgerollt wird.
+The rest of `manage-client/` **should** be checked in, so it's part of the
+release package and gets deployed along with it.
 
-## Verzeichnisse und Rechte
+## Directories and permissions
 
-PHP braucht Schreibrechte auf:
+PHP needs write access to:
 
-- `data/manage/backups/` – lokale Archive
-- `data/manage/work/` – Arbeitsverzeichnis für Updates (wird nach jedem Lauf geleert)
-- `data/manage/updates/` – Sicherungskopien der überschriebenen Dateien
-- den **gesamten Anwendungsstamm**, sofern Updates eingespielt werden sollen
+- `data/manage/backups/` – local archives
+- `data/manage/work/` – working directory for updates (cleared after every run)
+- `data/manage/updates/` – copies of the files an update overwrote
+- the **entire application root**, if updates are to be deployed
 
-Fehlen Schreibrechte im Anwendungsstamm, schlägt ein Update mittendrin fehl. Siehe
-[09_TROUBLESHOOTING](09_TROUBLESHOOTING.md).
+Without write access in the application root, an update fails partway
+through. See [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md).
 
-Diese Verzeichnisse dürfen nicht über das Web erreichbar sein. Bei Apache erledigt
-das üblicherweise die `.htaccess` des Projekts; das mitgelieferte
-`manage-client/.htaccess` schützt zusätzlich `config.php`, `lib/` und `bin/`.
+These directories must not be reachable over the web. On Apache, the
+project's own `.htaccess` usually handles that; the bundled
+`manage-client/.htaccess` additionally protects `config.php`, `lib/` and
+`bin/`.
 
-## Weiter
+## Next
 
-- [03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md) – alle Konstanten
-- [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md) – was gesichert wird
-- [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md) – wie Release-Pakete gebaut werden
+- [03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md) – every constant
+- [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md) – what gets backed up
+- [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md) – how release packages are built

+ 77 - 75
client-package/docs/03_CONFIG_REFERENCE.md

@@ -1,119 +1,121 @@
-# Konfigurationsreferenz
+# Configuration Reference
 
-## Überblick
+## Overview
 
-Alle Konstanten stehen in `manage-client/config.php`, kopiert aus
-`manage-client/config.sample.php`. Jede Konstante hat einen Standardwert in
-`manage-client/lib/client.php`; eine minimale `config.php` braucht nur die drei
-Verbindungswerte und `MANAGE_BACKUP_SOURCES`.
+All constants live in `manage-client/config.php`, copied from
+`manage-client/config.sample.php`. Every constant has a default in
+`manage-client/lib/client.php`; a minimal `config.php` only needs the three
+connection values plus `MANAGE_BACKUP_SOURCES`.
 
-Konstanten werden mit `define()` gesetzt, nicht als Array. Wird eine Konstante
-bereits vom Projekt definiert, gewinnt der Wert des Projekts.
+Constants are set with `define()`, not as an array. If a constant is
+already defined by the project, the project's value wins.
 
-## Verbindung
+## Connection
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_SERVER_URL` | `""` | Basis-URL des Manage-Servers, ohne `/api` und ohne Schrägstrich am Ende |
-| `MANAGE_INSTANCE` | `""` | Kennung der Instanz, wie auf dem Server angelegt |
-| `MANAGE_TOKEN` | `""` | Geheimes Token der Instanz, einmalig beim Anlegen angezeigt |
-| `MANAGE_HTTP_TIMEOUT` | `15` | Sekunden für Manifest und Heartbeat |
-| `MANAGE_HTTP_TIMEOUT_LONG` | `300` | Sekunden für Paket-Download und Backup-Upload |
+| `MANAGE_SERVER_URL` | `""` | base URL of the Manage server, without `/api` and without a trailing slash |
+| `MANAGE_INSTANCE` | `""` | instance id, as created on the server |
+| `MANAGE_TOKEN` | `""` | the instance's secret token, shown once when created |
+| `MANAGE_HTTP_TIMEOUT` | `15` | seconds for manifest and heartbeat |
+| `MANAGE_HTTP_TIMEOUT_LONG` | `300` | seconds for package download and backup upload |
 
-Fehlt einer der ersten drei Werte, melden alle Funktionen mit Serverkontakt einen
-Konfigurationsfehler. Lokale Backups funktionieren trotzdem, sofern
-`MANAGE_BACKUP_UPLOAD` auf `false` steht.
+If any of the first three values is missing, every function that contacts
+the server reports a configuration error. Local backups still work as long
+as `MANAGE_BACKUP_UPLOAD` is `false`.
 
-## Anwendungslayout
+## Application layout
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_APP_ROOT` | Elternverzeichnis von `manage-client/` | Wurzel der Anwendung. Ziel des Updates, Basis aller relativen Backup-Pfade |
-| `MANAGE_VERSION_FILE` | `MANAGE_APP_ROOT . "/VERSION"` | Datei mit der installierten Version |
-| `MANAGE_VERSION_CONSTANT` | `null` | Name der Konstante in dieser Datei, oder `null` für eine reine Textdatei |
-| `MANAGE_WORK_DIR` | `data/manage/work/` | Arbeitsverzeichnis für Updates, wird nach jedem Lauf geleert |
-| `MANAGE_UPDATE_BACKUP_DIR` | `data/manage/updates/` | Sicherungskopien der überschriebenen Dateien |
-| `MANAGE_BACKUP_DIR` | `data/manage/backups/` | Lokale Backup-Archive |
-| `MANAGE_LOG_FILE` | `data/manage/manage-client.log` | JSONL-Protokoll des Clients |
+| `MANAGE_APP_ROOT` | parent directory of `manage-client/` | root of the application. Update target, base of every relative backup path |
+| `MANAGE_VERSION_FILE` | `MANAGE_APP_ROOT . "/VERSION"` | file holding the installed version |
+| `MANAGE_VERSION_CONSTANT` | `null` | name of the constant in that file, or `null` for a plain text file |
+| `MANAGE_WORK_DIR` | `data/manage/work/` | working directory for updates, cleared after every run |
+| `MANAGE_UPDATE_BACKUP_DIR` | `data/manage/updates/` | copies of the files an update overwrote |
+| `MANAGE_BACKUP_DIR` | `data/manage/backups/` | local backup archives |
+| `MANAGE_LOG_FILE` | `data/manage/manage-client.log` | the client's JSONL log |
 
-### Versionsdatei
+### Version file
 
-Zwei Varianten werden unterstützt.
+Two variants are supported.
 
-PHP-Datei mit Konstante:
+PHP file with a constant:
 
 ```php
 define("MANAGE_VERSION_FILE", MANAGE_APP_ROOT . "/includes/version.php");
 define("MANAGE_VERSION_CONSTANT", "APP_VERSION");
 ```
 
-Reine Textdatei, die nur `v1.2.3` enthält:
+Plain text file that only contains `v1.2.3`:
 
 ```php
 define("MANAGE_VERSION_FILE", MANAGE_APP_ROOT . "/VERSION");
 define("MANAGE_VERSION_CONSTANT", null);
 ```
 
-Die Version wird bei der Variante mit Konstante per regulärem Ausdruck aus der Datei
-gelesen und die Datei dabei **nicht** ausgeführt. Damit funktioniert das Auslesen
-auch dann, wenn die Konstante im laufenden Prozess bereits mit dem alten Wert
-definiert ist – zum Beispiel unmittelbar nach einem Update.
+With the constant variant, the version is read from the file with a regular
+expression, and the file is **not** executed. That way reading it still
+works even when the constant is already defined in the running process with
+the old value — for example right after an update.
 
-Die Version selbst wird nie vom Client geschrieben: Sie ist Teil des Release-Pakets
-und ändert sich als Nebeneffekt des Dateikopierens.
+The version itself is never written by the client: it's part of the release
+package and changes as a side effect of copying files.
 
 ## Update
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_UPDATE_PROTECTED_PATHS` | `["config.php", "data/", ".git/", "manage-client/config.php"]` | Pfade, die nie überschrieben werden |
-| `MANAGE_UPDATE_SANITY_PATHS` | `["index.php"]` | Das Paket muss mindestens einen dieser Pfade enthalten |
-| `MANAGE_UPDATE_POST_HOOK` | `null` | Callback nach erfolgreichem Update |
-| `MANAGE_MIGRATIONS_DIR` | `MANAGE_APP_ROOT . "/migrations"` | Verzeichnis mit Migrationsskripten, `null` deaktiviert sie |
-| `MANAGE_MIGRATIONS_STATE` | `data/manage/migrations.json` | Welche Migrationen bereits gelaufen sind |
+| `MANAGE_UPDATE_PROTECTED_PATHS` | `["config.php", "data/", ".git/", "manage-client/config.php"]` | paths that are never overwritten |
+| `MANAGE_UPDATE_SANITY_PATHS` | `["index.php"]` | the package must contain at least one of these paths |
+| `MANAGE_UPDATE_POST_HOOK` | `null` | callback after a successful update |
+| `MANAGE_MIGRATIONS_DIR` | `MANAGE_APP_ROOT . "/migrations"` | directory with migration scripts, `null` disables them |
+| `MANAGE_MIGRATIONS_STATE` | `data/manage/migrations.json` | which migrations have already run |
 
-Pfade in `MANAGE_UPDATE_PROTECTED_PATHS` sind relativ zu `MANAGE_APP_ROOT`. Ein
-Eintrag mit Schrägstrich am Ende schützt das Verzeichnis samt Inhalt; ohne
-Schrägstrich wird der genaue Pfad geschützt, ein Verzeichnis aber ebenfalls
-mitsamt Inhalt. `config.php` und das Datenverzeichnis gehören immer dazu.
+Paths in `MANAGE_UPDATE_PROTECTED_PATHS` are relative to `MANAGE_APP_ROOT`.
+An entry with a trailing slash protects the directory and its contents;
+without a slash, the exact path is protected, but a directory is likewise
+protected with its contents. `config.php` and the data directory always
+belong here.
 
-`MANAGE_UPDATE_SANITY_PATHS` verhindert, dass ein völlig fremdes ZIP über die
-Anwendung kopiert wird. Der Wert sollte eine Datei oder ein Verzeichnis benennen,
-das in jedem Release enthalten ist.
+`MANAGE_UPDATE_SANITY_PATHS` prevents a completely unrelated ZIP from being
+copied over the application. The value should name a file or directory that
+every release contains.
 
-Hook und Migrationen sind in [07_POST_UPDATE_HOOKS](07_POST_UPDATE_HOOKS.md)
-beschrieben.
+The hook and migrations are described in
+[07_POST_UPDATE_HOOKS](07_POST_UPDATE_HOOKS.md).
 
 ## Backup
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_BACKUP_SOURCES` | `[["as" => "data", "glob" => "data/*.json"]]` | Was ins Archiv kommt |
-| `MANAGE_BACKUP_DATABASE` | `null` | Optionaler MySQL-Dump |
-| `MANAGE_BACKUP_LOCAL_RETENTION` | `4` | Lokale Archive, die behalten werden. Minimum 1 |
-| `MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` | `604800` | Intervall für `manageBackupCreateAutomaticIfDue()`, `0` deaktiviert |
-| `MANAGE_BACKUP_COMPRESS` | `true` | Einträge im ZIP komprimieren (Deflate) |
-| `MANAGE_BACKUP_UPLOAD` | `true` | Jedes neue Backup an den Manage-Server senden |
-| `MANAGE_BACKUP_REMOTE_TARGETS` | `[]` | Zusätzliche Ziele: `s3`, `sftp`, `custom` |
+| `MANAGE_BACKUP_SOURCES` | `[["as" => "data", "glob" => "data/*.json"]]` | what goes into the archive |
+| `MANAGE_BACKUP_DATABASE` | `null` | optional MySQL dump |
+| `MANAGE_BACKUP_LOCAL_RETENTION` | `4` | local archives kept. Minimum 1 |
+| `MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` | `604800` | interval for `manageBackupCreateAutomaticIfDue()`, `0` disables it |
+| `MANAGE_BACKUP_COMPRESS` | `true` | compress entries in the ZIP (deflate) |
+| `MANAGE_BACKUP_UPLOAD` | `true` | send every new backup to the Manage server |
+| `MANAGE_BACKUP_REMOTE_TARGETS` | `[]` | extra targets: `s3`, `sftp`, `custom` |
 
-Aufbau von Quellen und Zielen: [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md).
+Layout of sources and targets: [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md).
 
-`MANAGE_BACKUP_COMPRESS` benötigt zlib, das in PHP standardmäßig vorhanden ist.
-Fehlt es, wird ohne Kompression geschrieben statt abzubrechen. Für Archive, die
-überwiegend aus JPEGs bestehen, bringt Kompression fast nichts; für SQL-Dumps sehr viel.
+`MANAGE_BACKUP_COMPRESS` needs zlib, which is present in PHP by default. If
+it's missing, the client writes without compression instead of aborting.
+For archives that are mostly JPEGs, compression gains almost nothing; for
+SQL dumps it gains a lot.
 
-Die lokale Aufbewahrung ist unabhängig von der Aufbewahrung auf dem Manage-Server.
-Auf dem Server wird sie zentral in den Servereinstellungen gepflegt, üblicherweise
-deutlich höher als lokal.
+Local retention is independent of retention on the Manage server. On the
+server it's maintained centrally in the server settings, usually
+considerably higher than locally.
 
-## Zusammenspiel mit dem Projekt
+## Interplay with the project
 
-Definiert das Projekt eine Konstante bereits selbst, gewinnt sie, weil `config.php`
-des Clients vor den Standardwerten geladen wird und alle Standardwerte mit
-`if (!defined(...))` gesetzt sind. So kann ein Projekt seine Pfade zentral
-konfigurieren und der Client sie übernehmen.
+If the project already defines a constant itself, that value wins, because
+the client's `config.php` loads before the defaults, and every default is
+set with `if (!defined(...))`. This lets a project configure its paths
+centrally and have the client pick them up.
 
-## Weiter
+## Next
 
-- [04_FUNCTION_API](04_FUNCTION_API.md) – die aufrufbaren Funktionen
-- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – Fehlermeldungen und Ursachen
+- [04_FUNCTION_API](04_FUNCTION_API.md) – the callable functions
+- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – error messages and their causes

+ 79 - 75
client-package/docs/04_FUNCTION_API.md

@@ -1,57 +1,58 @@
-# Funktions-API
+# Function API
 
-## Überblick
+## Overview
 
-Alle Funktionen stehen nach einem einzigen `require` zur Verfügung:
+All functions become available after a single `require`:
 
 ```php
 require_once __DIR__ . "/manage-client/lib/client.php";
 ```
 
-Die Kommandozeile (`bin/manage-client.php`) und die Oberfläche (`ui/panel.php`)
-enthalten **keine eigene Logik**, sondern rufen genau diese Funktionen auf. Eine
-Funktion verhält sich deshalb identisch, egal wie sie ausgelöst wird.
+The command line (`bin/manage-client.php`) and the UI (`ui/panel.php`)
+contain **no logic of their own** — they call exactly these functions. A
+function therefore behaves identically no matter how it's triggered.
 
-Fehler werden als `RuntimeException` geworfen. Ausnahmen von dieser Regel sind
-ausdrücklich vermerkt.
+Errors are thrown as `RuntimeException`. Exceptions to this rule are noted
+explicitly.
 
 ## Status
 
 ### `manageClientStatus(): array`
 
-Sammelaufruf für Oberflächen. **Wirft nie**: Jeder Fehler landet im Rückgabewert,
-damit eine Einstellungsseite auch bei nicht erreichbarem Server rendert.
+Collective call for UIs. **Never throws**: every error ends up in the
+return value, so a settings page still renders even when the server is
+unreachable.
 
 ```php
 [
-    "instance"           => "mein-projekt-prod",
+    "instance"           => "my-project-prod",
     "server_url"         => "https://manage.example.org",
     "configured"         => true,
-    "version"            => "v1.2.3",   // "" wenn nicht ermittelbar
+    "version"            => "v1.2.3",   // "" if it can't be determined
     "php_version"        => "8.3.6",
-    "update"             => [...],      // Rückgabe von manageUpdateCheck(), oder null
-    "update_error"       => null,       // Fehlermeldung, wenn die Prüfung scheiterte
-    "backups"            => [...],      // Rückgabe von manageBackupList()
+    "update"             => [...],      // return value of manageUpdateCheck(), or null
+    "update_error"       => null,       // error message if the check failed
+    "backups"            => [...],      // return value of manageBackupList()
     "last_backup_at"     => "2026-08-20T09:21:04+00:00",
     "pending_migrations" => [...],
-    "errors"             => [],         // nicht-fatale Warnungen
+    "errors"             => [],         // non-fatal warnings
 ]
 ```
 
 ### `manageClientVersion(): string`
 
-Installierte Version, zum Beispiel `"v1.2.3"`. Gibt `""` zurück, wenn sie nicht
-ermittelbar ist; das ist kein Fehler, sondern "unbekannt".
+Installed version, for example `"v1.2.3"`. Returns `""` if it can't be
+determined; that's not an error, just "unknown".
 
 ### `manageClientConfigured(): bool`
 
-Ob Server-URL, Instanz und Token gesetzt sind.
+Whether server URL, instance and token are set.
 
 ## Update
 
 ### `manageUpdateCheck(): array`
 
-Holt das Manifest und vergleicht die Versionen.
+Fetches the manifest and compares versions.
 
 ```php
 [
@@ -68,22 +69,22 @@ Holt das Manifest und vergleicht die Versionen.
 ]
 ```
 
-Ist die installierte Version unbekannt, gilt `available => true`, damit eine
-Installation ohne lesbare Versionsdatei nicht dauerhaft blockiert.
+If the installed version is unknown, `available => true` holds, so an
+installation without a readable version file isn't permanently blocked.
 
 ### `manageUpdateApply(array $options = []): array`
 
-Lädt das Paket, prüft Größe und SHA-256, entpackt es, rollt es aus und führt
-anschließend den Post-Update-Schritt aus.
+Downloads the package, checks size and SHA-256, extracts it, deploys it,
+and then runs the post-update step.
 
-Optionen:
+Options:
 
-| Option | Bedeutung |
+| Option | Meaning |
 |---|---|
-| `force` | Auch ausrollen, wenn keine neuere Version vorliegt |
-| `skip_hook` | Nur Dateien ausrollen, weder Migrationen noch Callback ausführen |
+| `force` | deploy even when no newer version is available |
+| `skip_hook` | only deploy files, run neither migrations nor the callback |
 
-Rückgabe:
+Return value:
 
 ```php
 [
@@ -92,25 +93,25 @@ Rückgabe:
     "to_version"      => "v1.3.0",
     "copied"          => 128,
     "backed_up"       => 126,
-    "skipped"         => 2,      // geschützte Pfade
+    "skipped"         => 2,      // protected paths
     "removed_backups" => 1,
     "backup_dir"      => "/…/data/manage/updates/20260820-092114-v1.3.0",
-    "hook"            => [...],  // siehe unten
+    "hook"            => [...],  // see below
 ]
 ```
 
-Wirft, wenn das **Ausrollen** scheitert. Scheitert nur der Post-Update-Schritt,
-kehrt die Funktion normal zurück und `hook["success"]` ist `false` – die Dateien
-sind dann bereits ausgerollt. Aufrufer müssen beides unterscheiden; siehe
+Throws if **deployment** fails. If only the post-update step fails, the
+function returns normally and `hook["success"]` is `false` — the files are
+then already deployed. Callers must distinguish between the two; see
 [07_POST_UPDATE_HOOKS](07_POST_UPDATE_HOOKS.md).
 
-Wichtig: Das Ausrollen überschreibt Dateien im laufenden Betrieb. Es gibt keine
-Wartungsseite und keine Rücknahme. Die überschriebenen Dateien liegen als Kopie in
-`backup_dir`, aber ausschließlich für die manuelle Wiederherstellung.
+Important: deployment overwrites files while the application is live. There
+is no maintenance page and no rollback. The overwritten files sit as copies
+in `backup_dir`, but exclusively for manual restoration.
 
 ### `manageUpdatePendingMigrations(): array`
 
-Noch nicht ausgeführte Migrationen in Ausführungsreihenfolge:
+Migrations not yet run, in execution order:
 
 ```php
 [["id" => "2026-08-20-01-add-index", "path" => "/…/migrations/2026-08-20-01-add-index.php"]]
@@ -118,7 +119,7 @@ Noch nicht ausgeführte Migrationen in Ausführungsreihenfolge:
 
 ### `manageUpdateRunMigrations(array $context = []): array`
 
-Führt die offenen Migrationen aus. **Wirft nicht**, sondern meldet:
+Runs the pending migrations. **Never throws**, reports instead:
 
 ```php
 [
@@ -126,7 +127,7 @@ Führt die offenen Migrationen aus. **Wirft nicht**, sondern meldet:
     "applied" => ["2026-08-20-01-add-index"],
     "failed"  => "2026-08-20-02-backfill",
     "error"   => "SQLSTATE[42S22]: …",
-    "pending" => 2,   // inklusive der fehlgeschlagenen
+    "pending" => 2,   // including the failed one
 ]
 ```
 
@@ -134,10 +135,11 @@ Führt die offenen Migrationen aus. **Wirft nicht**, sondern meldet:
 
 ### `manageBackupCreate(string $trigger = "manual"): array`
 
-Erstellt ein lokales Archiv und lädt es hoch, sofern `MANAGE_BACKUP_UPLOAD` aktiv ist.
+Creates a local archive and uploads it, provided `MANAGE_BACKUP_UPLOAD` is
+active.
 
-`$trigger` ist frei wählbar; üblich sind `manual`, `automatic`, `cron`, `update`.
-Nur `automatic` und `cron` zählen für die Intervallprüfung.
+`$trigger` is free-form; common values are `manual`, `automatic`, `cron`,
+`update`. Only `automatic` and `cron` count toward the interval check.
 
 ```php
 [
@@ -149,48 +151,50 @@ Nur `automatic` und `cron` zählen für die Intervallprüfung.
     "source_bytes"   => 63,
     "sha256"         => "…",
     "app_version"    => "v1.2.3",
-    "database"       => null,   // oder ["tables" => 12, "rows" => 4711, …]
+    "database"       => null,   // or ["tables" => 12, "rows" => 4711, …]
     "remote_uploads" => [
-        ["target" => "Manage-Server", "type" => "manage", "success" => true, …],
+        ["target" => "Manage Server", "type" => "manage", "success" => true, …],
     ],
 ]
 ```
 
-Ein fehlgeschlagener Upload macht das lokale Archiv **nicht** ungültig: Der Fehler
-steht in `remote_uploads[].error` und im Protokoll, die Funktion wirft nicht. Wirft
-sie doch, ist das Archiv selbst nicht zustande gekommen.
+A failed upload does **not** invalidate the local archive: the error sits
+in `remote_uploads[].error` and in the log; the function doesn't throw. If
+it does throw, the archive itself never came into being.
 
-Gleichzeitige Läufe werden über eine Sperrdatei verhindert; der zweite Lauf wirft
-sofort `Es läuft bereits ein Backup.`
+Concurrent runs are prevented by a lock file; a second run throws
+immediately with `Es läuft bereits ein Backup.` ("a backup is already
+running" — the literal, still German, message text).
 
 ### `manageBackupCreateAutomaticIfDue(): ?array`
 
-Erstellt ein Backup, wenn seit dem letzten automatischen Backup
-`MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` vergangen sind, sonst `null`. Für Hosting ohne
-Cron gedacht; gehört auf eine selten geladene Adminseite.
+Creates a backup once `MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` has passed since
+the last automatic backup, otherwise `null`. Meant for hosting without
+cron; belongs on a rarely loaded admin page.
 
 ### `manageBackupList(): array`
 
-Lokale Archive, neuestes zuerst. Selbstheilend: Einträge ohne Datei werden entfernt,
-Größen werden von der Festplatte aktualisiert.
+Local archives, newest first. Self-healing: entries without a file are
+removed, sizes are refreshed from disk.
 
 ### `manageBackupUpload(string $archivePath, array $meta = []): array`
 
-Lädt ein vorhandenes Archiv zum Manage-Server. Wird von `manageBackupCreate()`
-automatisch aufgerufen und ist nur für Sonderfälle einzeln nötig – etwa um ein
-Archiv nach einem Serverausfall nachzureichen.
+Uploads an existing archive to the Manage server. Called automatically by
+`manageBackupCreate()`; only needed on its own for special cases — for
+example, resending an archive after a server outage.
 
 ### `manageBackupPath(string $filename): string`
 
-Absoluter Pfad eines lokalen Archivs. Validiert den Dateinamen streng, damit ein
-Download-Formular keinen beliebigen Pfad ausliefern kann.
+Absolute path of a local archive. Validates the filename strictly, so a
+download form can't be made to serve an arbitrary path.
 
 ## Heartbeat
 
 ### `manageHeartbeatSend(): array`
 
-Meldet Version, PHP-Version, freien Speicher, offene Migrationen und den Zeitpunkt
-des letzten Backups. Die Antwort enthält nebenbei die Update-Information:
+Reports version, PHP version, free disk space, pending migrations and the
+time of the last backup. The response includes the update information as a
+side effect:
 
 ```php
 ["success" => true, "latest" => "v1.3.0", "update_available" => true, "server_time" => "…"]
@@ -198,33 +202,33 @@ des letzten Backups. Die Antwort enthält nebenbei die Update-Information:
 
 ### `manageHeartbeatSendQuietly(): ?array`
 
-Wie oben, wirft aber nie und gibt bei Fehlern `null` zurück. Für Aufrufe innerhalb
-des Projekts, in denen ein nicht erreichbarer Server folgenlos bleiben soll.
+Same as above, but never throws and returns `null` on error. For calls
+inside the project where an unreachable server should have no consequence.
 
-## Hilfsfunktionen
+## Helper functions
 
-| Funktion | Zweck |
+| Function | Purpose |
 |---|---|
-| `manageFormatBytes(int $bytes): string` | `1.234.567` → `1,18 MB` |
-| `manageClientLog(string $level, string $message, array $context = []): void` | Eine Zeile ins Client-Protokoll. Wirft nie |
-| `manageRemoteCapabilities(): array` | Welche konfigurierten Zieltypen dieses System unterstützt |
+| `manageFormatBytes(int $bytes): string` | `1234567` → `1.18 MB` |
+| `manageClientLog(string $level, string $message, array $context = []): void` | one line into the client log. Never throws |
+| `manageRemoteCapabilities(): array` | which configured target types this system supports |
 
-## Beispiel
+## Example
 
 ```php
 require_once __DIR__ . "/manage-client/lib/client.php";
 
 $check = manageUpdateCheck();
 if ($check["available"]) {
-    manageBackupCreate("update");          // vor dem Update sichern
+    manageBackupCreate("update");          // back up before the update
     $result = manageUpdateApply();
 
     if (!$result["hook"]["success"]) {
-        // Dateien sind ausgerollt, der Post-Update-Schritt nicht.
-        error_log("Migration fehlgeschlagen: " . $result["hook"]["error"]);
+        // Files are deployed, the post-update step is not.
+        error_log("Migration failed: " . $result["hook"]["error"]);
     }
 }
 ```
 
-Ein Backup vor dem Update ist bewusst **nicht** eingebaut, sondern eine Zeile im
-Projekt – so bleibt sichtbar, dass es passiert.
+A backup before the update is deliberately **not** built in, but a line in
+the project — so it stays visible that it happens.

+ 103 - 99
client-package/docs/05_BACKUP_SOURCES.md

@@ -1,159 +1,163 @@
-# Backup-Quellen und Ziele
+# Backup Sources and Targets
 
-## Überblick
+## Overview
 
-Was gesichert wird, steht in `MANAGE_BACKUP_SOURCES`. Optional kommt ein
-MySQL-Dump dazu. Wohin gesichert wird, steuern `MANAGE_BACKUP_UPLOAD` und
-`MANAGE_BACKUP_REMOTE_TARGETS`.
+What gets backed up lives in `MANAGE_BACKUP_SOURCES`. A MySQL dump can be
+added optionally. Where it gets backed up to is controlled by
+`MANAGE_BACKUP_UPLOAD` and `MANAGE_BACKUP_REMOTE_TARGETS`.
 
-Relevante Dateien:
+Relevant files:
 
-- `manage-client/lib/backup.php` – Sammeln, Archivieren, Aufbewahrung, Upload
-- `manage-client/lib/zip.php` – ZIP-Erzeugung ohne externe Bibliothek
-- `manage-client/lib/mysql.php` – optionaler Datenbank-Dump
-- `manage-client/lib/remote.php` – zusätzliche Ziele
+- `manage-client/lib/backup.php` – collecting, archiving, retention, upload
+- `manage-client/lib/zip.php` – ZIP creation without an external library
+- `manage-client/lib/mysql.php` – optional database dump
+- `manage-client/lib/remote.php` – extra targets
 
-## Quellen deklarieren
+## Declaring sources
 
-Alle Pfade sind relativ zu `MANAGE_APP_ROOT`. Jeder Eintrag verwendet genau eine
-der drei Formen:
+All paths are relative to `MANAGE_APP_ROOT`. Every entry uses exactly one
+of three forms:
 
 ```php
 define("MANAGE_BACKUP_SOURCES", [
-    // Nicht-rekursives Glob-Muster
+    // Non-recursive glob pattern
     ["as" => "data", "glob" => "data/*.json"],
 
-    // Verzeichnis, rekursiv
+    // Directory, recursive
     ["as" => "data/uploads", "dir" => "data/uploads"],
 
-    // Einzelne Datei
+    // Single file
     ["as" => "config", "file" => "config.dist.php"],
 ]);
 ```
 
-`as` ist das Präfix im Archiv. Ohne `as` landet der Eintrag auf oberster Ebene –
-bei `dir` wird der Verzeichnisname als Präfix verwendet.
+`as` is the prefix inside the archive. Without `as`, the entry lands at the
+top level — for `dir`, the directory name is used as the prefix.
 
-Das obige Beispiel erzeugt:
+The example above produces:
 
 ```text
 data/orders.json
 data/settings.json
-data/uploads/2026/bild.jpg
+data/uploads/2026/image.jpg
 config/config.dist.php
 ```
 
-### Was automatisch übersprungen wird
+### What gets skipped automatically
 
-- Dateien, deren Name mit einem Punkt beginnt
-- Dateien mit den Endungen `.tmp` und `.part`
-- nicht lesbare Dateien
+- files whose name starts with a dot
+- files with the extensions `.tmp` and `.part`
+- unreadable files
 
-Ergeben zwei Quellen denselben Archivpfad, gewinnt die erste. Ein Archiv enthält
-nie zwei Einträge mit gleichem Namen.
+If two sources produce the same archive path, the first one wins. An
+archive never contains two entries with the same name.
 
-### Was **nicht** hineingehört
+### What does **not** belong in it
 
-- `config.php` mit echten Zugangsdaten – ein Backup wird an den Server übertragen
-  und dort heruntergeladen; siehe [10_SECURITY](10_SECURITY.md)
-- das Backup-Verzeichnis selbst
-- Protokolle und Cache-Verzeichnisse
-- die Anwendungsdateien: Die kommen aus dem Release-Paket, nicht aus dem Backup
+- `config.php` with real credentials — a backup gets transferred to the
+  server and downloaded there; see [10_SECURITY](10_SECURITY.md)
+- the backup directory itself
+- logs and cache directories
+- the application files: those come from the release package, not from the
+  backup
 
-Ein Backup sichert **Betriebsdaten**, kein vollständiges Systemabbild.
+A backup secures **operational data**, not a complete system image.
 
-## Datenbank-Dump
+## Database dump
 
-Nur aktiv, wenn `MANAGE_BACKUP_DATABASE` gesetzt ist:
+Only active when `MANAGE_BACKUP_DATABASE` is set:
 
 ```php
 define("MANAGE_BACKUP_DATABASE", [
-    "dsn"      => "mysql:host=localhost;dbname=meinprojekt;charset=utf8mb4",
-    "user"     => "meinprojekt",
+    "dsn"      => "mysql:host=localhost;dbname=myproject;charset=utf8mb4",
+    "user"     => "myproject",
     "password" => "…",
 
-    // Optional: von diesen Tabellen nur die Struktur sichern, nicht die Zeilen.
+    // Optional: back up only the structure of these tables, not their rows.
     "skip_data_tables" => ["sessions", "cache"],
 
-    // Optional: Name im Archiv, falls SELECT DATABASE() nichts liefert.
-    "name" => "meinprojekt",
+    // Optional: name inside the archive, if SELECT DATABASE() returns nothing.
+    "name" => "myproject",
 ]);
 ```
 
-Der Dump landet im Archiv als `database/<name>.sql` und enthält für jede Tabelle
-`DROP TABLE IF EXISTS`, das `CREATE TABLE` aus `SHOW CREATE TABLE` und die Zeilen
-als einzelne `INSERT`-Anweisungen. `SET FOREIGN_KEY_CHECKS=0` steht am Anfang, am
-Ende wird wieder auf `1` gesetzt.
+The dump lands in the archive as `database/<name>.sql` and contains, for
+every table, `DROP TABLE IF EXISTS`, the `CREATE TABLE` from `SHOW CREATE
+TABLE`, and the rows as individual `INSERT` statements.
+`SET FOREIGN_KEY_CHECKS=0` sits at the top, set back to `1` at the end.
 
-Eigenschaften und Grenzen:
+Properties and limits:
 
-- Nur PDO, kein `mysqldump`-Aufruf – das ist auf vielen Shared-Hostern die einzige
-  Möglichkeit, funktioniert aber nur für MySQL und MariaDB.
-- Die Zeilen werden in Blöcken von 500 gelesen und sofort geschrieben, sodass der
-  Speicherbedarf nicht mit der Tabellengröße wächst.
-- Binärwerte werden als `0x…`-Literal geschrieben, damit der Dump gültiges ASCII bleibt.
-- Views werden als Struktur gesichert, aber nicht mit Daten.
-- Der Dump ist **nicht** transaktional konsistent. Für eine Anwendung mit
-  gleichzeitigem Schreibverkehr sollte das Backup in einer ruhigen Zeit laufen.
+- PDO only, no `mysqldump` call — on many shared hosts that's the only
+  option available, but it only works for MySQL and MariaDB.
+- Rows are read in blocks of 500 and written immediately, so memory use
+  doesn't grow with table size.
+- Binary values are written as `0x…` literals, so the dump stays valid
+  ASCII.
+- Views are backed up as structure, but without data.
+- The dump is **not** transactionally consistent. For an application with
+  concurrent writes, the backup should run during a quiet period.
 
-Wiederherstellung von Hand:
+Manual restore:
 
 ```bash
-unzip -p backup-20260820-092104.zip database/meinprojekt.sql | mysql meinprojekt
+unzip -p backup-20260820-092104.zip database/myproject.sql | mysql myproject
 ```
 
-## Kompression
+## Compression
 
-`MANAGE_BACKUP_COMPRESS` (Standard `true`) schaltet Deflate ein. Der Unterschied
-ist bei SQL-Dumps und JSON groß, bei bereits komprimierten Bildern praktisch null.
-Fehlt zlib, schreibt der Client unkomprimiert weiter statt abzubrechen.
+`MANAGE_BACKUP_COMPRESS` (default `true`) turns on deflate. The difference
+is large for SQL dumps and JSON, practically zero for already-compressed
+images. If zlib is missing, the client keeps writing uncompressed instead
+of aborting.
 
-Grenzen des ZIP-Formats ohne Zip64: 4 GB pro Datei, 4 GB pro Archiv, 65535 Einträge.
-Wird eine Grenze überschritten, bricht das Backup mit einer klaren Meldung ab.
+Limits of the ZIP format without Zip64: 4 GB per file, 4 GB per archive,
+65535 entries. If a limit is exceeded, the backup aborts with a clear
+message.
 
-## Ziele
+## Targets
 
-### Manage-Server
+### Manage server
 
-Standardziel, aktiv über `MANAGE_BACKUP_UPLOAD` (Standard `true`). Konfiguriert
-wird nichts weiter: Es gelten `MANAGE_SERVER_URL`, `MANAGE_INSTANCE` und
-`MANAGE_TOKEN`. Der Server prüft die mitgesendete Prüfsumme erneut und lehnt
-abweichende Uploads ab.
+Default target, active via `MANAGE_BACKUP_UPLOAD` (default `true`). Nothing
+else needs configuring: `MANAGE_SERVER_URL`, `MANAGE_INSTANCE` and
+`MANAGE_TOKEN` apply. The server re-checks the submitted checksum and
+rejects a mismatching upload.
 
-Für rein lokale Backups:
+For purely local backups:
 
 ```php
 define("MANAGE_BACKUP_UPLOAD", false);
 ```
 
-### Zusätzliche Ziele
+### Extra targets
 
-Jedes Ziel wird unabhängig versucht. Ein Fehlschlag macht weder das lokale Archiv
-noch die anderen Ziele ungültig.
+Every target is attempted independently. A failure invalidates neither the
+local archive nor the other targets.
 
-S3-kompatibler Speicher:
+S3-compatible storage:
 
 ```php
 define("MANAGE_BACKUP_REMOTE_TARGETS", [
     [
-        "name"       => "S3 Archiv",
+        "name"       => "S3 Archive",
         "type"       => "s3",
         "bucket"     => "example-bucket",
         "region"     => "eu-central-1",
-        "prefix"     => "meinprojekt",
+        "prefix"     => "myproject",
         "access_key" => "AKIA…",
         "secret_key" => "…",
-        // Für S3-kompatible Anbieter:
+        // For S3-compatible providers:
         // "endpoint" => "https://fsn1.your-objectstorage.com",
     ],
 ]);
 ```
 
-Signiert wird mit AWS Signature V4 über normale PHP-HTTPS-Streams; eine Bibliothek
-wird nicht benötigt. Das Archiv wird für die Signatur vollständig in den Speicher
-geladen – ein Backup größer als `memory_limit` kann dieses Ziel nicht nutzen.
+Signing uses AWS Signature V4 over plain PHP HTTPS streams; no library is
+required. The archive is loaded fully into memory for signing — a backup
+larger than `memory_limit` cannot use this target.
 
-SFTP (benötigt die PHP-Erweiterung `ssh2`):
+SFTP (needs the PHP `ssh2` extension):
 
 ```php
 [
@@ -163,31 +167,31 @@ SFTP (benötigt die PHP-Erweiterung `ssh2`):
     "port"     => 22,
     "username" => "backup-user",
     "password" => "…",
-    "path"     => "/backups/meinprojekt",
+    "path"     => "/backups/myproject",
 ]
 ```
 
-Mit Schlüssel statt Passwort:
+With a key instead of a password:
 
 ```php
 [
     "type"        => "sftp",
     "host"        => "backup.example.org",
     "username"    => "backup-user",
-    "public_key"  => "/pfad/zu/backup.pub",
-    "private_key" => "/pfad/zu/backup",
-    "password"    => "",        // Passphrase des Schlüssels, sonst leer
-    "path"        => "/backups/meinprojekt",
+    "public_key"  => "/path/to/backup.pub",
+    "private_key" => "/path/to/backup",
+    "password"    => "",        // key passphrase, empty otherwise
+    "path"        => "/backups/myproject",
 ]
 ```
 
-Das Zielverzeichnis muss existieren und beschreibbar sein; es wird nicht angelegt.
+The target directory must exist and be writable; it is not created.
 
-Eigener Uploader:
+Custom uploader:
 
 ```php
 [
-    "name"     => "Eigenes Ziel",
+    "name"     => "Custom Target",
     "type"     => "custom",
     "file"     => MANAGE_APP_ROOT . "/includes/backup-uploader.php",
     "callback" => "myProjectUploadBackup",
@@ -198,22 +202,22 @@ Eigener Uploader:
 function myProjectUploadBackup(string $archivePath, array $metadata, array $target)
 {
     // $metadata: filename, created_at, trigger, sha256, file_count, source_bytes
-    // Erfolg: true oder ein Array zurückgeben.
-    // Fehler: Exception werfen oder ["success" => false, "error" => "…"].
+    // Success: return true or an array.
+    // Failure: throw an exception, or return ["success" => false, "error" => "…"].
     return ["remote_path" => "…"];
 }
 ```
 
-## Protokollierung und Geheimnisse
+## Logging and secrets
 
-Fehlgeschlagene Uploads werden mit HTTP-Status und Antwortauszug protokolliert.
-Zugangsdaten sind davon ausgenommen: Aus der Zielkonfiguration wird nur eine
-Positivliste unkritischer Schlüssel übernommen (`name`, `type`, `url`, `bucket`,
+Failed uploads are logged with HTTP status and a response excerpt.
+Credentials are excluded from that: only an allowlist of non-sensitive keys
+is taken from the target configuration (`name`, `type`, `url`, `bucket`,
 `region`, `prefix`, `endpoint`, `host`, `port`, `username`, `path`, `file`,
-`callback`, `timeout`). `access_key`, `secret_key` und `password` erscheinen nie
-im Protokoll.
+`callback`, `timeout`). `access_key`, `secret_key` and `password` never
+appear in the log.
 
-## Weiter
+## Next
 
-- [04_FUNCTION_API](04_FUNCTION_API.md) – `manageBackupCreate()` und Rückgabewerte
-- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – Fehlermeldungen beim Backup
+- [04_FUNCTION_API](04_FUNCTION_API.md) – `manageBackupCreate()` and its return values
+- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – error messages during backup

+ 85 - 80
client-package/docs/06_UPDATE_PACKAGING.md

@@ -1,91 +1,95 @@
-# Release-Pakete bauen
+# Building Release Packages
 
-## Überblick
+## Overview
 
-Der Updater rollt ein ZIP über den Anwendungsstamm aus: Jede Datei im Paket wird an
-dieselbe relative Position im Projekt kopiert. Damit das funktioniert, muss das
-Paket richtig geschnitten sein.
+The updater rolls a ZIP out over the application root: every file in the
+package is copied to the same relative position in the project. For that
+to work, the package has to be cut correctly.
 
-Relevante Dateien:
+Relevant files:
 
-- `manage-client/lib/updater.php` – Prüfung, Entpacken, Ausrollen
-- in diesem Paket: `scripts/create-release-zip.sh` – fertiges Build-Skript
+- `manage-client/lib/updater.php` – checking, extracting, deploying
+- in this package: `scripts/create-release-zip.sh` – ready-made build script
 
-## Aufbau des Pakets
+## Package layout
 
-Die Wurzel des ZIP **ist** der Anwendungsstamm. Kein zusätzliches Oberverzeichnis:
+The root of the ZIP **is** the application root. No extra top-level
+directory:
 
 ```text
-richtig                        falsch
-------                         ------
-index.php                      meinprojekt-v1.3.0/index.php
-admin/orders.php               meinprojekt-v1.3.0/admin/orders.php
-includes/version.php           meinprojekt-v1.3.0/includes/version.php
+correct                        wrong
+-------                        -----
+index.php                      myproject-v1.3.0/index.php
+admin/orders.php               myproject-v1.3.0/admin/orders.php
+includes/version.php           myproject-v1.3.0/includes/version.php
 manage-client/lib/client.php
 migrations/2026-08-20-01-x.php
 ```
 
-Ein Paket mit Oberverzeichnis würde das Projekt nicht aktualisieren, sondern einen
-neuen Unterordner anlegen.
+A package with a top-level directory wouldn't update the project — it would
+create a new subfolder.
 
-## Was hineingehört
+## What belongs in it
 
-- alle Anwendungsdateien
-- `includes/version.php` (oder die konfigurierte Versionsdatei) mit der **neuen** Version
-- `manage-client/` **ohne** `config.php` – so wird der Client mit aktualisiert
-- `migrations/`, sofern das Release Migrationen mitbringt
+- all application files
+- `includes/version.php` (or the configured version file) with the **new**
+  version
+- `manage-client/` **without** `config.php` — this is how the client itself
+  gets updated too
+- `migrations/`, if the release brings migrations along
 
-## Was draußen bleiben muss
+## What must stay out
 
-| Ausschluss | Grund |
+| Exclusion | Reason |
 |---|---|
-| `config.php` | enthält Zugangsdaten der Zielinstallation |
-| `manage-client/config.php` | enthält Instanz-Token der Zielinstallation |
-| `data/` | Betriebsdaten der Zielinstallation |
-| `.git/` | gehört nicht auf einen Produktivserver |
-| `build/`, `storage/` | Artefakte |
+| `config.php` | holds the target installation's credentials |
+| `manage-client/config.php` | holds the target installation's instance token |
+| `data/` | the target installation's operational data |
+| `.git/` | doesn't belong on a production server |
+| `build/`, `storage/` | build artifacts |
 
-`config.php`, `data/` und `.git/` sind zusätzlich über
-`MANAGE_UPDATE_PROTECTED_PATHS` geschützt: Selbst wenn sie versehentlich im Paket
-landen, werden sie beim Ausrollen übersprungen. Der Ausschluss beim Bauen ist
-trotzdem nötig, weil das Paket sonst fremde Zugangsdaten enthält und auf dem
-Manage-Server heruntergeladen werden kann.
+`config.php`, `data/` and `.git/` are additionally protected via
+`MANAGE_UPDATE_PROTECTED_PATHS`: even if they end up in the package by
+accident, they're skipped during deployment. Excluding them at build time
+is still necessary, because otherwise the package contains someone else's
+credentials and can be downloaded from the Manage server.
 
-## Versionsnummer
+## Version number
 
-Das Format ist `vX.Y.Z` – ohne Suffix, ohne Präfix. Sowohl der Client als auch der
-Server lehnen alles andere ab.
+The format is `vX.Y.Z` — no suffix, no prefix. Both client and server
+reject anything else.
 
-Die Version steht an genau einer Stelle: in der Versionsdatei innerhalb des Pakets.
-Der Client schreibt sie nie selbst; sie ändert sich als Nebeneffekt des
-Dateikopierens. Wird sie beim Bauen vergessen, meldet die Instanz nach dem Update
-weiterhin die alte Version und bietet dasselbe Update erneut an.
+The version lives in exactly one place: the version file inside the
+package. The client never writes it itself; it changes as a side effect of
+copying files. If it's forgotten at build time, the instance keeps
+reporting the old version after the update and keeps offering the same
+update again.
 
-## Build mit dem mitgelieferten Skript
+## Building with the bundled script
 
-Dieses Paket enthält `scripts/create-release-zip.sh`. Das Skript wird nach
-`scripts/` des Projekts kopiert, einmal pro Projekt am Kopf angepasst
-(Produktname, Versionsdatei, Ausschlüsse) und dann im Projektverzeichnis
-aufgerufen:
+This package contains `scripts/create-release-zip.sh`. The script gets
+copied to `scripts/` of the project, adjusted once per project at the top
+(product name, version file, exclusions), and then run from the project
+directory:
 
 ```bash
 ./scripts/create-release-zip.sh v1.3.0
 ```
 
-Das Skript
+The script
 
-1. schreibt die Version in die Versionsdatei,
-2. packt alle von Git verfolgten Dateien abzüglich der Ausschlussliste,
-3. gibt SHA-256 und Größe aus.
+1. writes the version into the version file,
+2. packs every file tracked by Git, minus the exclusion list,
+3. prints SHA-256 and size.
 
-Es verwendet `git ls-files`, damit nur eingecheckte Dateien im Paket landen –
-lokale Experimente und ignorierte Dateien bleiben automatisch draußen.
+It uses `git ls-files`, so only checked-in files end up in the package —
+local experiments and ignored files stay out automatically.
 
-## Build von Hand
+## Building by hand
 
 ```bash
-cd /pfad/zum/projekt
-zip -r ../meinprojekt-v1.3.0.zip . \
+cd /path/to/project
+zip -r ../myproject-v1.3.0.zip . \
     -x 'config.php' \
        'manage-client/config.php' \
        'data/*' \
@@ -93,40 +97,41 @@ zip -r ../meinprojekt-v1.3.0.zip . \
        'build/*'
 ```
 
-Prüfen, was tatsächlich drin ist – dieser Schritt lohnt sich immer:
+Check what's actually in it — this step always pays off:
 
 ```bash
-unzip -l ../meinprojekt-v1.3.0.zip | head -30
+unzip -l ../myproject-v1.3.0.zip | head -30
 ```
 
-## Veröffentlichen
+## Publishing
 
-Im Manage-Server unter **Releases**: Version eintragen, ZIP hochladen. Prüfsumme
-und Größe berechnet der Server selbst; sie werden nie vom Hochladenden übernommen.
-Ein Upload setzt das Release automatisch als aktuell.
+In the Manage server under **Releases**: enter the version, upload the
+ZIP. Checksum and size are computed by the server itself; they are never
+taken from the uploader. An upload automatically sets the release as
+current.
 
-## Was der Client beim Ausrollen prüft
+## What the client checks when deploying
 
-1. Größe und SHA-256 müssen dem Manifest entsprechen, sonst wird die Datei gelöscht.
-2. Jeder Eintrag im ZIP wird gegen Pfad-Ausbruch geprüft (`..`, absolute Pfade,
-   Laufwerksbuchstaben, Nullbytes).
-3. Das Paket muss mindestens einen der Pfade aus `MANAGE_UPDATE_SANITY_PATHS`
-   enthalten.
-4. Beim Kopieren wird jede vorhandene Zieldatei zuerst nach
-   `MANAGE_UPDATE_BACKUP_DIR` gesichert.
+1. Size and SHA-256 must match the manifest, otherwise the file is deleted.
+2. Every entry in the ZIP is checked against path traversal (`..`, absolute
+   paths, drive letters, null bytes).
+3. The package must contain at least one of the paths from
+   `MANAGE_UPDATE_SANITY_PATHS`.
+4. While copying, every existing target file is first backed up to
+   `MANAGE_UPDATE_BACKUP_DIR`.
 
-## Grenzen des Verfahrens
+## Limits of this approach
 
-- **Gelöschte Dateien werden nicht entfernt.** Das Ausrollen ist ein Überlagern.
-  Eine Datei, die es im neuen Release nicht mehr gibt, bleibt in der Installation
-  liegen. Soll sie wirklich verschwinden, gehört das in eine Migration.
-- **Kein Wartungsmodus.** Die Anwendung bleibt während des Kopierens erreichbar.
-  Bei größeren Umbauten sollte in einer Randzeit aktualisiert werden.
-- **Keine Rücknahme.** Die Sicherungskopien in `MANAGE_UPDATE_BACKUP_DIR` sind für
-  die manuelle Wiederherstellung gedacht; es gibt keinen Befehl dafür. Aufbewahrt
-  wird nur der letzte Lauf.
+- **Deleted files are not removed.** Deployment is an overlay. A file no
+  longer present in the new release stays behind in the installation. To
+  make it truly disappear, that belongs in a migration.
+- **No maintenance mode.** The application stays reachable while files are
+  being copied. For larger changes, update during a quiet period.
+- **No rollback.** The backup copies in `MANAGE_UPDATE_BACKUP_DIR` are meant
+  for manual restoration; there's no command for it. Only the most recent
+  run is kept.
 
-## Weiter
+## Next
 
-- [07_POST_UPDATE_HOOKS](07_POST_UPDATE_HOOKS.md) – Migrationen im Paket
+- [07_POST_UPDATE_HOOKS](07_POST_UPDATE_HOOKS.md) – migrations in the package
 - [04_FUNCTION_API](04_FUNCTION_API.md) – `manageUpdateApply()`

+ 82 - 80
client-package/docs/07_POST_UPDATE_HOOKS.md

@@ -1,31 +1,30 @@
-# Post-Update-Hook und Migrationen
+# Post-Update Hook and Migrations
 
-## Überblick
+## Overview
 
-Nach einem erfolgreichen Ausrollen führt der Client einen Post-Update-Schritt aus.
-Er besteht aus zwei unabhängigen Mechanismen, die einzeln oder gemeinsam genutzt
-werden:
+After a successful deployment, the client runs a post-update step. It
+consists of two independent mechanisms, used individually or together:
 
-1. **Migrationen** – geordnete, einmalig laufende Skripte, die mit dem Release
-   ausgeliefert werden. Der übliche Ort für Datenbankänderungen.
-2. **Projekt-Callback** – eine Funktion des Projekts, die nach jedem Update läuft.
-   Für Cache leeren, abgeleitete Dateien neu bauen, Rechte setzen.
+1. **Migrations** – ordered, one-time scripts shipped with the release. The
+   usual place for database changes.
+2. **Project callback** – a project function that runs after every update.
+   For clearing caches, rebuilding derived files, setting permissions.
 
-Relevante Dateien:
+Relevant files:
 
-- `manage-client/lib/hooks.php` – beide Mechanismen
+- `manage-client/lib/hooks.php` – both mechanisms
 - `MANAGE_MIGRATIONS_DIR`, `MANAGE_MIGRATIONS_STATE`, `MANAGE_UPDATE_POST_HOOK`
 
-Reihenfolge: erst die Migrationen, dann der Callback – damit der Callback sich auf
-das neue Schema verlassen kann. Scheitert eine Migration, wird der Callback
-**nicht** ausgeführt.
+Order: migrations first, then the callback — so the callback can rely on
+the new schema. If a migration fails, the callback is **not** run.
 
-## Migrationen
+## Migrations
 
-### Ablage
+### Location
 
-Migrationen liegen im Verzeichnis aus `MANAGE_MIGRATIONS_DIR` (Standard
-`migrations/` im Anwendungsstamm) und werden **mit dem Release-Paket ausgeliefert**.
+Migrations live in the directory from `MANAGE_MIGRATIONS_DIR` (default
+`migrations/` in the application root) and are **shipped with the release
+package**.
 
 ```text
 migrations/
@@ -33,13 +32,13 @@ migrations/
   2026-08-21-01-backfill-categories.php
 ```
 
-Ausgeführt wird in **Dateinamen-Reihenfolge**. Ein Datum als Präfix mit laufender
-Nummer sortiert zuverlässig. Der Dateiname ohne `.php` ist die Kennung der
-Migration; wird eine bereits ausgeführte Datei umbenannt, läuft sie erneut.
+Run in **filename order**. A date prefix with a running number sorts
+reliably. The filename without `.php` is the migration's id; renaming an
+already-run file makes it run again.
 
-### Aufbau
+### Structure
 
-Empfohlene Form – die Datei gibt eine Funktion zurück:
+Recommended form — the file returns a function:
 
 ```php
 <?php
@@ -49,7 +48,7 @@ return function (array $context): void {
 };
 ```
 
-Alternativ definiert die Datei eine Funktion `up()`:
+Alternatively, the file defines a function `up()`:
 
 ```php
 <?php
@@ -60,29 +59,29 @@ function up(array $context): void
 }
 ```
 
-Die zurückgegebene Funktion ist die bessere Wahl: Zwei Migrationen, die beide `up()`
-definieren, würden sich im selben Prozess in die Quere kommen.
+The returned-function form is the better choice: two migrations that both
+define `up()` would collide within the same process.
 
-### Der Kontext
+### The context
 
 ```php
 [
-    "app_root"     => "/var/www/meinprojekt",
-    "instance"     => "meinprojekt-prod",
+    "app_root"     => "/var/www/myproject",
+    "instance"     => "myproject-prod",
     "from_version" => "v1.2.3",
     "to_version"   => "v1.3.0",
     "backup_dir"   => "/…/data/manage/updates/20260820-092114-v1.3.0",
     "run_id"       => "20260820-092114",
     "migration_id" => "2026-08-20-01-add-orders-index",
-    "pdo"          => PDO,   // nur wenn MANAGE_BACKUP_DATABASE konfiguriert ist
+    "pdo"          => PDO,   // only when MANAGE_BACKUP_DATABASE is configured
 ]
 ```
 
-`pdo` verwendet die Zugangsdaten, die ohnehin für den Datenbank-Dump konfiguriert
-sind. Eine zweite Konfiguration ist nicht nötig. Projekte ohne Datenbank arbeiten
-mit `app_root`.
+`pdo` uses the credentials already configured for the database dump. A
+second configuration isn't needed. Projects without a database work with
+`app_root`.
 
-### Beispiel: Datenbank
+### Example: database
 
 ```php
 <?php
@@ -90,7 +89,7 @@ mit `app_root`.
 return function (array $context): void {
     $pdo = $context["pdo"];
 
-    // Idempotent halten: die Migration kann nach einem Teilfehler erneut laufen.
+    // Keep it idempotent: the migration can run again after a partial failure.
     $exists = $pdo->query(
         "SELECT COUNT(*) FROM information_schema.statistics
          WHERE table_schema = DATABASE()
@@ -104,7 +103,7 @@ return function (array $context): void {
 };
 ```
 
-### Beispiel: JSON-Daten
+### Example: JSON data
 
 ```php
 <?php
@@ -126,9 +125,9 @@ return function (array $context): void {
 };
 ```
 
-### Zustand
+### State
 
-Ausgeführte Migrationen werden in `MANAGE_MIGRATIONS_STATE` festgehalten:
+Executed migrations are recorded in `MANAGE_MIGRATIONS_STATE`:
 
 ```json
 {
@@ -143,16 +142,17 @@ Ausgeführte Migrationen werden in `MANAGE_MIGRATIONS_STATE` festgehalten:
 }
 ```
 
-Diese Datei liegt im Datenverzeichnis und ist damit von Updates ausgenommen. Sie
-sollte im Backup enthalten sein, wenn `data/manage/` in den Quellen steht – ist es
-standardmäßig nicht, weil das Backup-Verzeichnis sich sonst selbst sichern würde.
-Wer den Migrationszustand mitsichern will, nimmt ihn einzeln auf:
+This file lives in the data directory and is therefore exempt from updates.
+It should be included in the backup if `data/manage/` is among the
+sources — by default it isn't, because the backup directory would
+otherwise back up itself. To include the migration state, add it
+individually:
 
 ```php
 ["as" => "manage", "file" => "data/manage/migrations.json"],
 ```
 
-## Projekt-Callback
+## Project callback
 
 ```php
 define("MANAGE_UPDATE_POST_HOOK", [
@@ -166,69 +166,71 @@ define("MANAGE_UPDATE_POST_HOOK", [
 
 function myProjectAfterUpdate(array $context): void
 {
-    // Cache leeren, abgeleitete Dateien neu bauen, Verzeichnis anlegen …
+    // Clear caches, rebuild derived files, create a directory …
     array_map("unlink", glob($context["app_root"] . "/data/cache/*.php") ?: []);
 }
 ```
 
-Als Fehlschlag gilt: eine geworfene Exception, `return false` oder
-`return ["success" => false, "error" => "…"]`. Alles andere gilt als Erfolg.
+Counted as failure: a thrown exception, `return false`, or
+`return ["success" => false, "error" => "…"]`. Everything else counts as
+success.
 
-Der Callback erhält denselben Kontext wie eine Migration, zusätzlich `migrations`
-mit der Liste der in diesem Lauf ausgeführten Kennungen.
+The callback receives the same context as a migration, plus `migrations`
+with the list of ids executed in this run.
 
-## Fehlerverhalten
+## Failure behavior
 
-Der wichtigste Punkt: **Zu diesem Zeitpunkt sind die Dateien bereits ausgerollt,
-und es gibt keine Rücknahme.** Ein Fehler wird deshalb laut gemeldet statt still
-verschluckt.
+The most important point: **at this point the files are already deployed,
+and there is no rollback.** A failure is therefore reported loudly instead
+of swallowed silently.
 
-Konkret:
+Specifically:
 
-- Der Lauf stoppt bei der ersten fehlgeschlagenen Migration. Die folgenden bleiben
-  offen und werden nicht versucht.
-- Der Callback wird bei einer fehlgeschlagenen Migration übersprungen.
-- `manageUpdateApply()` kehrt **normal zurück**, mit `deployed => true` und
+- The run stops at the first failed migration. The following ones stay
+  pending and are not attempted.
+- The callback is skipped if a migration failed.
+- `manageUpdateApply()` returns **normally**, with `deployed => true` and
   `hook["success"] => false`.
-- Die Kommandozeile beendet sich mit Exit-Code `1` und nennt die betroffene Datei –
-  meldet aber ausdrücklich, dass das Ausrollen erfolgreich war.
-- Die Oberfläche zeigt einen roten Hinweis mit dem Namen der Migration.
-- Beides steht im Client-Protokoll.
+- The command line exits with code `1` and names the affected file — but
+  explicitly reports that deployment itself succeeded.
+- The UI shows a red notice with the migration's name.
+- Both are recorded in the client log.
 
-Wiederherstellung nach einem Fehler:
+Recovery after a failure:
 
 ```bash
-# 1. Ursache beheben (Migration korrigieren, Rechte setzen, Datenbank prüfen)
-# 2. Offene Migrationen ansehen
+# 1. Fix the cause (correct the migration, set permissions, check the database)
+# 2. Look at pending migrations
 php manage-client/bin/manage-client.php migrate --dry-run
-# 3. Nachziehen
+# 3. Catch up
 php manage-client/bin/manage-client.php migrate
 ```
 
-Ein erneutes `update --force` rollt die Dateien nochmals aus, führt aber **keine
-bereits ausgeführten Migrationen erneut aus**.
+Running `update --force` again redeploys the files, but **does not re-run
+migrations that already ran**.
 
-## Idempotenz
+## Idempotency
 
-Migrationen sollen mehrfach ausführbar sein. Grund: Wenn eine Migration mittendrin
-scheitert – etwa nach der Hälfte einer Datenumstellung – wird sie nicht als
-ausgeführt vermerkt und läuft beim nächsten `migrate` erneut von vorn. Nur eine
-idempotente Migration übersteht das unbeschadet.
+Migrations should be safe to run more than once. Reason: if a migration
+fails partway through — say, after half of a data conversion — it is not
+recorded as applied and runs again from the start on the next `migrate`.
+Only an idempotent migration survives that unscathed.
 
-Praktisch heißt das: vor dem Ändern prüfen, ob die Änderung schon da ist, und
-Datenumstellungen so schreiben, dass bereits umgestellte Sätze übersprungen werden.
+In practice that means: check whether the change is already there before
+making it, and write data conversions so that already-converted rows are
+skipped.
 
-## Ohne Post-Update-Schritt ausrollen
+## Deploying without the post-update step
 
 ```bash
 php manage-client/bin/manage-client.php update --skip-hook
 ```
 
-Rollt nur die Dateien aus. Die Migrationen bleiben offen und können später mit
-`migrate` nachgezogen werden. Nützlich, wenn die Dateien dringend gebraucht werden,
-die Datenbankänderung aber in ein Wartungsfenster gehört.
+Deploys only the files. Migrations stay pending and can be caught up later
+with `migrate`. Useful when the files are urgently needed but the database
+change belongs in a maintenance window.
 
-## Weiter
+## Next
 
-- [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md) – Migrationen ins Paket bekommen
+- [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md) – getting migrations into the package
 - [04_FUNCTION_API](04_FUNCTION_API.md) – `manageUpdateRunMigrations()`

+ 64 - 60
client-package/docs/08_PROTOCOL.md

@@ -1,38 +1,42 @@
-# Protokoll v1
+# Protocol v1
 
-## Überblick
+## Overview
 
-Die Schnittstelle zwischen Client und Manage-Server. Wer den mitgelieferten Client
-verwendet, braucht dieses Dokument nicht – es ist für eigene Clients, für Debugging
-und für die Fehlersuche mit `curl` gedacht.
+The interface between client and Manage server. Anyone using the bundled
+client doesn't need this document — it's for custom clients, for debugging,
+and for troubleshooting with `curl`.
 
-Basis-URL: `<MANAGE_SERVER_URL>/api/v1/`
+Base URL: `<MANAGE_SERVER_URL>/api/v1/`
 
-## Authentifizierung
+Note: the server's actual `error` strings are still German text, shown
+verbatim in the examples below — this document's prose is in English, the
+API contract is not.
 
-Jede Anfrage trägt zwei Header:
+## Authentication
+
+Every request carries two headers:
 
 ```http
-X-Manage-Instance: meinprojekt-prod
+X-Manage-Instance: myproject-prod
 X-Manage-Token:    e4032c4dc51e9100…
 ```
 
-Der Server speichert nur den SHA-256-Hash des Tokens und vergleicht in konstanter
-Zeit. Es gibt keine Sitzung, kein Cookie und kein gemeinsames Passwort.
+The server stores only the SHA-256 hash of the token and compares it in
+constant time. There is no session, no cookie and no shared password.
 
-Fehlerantworten:
+Error responses:
 
-| Status | Bedeutung |
+| Status | Meaning |
 |---|---|
-| `401` | Header fehlen, Instanz unbekannt oder Token falsch – bewusst nicht unterscheidbar |
-| `403` | Instanz existiert, ist aber deaktiviert |
-| `405` | Falsche HTTP-Methode |
-| `429` | Zu viele fehlgeschlagene Authentifizierungen von dieser IP |
+| `401` | headers missing, instance unknown, or token wrong — deliberately indistinguishable |
+| `403` | instance exists but is deactivated |
+| `405` | wrong HTTP method |
+| `429` | too many failed authentications from this IP |
 
-Fehlgeschlagene Anmeldungen sind pro IP begrenzt, damit Instanz-Kennungen nicht
-durchprobiert werden können. Eine erfolgreiche Anmeldung setzt den Zähler zurück.
+Failed logins are rate-limited per IP, so instance ids can't be brute-forced.
+A successful login resets the counter.
 
-Alle Fehlerantworten haben denselben Aufbau:
+Every error response has the same shape:
 
 ```json
 {
@@ -43,11 +47,11 @@ Alle Fehlerantworten haben denselben Aufbau:
 
 ## GET manifest.php
 
-Liefert das Release, das die Instanz installieren soll.
+Returns the release the instance should install.
 
 ```bash
 curl -s https://manage.example.org/api/v1/manifest.php \
-  -H "X-Manage-Instance: meinprojekt-prod" \
+  -H "X-Manage-Instance: myproject-prod" \
   -H "X-Manage-Token: $TOKEN"
 ```
 
@@ -63,46 +67,46 @@ curl -s https://manage.example.org/api/v1/manifest.php \
 }
 ```
 
-`404`, wenn kein gültiges Release veröffentlicht ist.
+`404` if no valid release is published.
 
-`package_url` wird aus der Serverkonfiguration (`MANAGE_PUBLIC_URL`) gebildet, nicht
-aus dem `Host`-Header der Anfrage. Ein gefälschter Header kann einen Client daher
-nicht auf einen fremden Server umlenken.
+`package_url` is built from the server configuration (`MANAGE_PUBLIC_URL`),
+not from the request's `Host` header. A spoofed header therefore can't
+redirect a client to a foreign server.
 
 ## GET package.php
 
-Liefert das Release-ZIP.
+Returns the release ZIP.
 
 ```bash
 curl -s -o release.zip \
   "https://manage.example.org/api/v1/package.php?version=v1.3.0" \
-  -H "X-Manage-Instance: meinprojekt-prod" \
+  -H "X-Manage-Instance: myproject-prod" \
   -H "X-Manage-Token: $TOKEN"
 ```
 
-Antwort: `application/zip` mit `Content-Length` und
-`Cache-Control: private, no-store`. Bei Erfolg kein JSON.
+Response: `application/zip` with `Content-Length` and
+`Cache-Control: private, no-store`. No JSON on success.
 
-`400` bei ungültigem Versionsformat, `404`, wenn das Release nicht existiert.
+`400` on an invalid version format, `404` if the release doesn't exist.
 
-Der Client vergleicht Größe und SHA-256 mit dem Manifest und löscht die Datei bei
-Abweichung. Ein eigener Client muss das ebenso tun – ohne diese Prüfung wird
-beliebiger Code ausgerollt.
+The client compares size and SHA-256 against the manifest and deletes the
+file on mismatch. A custom client must do the same — without this check,
+arbitrary code gets deployed.
 
 ## POST backup.php
 
-Nimmt ein Backup-Archiv entgegen. `multipart/form-data`:
+Accepts a backup archive. `multipart/form-data`:
 
-| Feld | Pflicht | Bedeutung |
+| Field | Required | Meaning |
 |---|---|---|
-| `backup` | ja | die ZIP-Datei |
-| `filename` | nein | `backup-YYYYmmdd-HHMMSS[-N].zip`; ohne Angabe vergibt der Server einen Namen |
-| `sha256` | nein | Prüfsumme; wird serverseitig neu berechnet und verglichen |
-| `meta` | nein | JSON mit `trigger`, `file_count`, `source_bytes`, `app_version` |
+| `backup` | yes | the ZIP file |
+| `filename` | no | `backup-YYYYmmdd-HHMMSS[-N].zip`; the server assigns a name if omitted |
+| `sha256` | no | checksum; recomputed and compared server-side |
+| `meta` | no | JSON with `trigger`, `file_count`, `source_bytes`, `app_version` |
 
 ```bash
 curl -s https://manage.example.org/api/v1/backup.php \
-  -H "X-Manage-Instance: meinprojekt-prod" \
+  -H "X-Manage-Instance: myproject-prod" \
   -H "X-Manage-Token: $TOKEN" \
   -F "filename=backup-20260820-092104.zip" \
   -F "sha256=824f3f80…" \
@@ -113,7 +117,7 @@ curl -s https://manage.example.org/api/v1/backup.php \
 ```json
 {
     "success": true,
-    "instance": "meinprojekt-prod",
+    "instance": "myproject-prod",
     "filename": "backup-20260820-092104.zip",
     "size": 427,
     "sha256": "824f3f80…",
@@ -122,18 +126,18 @@ curl -s https://manage.example.org/api/v1/backup.php \
 }
 ```
 
-Der Server prüft in dieser Reihenfolge: Upload-Fehlercode, `is_uploaded_file`,
-Größenlimit, ZIP-Signatur, Dateinamensmuster, Prüfsumme nach dem Speichern. Weicht
-die Prüfsumme ab, wird die Datei wieder gelöscht und `400` gemeldet.
+The server checks in this order: upload error code, `is_uploaded_file`,
+size limit, ZIP signature, filename pattern, checksum after storing. If the
+checksum doesn't match, the file is deleted again and `400` is reported.
 
-Ein vorhandener Dateiname wird nie überschrieben: Der Server hängt `-2`, `-3` an.
+An existing filename is never overwritten: the server appends `-2`, `-3`.
 
-Fehler beim S3-Archivieren lassen den Upload **nicht** fehlschlagen – die lokale
-Kopie ist gespeichert und wird später nachgezogen.
+Errors during S3 archiving **do not** fail the upload — the local copy is
+stored and gets caught up later.
 
 ## POST heartbeat.php
 
-Statusmeldung. `application/json`:
+Status report. `application/json`:
 
 ```json
 {
@@ -148,24 +152,24 @@ Statusmeldung. `application/json`:
 ```json
 {
     "success": true,
-    "instance": "meinprojekt-prod",
+    "instance": "myproject-prod",
     "latest": "v1.3.0",
     "update_available": false,
     "server_time": "2026-08-20T09:23:11+00:00"
 }
 ```
 
-Alle Felder der Anfrage sind optional; fehlende Felder lassen den bisherigen Wert
-auf dem Server unverändert. Die Antwort ersetzt für einfache Überwachung einen
-eigenen Aufruf von `manifest.php`.
+All request fields are optional; missing fields leave the server's current
+value unchanged. The response replaces a separate call to `manifest.php`
+for simple monitoring.
 
-## Nebenwirkung jeder Anfrage
+## Side effect of every request
 
-Jede erfolgreich authentifizierte Anfrage aktualisiert `last_seen_at` und die
-letzte IP der Instanz. Die Übersicht im Manage-Server bleibt dadurch aktuell, auch
-wenn nur Backups laufen und nie ein Heartbeat gesendet wird.
+Every successfully authenticated request updates `last_seen_at` and the
+instance's latest IP. The overview in the Manage server stays current this
+way, even when only backups run and a heartbeat is never sent.
 
-## Weiter
+## Next
 
-- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – was einzelne Fehlermeldungen bedeuten
-- [10_SECURITY](10_SECURITY.md) – Umgang mit dem Token
+- [09_TROUBLESHOOTING](09_TROUBLESHOOTING.md) – what individual error messages mean
+- [10_SECURITY](10_SECURITY.md) – handling the token

+ 202 - 137
client-package/docs/09_TROUBLESHOOTING.md

@@ -1,116 +1,148 @@
-# Fehlersuche
+# Troubleshooting
 
-## Überblick
+## Overview
 
-Jede Fehlermeldung, die der Client erzeugen kann, mit Ursache und Behebung.
-Die Meldungen stammen aus `manage-client/lib/`.
+Every error message the client can produce, with cause and fix. The
+messages come from `manage-client/lib/`.
 
-Erste Anlaufstelle ist immer:
+Note: the client's actual messages are still German text (this is a German
+product), quoted verbatim in bold below. Each is followed by an *italic
+English gloss* and then an English explanation, so the heading itself stays
+searchable against what actually appears in your terminal or log.
+
+First stop is always:
 
 ```bash
 php manage-client/bin/manage-client.php status
 ```
 
-und danach das Protokoll in `MANAGE_LOG_FILE` (Standard
-`data/manage/manage-client.log`, eine JSON-Zeile pro Ereignis):
+and after that the log in `MANAGE_LOG_FILE` (default
+`data/manage/manage-client.log`, one JSON line per event):
 
 ```bash
 tail -20 data/manage/manage-client.log | php -r 'while($l=fgets(STDIN)) { $e=json_decode($l,true); echo $e["timestamp"]," ",$e["level"]," ",$e["message"],"\n"; }'
 ```
 
-## Konfiguration und Verbindung
+## Configuration and connection
 
 **`Manage-Client ist nicht konfiguriert. MANAGE_SERVER_URL, MANAGE_INSTANCE und MANAGE_TOKEN müssen in manage-client/config.php gesetzt sein.`**
-`config.php` fehlt oder einer der drei Werte ist leer. `config.sample.php` kopieren
-und die Werte aus dem Manage-Server eintragen. Prüfen, ob die Datei wirklich
-`manage-client/config.php` heißt.
-
-**`Manage-Server ist nicht erreichbar: <URL>`**
-Keine Antwort. Mögliche Ursachen: falsche `MANAGE_SERVER_URL`, DNS, Firewall,
-ausgehende Verbindungen auf dem Hoster gesperrt, TLS-Zertifikat nicht vertrauenswürdig.
-Prüfen mit `curl -v <URL>/api/v1/manifest.php` vom selben Server aus.
-
-**`Ungültige Server-URL: <URL>`**
-`MANAGE_SERVER_URL` ist keine gültige URL. Sie muss mit `https://` beginnen und darf
-weder `/api` noch einen Schrägstrich am Ende enthalten.
-
-**`Authentifizierung fehlgeschlagen. (HTTP 401)`**
-Instanz-Kennung oder Token stimmen nicht. Beides ist absichtlich nicht
-unterscheidbar. Im Manage-Server unter **Instanzen** ein neues Token erzeugen und
-eintragen; das alte wird dabei sofort ungültig.
-
-**`Diese Instanz ist deaktiviert. (HTTP 403)`**
-Die Instanz existiert, ist aber im Manage-Server deaktiviert. Dort wieder aktivieren.
-
-**`Zu viele Anfragen. Bitte später erneut versuchen. (HTTP 429)`**
-Zu viele fehlgeschlagene Authentifizierungen von dieser IP. Nach Ablauf des
-Zeitfensters (Standard 5 Minuten) mit korrektem Token erneut versuchen.
-
-**`Antwort des Servers ist kein gültiges JSON.`**
-Die Antwort kam nicht vom Manage-Server: meist eine Fehlerseite des Webservers,
-eine Umleitung oder ein Captive Portal. Antwort direkt mit `curl` ansehen.
+*("The Manage client is not configured. MANAGE_SERVER_URL, MANAGE_INSTANCE
+and MANAGE_TOKEN must be set in manage-client/config.php.")*
+`config.php` is missing, or one of the three values is empty. Copy
+`config.sample.php` and enter the values from the Manage server. Check that
+the file is really named `manage-client/config.php`.
+
+**`Manage-Server ist nicht erreichbar: <URL>`** *("Manage server is
+unreachable: <URL>")*
+No response. Possible causes: wrong `MANAGE_SERVER_URL`, DNS, a firewall,
+outbound connections blocked by the host, an untrusted TLS certificate.
+Check with `curl -v <URL>/api/v1/manifest.php` from the same server.
+
+**`Ungültige Server-URL: <URL>`** *("Invalid server URL: <URL>")*
+`MANAGE_SERVER_URL` isn't a valid URL. It must start with `https://` and
+must contain neither `/api` nor a trailing slash.
+
+**`Authentifizierung fehlgeschlagen. (HTTP 401)`** *("Authentication
+failed.")*
+Instance id or token don't match. Both are deliberately indistinguishable.
+In the Manage server, under **Instances**, generate a new token and enter
+it; the old one becomes invalid immediately.
+
+**`Diese Instanz ist deaktiviert. (HTTP 403)`** *("This instance is
+deactivated.")*
+The instance exists but is deactivated in the Manage server. Reactivate it
+there.
+
+**`Zu viele Anfragen. Bitte später erneut versuchen. (HTTP 429)`** *("Too
+many requests. Please try again later.")*
+Too many failed authentications from this IP. Retry with the correct token
+after the time window expires (default 5 minutes).
+
+**`Antwort des Servers ist kein gültiges JSON.`** *("The server's response
+is not valid JSON.")*
+The response didn't come from the Manage server: usually a web server error
+page, a redirect, or a captive portal. Inspect the response directly with
+`curl`.
 
 ## Update
 
 **`Es ist kein neueres Update verfügbar. Mit der Option "force" kann dasselbe Paket erneut ausgerollt werden.`**
-Kein Fehler. Auf der Kommandozeile `update --force`, in der Oberfläche das Häkchen
-"erneut ausrollen".
+*("No newer update is available. The "force" option can redeploy the same
+package.")*
+Not an error. On the command line, `update --force`; in the UI, the
+"redeploy" checkbox.
 
 **`Version im Manifest ist ungültig.` / `Prüfsumme im Manifest ist ungültig.` / `Paket-URL im Manifest ist ungültig.`**
-Der Server liefert ein unbrauchbares Manifest. Auf dem Server prüfen, ob ein Release
-veröffentlicht und als aktuell gesetzt ist. Bei "Paket-URL ungültig" ist meist
-`MANAGE_PUBLIC_URL` in der Serverkonfiguration nicht oder falsch gesetzt.
+*("Version in the manifest is invalid." / "Checksum in the manifest is
+invalid." / "Package URL in the manifest is invalid.")*
+The server returns an unusable manifest. On the server, check whether a
+release is published and set as current. For "package URL invalid",
+`MANAGE_PUBLIC_URL` in the server configuration is usually unset or wrong.
 
 **`Größe des heruntergeladenen Pakets stimmt nicht überein.` / `Prüfsumme des Pakets stimmt nicht überein.`**
-Das Paket entspricht nicht dem Manifest. Die heruntergeladene Datei wird sofort
-gelöscht und **nichts** ausgerollt. Ursachen: abgebrochener Download, ein Proxy der
-den Inhalt verändert, oder ein auf dem Server ausgetauschtes Paket. Release neu
-hochladen und erneut versuchen. Wiederholt sich der Fehler, ist die Übertragungskette
-zu prüfen, bevor ausgerollt wird.
-
-**`Das heruntergeladene Paket ist keine lesbare ZIP-Datei.`**
-Die Datei ist beschädigt oder es wurde etwas anderes als ein ZIP hochgeladen.
-
-**`Das Paket enthält einen unsicheren Pfad: <Pfad>`**
-Ein Eintrag versucht aus dem Zielverzeichnis auszubrechen (`..`, absoluter Pfad,
-Laufwerksbuchstabe, Nullbyte). Es wird nichts entpackt. Ein solches Paket darf nicht
-ausgerollt werden – Herkunft klären.
+*("Size of the downloaded package doesn't match." / "Checksum of the
+package doesn't match.")*
+The package doesn't match the manifest. The downloaded file is deleted
+immediately and **nothing** is deployed. Causes: an interrupted download, a
+proxy that alters the content, or a package swapped out on the server.
+Re-upload the release and try again. If it keeps happening, check the
+transfer chain before deploying.
+
+**`Das heruntergeladene Paket ist keine lesbare ZIP-Datei.`** *("The
+downloaded package is not a readable ZIP file.")*
+The file is corrupted, or something other than a ZIP was uploaded.
+
+**`Das Paket enthält einen unsicheren Pfad: <Pfad>`** *("The package
+contains an unsafe path: <path>")*
+An entry tries to break out of the target directory (`..`, an absolute
+path, a drive letter, a null byte). Nothing gets extracted. Such a package
+must not be deployed — find out where it came from.
 
 **`Das Paket sieht nicht wie ein Release dieser Anwendung aus (erwartet: index.php)`**
-Keiner der Pfade aus `MANAGE_UPDATE_SANITY_PATHS` ist im Paket. Fast immer wurde das
-ZIP mit einem Oberverzeichnis gebaut. Siehe
+*("The package doesn't look like a release of this application (expected:
+index.php)")*
+None of the paths from `MANAGE_UPDATE_SANITY_PATHS` is in the package.
+Almost always the ZIP was built with a top-level directory. See
 [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md).
 
-**`Die PHP-Erweiterung ZipArchive ist nicht verfügbar.`**
-`ext-zip` fehlt. Updates brauchen sie; Backups funktionieren auch ohne, weil der
-Client dort einen eigenen ZIP-Writer verwendet. Beim Hoster aktivieren lassen.
+**`Die PHP-Erweiterung ZipArchive ist nicht verfügbar.`** *("The PHP
+ZipArchive extension is not available.")*
+`ext-zip` is missing. Updates need it; backups still work without it,
+because the client uses its own ZIP writer there. Have the host enable it.
 
 **`Datei konnte nicht ausgerollt werden: <Pfad>` / `Datei konnte nicht gesichert werden: <Pfad>`**
-Fehlende Schreibrechte im Anwendungsstamm. **Wichtig:** Dieser Fehler tritt
-mittendrin auf, das Ausrollen ist dann unvollständig. Rechte korrigieren und
-`update --force` erneut ausführen – der Lauf beginnt von vorn und stellt den
-vollständigen Zustand her.
+*("File could not be deployed: <path>" / "File could not be backed up:
+<path>")*
+Missing write permission in the application root. **Important:** this
+error occurs mid-run, so the deployment is then incomplete. Fix permissions
+and run `update --force` again — the run starts over and restores the
+complete state.
 
-**`Verzeichnis konnte nicht erstellt werden: <Pfad>`**
-Fehlende Schreibrechte auf dem übergeordneten Verzeichnis.
+**`Verzeichnis konnte nicht erstellt werden: <Pfad>`** *("Directory could
+not be created: <path>")*
+Missing write permission on the parent directory.
 
-**`Altes Backup-Verzeichnis konnte nicht entfernt werden: <Pfad>`**
-Das Ausrollen war erfolgreich, nur das Aufräumen alter Sicherungen scheiterte.
-Verzeichnis von Hand entfernen.
+**`Altes Backup-Verzeichnis konnte nicht entfernt werden: <Pfad>`** *("Old
+backup directory could not be removed: <path>")*
+Deployment succeeded; only cleaning up old backups failed. Remove the
+directory by hand.
 
-**`MANAGE_APP_ROOT existiert nicht: <Pfad>`**
-Der konfigurierte Anwendungsstamm ist falsch. Standard ist das Elternverzeichnis von
-`manage-client/`.
+**`MANAGE_APP_ROOT existiert nicht: <Pfad>`** *("MANAGE_APP_ROOT does not
+exist: <path>")*
+The configured application root is wrong. The default is the parent
+directory of `manage-client/`.
 
-## Migrationen und Hook
+## Migrations and hook
 
 **`Migration <id> liefert keine Funktion zurück und definiert kein up().`**
-Die Migrationsdatei muss entweder eine Funktion zurückgeben (`return function (array $context) {...};`)
-oder eine Funktion `up(array $context)` definieren.
+*("Migration <id> does not return a function and does not define up().")*
+The migration file must either return a function
+(`return function (array $context) {...};`) or define a function
+`up(array $context)`.
 
-**Migration schlägt mit einem eigenen Fehler fehl**
-Der Lauf stoppt, die folgenden Migrationen bleiben offen, die Dateien sind aber
-bereits ausgerollt. Ursache beheben, dann:
+**A migration fails with its own error**
+The run stops, the following migrations stay pending, but the files are
+already deployed. Fix the cause, then:
 
 ```bash
 php manage-client/bin/manage-client.php migrate --dry-run
@@ -118,108 +150,141 @@ php manage-client/bin/manage-client.php migrate
 ```
 
 **`Hook-Datei wurde nicht gefunden: <Pfad>` / `Hook-Callback ist nicht aufrufbar: <Name>`**
-`MANAGE_UPDATE_POST_HOOK` verweist auf eine fehlende Datei oder eine Funktion, die
-dort nicht definiert wird. Häufig, wenn die Hook-Datei nicht im Release-Paket liegt.
+*("Hook file was not found: <path>" / "Hook callback is not callable:
+<name>")*
+`MANAGE_UPDATE_POST_HOOK` points to a missing file, or a function that
+isn't defined there. Common when the hook file isn't in the release
+package.
 
-**`Post-Update-Hook meldet einen Fehler`**
-Der Callback hat `false` oder `["success" => false]` zurückgegeben. Die Dateien sind
-ausgerollt; die Details stehen im Protokoll.
+**`Post-Update-Hook meldet einen Fehler`** *("Post-update hook reports an
+error")*
+The callback returned `false` or `["success" => false]`. The files are
+deployed; the details are in the log.
 
 ## Backup
 
-**`Es läuft bereits ein Backup.`**
-Die Sperrdatei ist belegt: ein zweiter Lauf startete, während der erste noch lief.
-Meist überlappen Cron-Job und manueller Aufruf. Warten und erneut versuchen. Bleibt
-es dauerhaft, wurde ein früherer Lauf hart abgebrochen – die Sperre löst sich mit
-dem Prozessende von selbst; hilft das nicht, `data/manage/backups/.backup.lock`
-entfernen, wenn sicher kein Backup läuft.
+**`Es läuft bereits ein Backup.`** *("A backup is already running.")*
+The lock file is held: a second run started while the first was still
+going. Usually a cron job and a manual run overlap. Wait and retry. If it
+persists, an earlier run was killed hard — the lock releases itself when
+that process ends; if that doesn't help, remove
+`data/manage/backups/.backup.lock` once you're certain no backup is
+running.
 
 **`Keine Dateien für das Backup gefunden.` / `Keine lesbaren Dateien für das Backup gefunden.`**
-`MANAGE_BACKUP_SOURCES` trifft auf keine existierende Datei. Pfade sind relativ zu
-`MANAGE_APP_ROOT`. Prüfen mit:
+*("No files found for the backup." / "No readable files found for the
+backup.")*
+`MANAGE_BACKUP_SOURCES` matches no existing file. Paths are relative to
+`MANAGE_APP_ROOT`. Check with:
 
 ```bash
 php -r 'require "manage-client/lib/client.php"; print_r(manageBackupCollectSources());'
 ```
 
 **`Backup-ZIP konnte nicht erstellt werden.` / `Backup-ZIP konnte nicht finalisiert werden.`**
-Keine Schreibrechte auf `MANAGE_BACKUP_DIR` oder die Festplatte ist voll.
+*("Backup ZIP could not be created." / "Backup ZIP could not be
+finalized.")*
+No write access to `MANAGE_BACKUP_DIR`, or the disk is full.
 
 **`Datei ist zu groß für dieses Backup-Format: <Name>` / `Backup-ZIP ist zu groß für dieses Backup-Format.` / `Zu viele Dateien für dieses Backup-Format.`**
-Grenzen des ZIP-Formats ohne Zip64: 4 GB pro Datei, 4 GB pro Archiv, 65535 Einträge.
-Quellen aufteilen oder große Mediendateien getrennt sichern.
+*("File is too large for this backup format: <name>" / "Backup ZIP is too
+large for this backup format." / "Too many files for this backup
+format.")*
+Limits of the ZIP format without Zip64: 4 GB per file, 4 GB per archive,
+65535 entries. Split up the sources, or back up large media files
+separately.
 
 **`Ungültiger Pfad im Backup: <Name>` / `Pfad im Backup ist zu lang: <Name>`**
-Ein Dateiname enthält ungültige Zeichen oder das `as`-Präfix erzeugt einen ungültigen
-Archivpfad. Präfixe ohne führenden Schrägstrich und ohne `..` verwenden.
+*("Invalid path in the backup: <name>" / "Path in the backup is too long:
+<name>")*
+A filename contains invalid characters, or the `as` prefix produces an
+invalid archive path. Use prefixes without a leading slash and without
+`..`.
 
 **`Der Manage-Server hat das Backup abgelehnt: Ungültiger Backup-Dateiname.`**
-Der Dateiname entspricht nicht `backup-YYYYmmdd-HHMMSS[-N].zip`. Tritt nur bei
-selbst gebauten Uploads auf.
+*("The Manage server rejected the backup: invalid backup filename.")*
+The filename doesn't match `backup-YYYYmmdd-HHMMSS[-N].zip`. Only happens
+with self-built uploads.
 
 **`Der Manage-Server hat das Backup abgelehnt: Prüfsumme des Backups stimmt nicht überein.`**
-Das Archiv wurde unterwegs verändert oder unvollständig übertragen. Die Datei wird
-serverseitig gelöscht. Das lokale Archiv ist in Ordnung; erneut versuchen.
+*("The Manage server rejected the backup: backup checksum doesn't
+match.")*
+The archive was altered in transit, or transferred incompletely. The file
+is deleted server-side. The local archive is fine; try again.
 
 **`Backup überschreitet das Upload-Limit des Servers (upload_max_filesize / post_max_size).`**
-Die PHP-Grenzen auf dem **Manage-Server** sind kleiner als das Archiv. Dort
-`upload_max_filesize` und `post_max_size` erhöhen (beide!) oder den Backup-Umfang
-reduzieren. Die aktuellen Werte stehen im Manage-Server unter **Einstellungen → Diagnose**.
+*("Backup exceeds the server's upload limit (upload_max_filesize /
+post_max_size).")*
+The PHP limits on the **Manage server** are smaller than the archive.
+Increase `upload_max_filesize` and `post_max_size` there (both!), or reduce
+the backup's scope. The current values are shown in the Manage server
+under **Settings → Diagnostics**.
 
-Der Upload-Fehler macht das lokale Archiv nicht ungültig – es liegt vollständig in
-`data/manage/backups/`.
+The upload failure does not invalidate the local archive — it sits complete
+in `data/manage/backups/`.
 
-## Datenbank
+## Database
 
-**`Die PHP-PDO-Erweiterung ist nicht verfügbar.`**
-`pdo_mysql` fehlt. Ohne sie kann kein Dump erstellt werden;
-`MANAGE_BACKUP_DATABASE` auf `null` setzen oder die Erweiterung aktivieren lassen.
+**`Die PHP-PDO-Erweiterung ist nicht verfügbar.`** *("The PHP PDO extension
+is not available.")*
+`pdo_mysql` is missing. No dump can be created without it; set
+`MANAGE_BACKUP_DATABASE` to `null` or have the extension enabled.
 
-**`Datenbankverbindung fehlgeschlagen: <Meldung>`**
-DSN, Benutzer oder Passwort stimmen nicht, oder der Server ist nicht erreichbar. Die
-Originalmeldung von PDO steht dahinter.
+**`Datenbankverbindung fehlgeschlagen: <Meldung>`** *("Database connection
+failed: <message>")*
+DSN, user or password are wrong, or the server is unreachable. PDO's
+original message follows.
 
-**`MANAGE_BACKUP_DATABASE benötigt einen DSN.`**
-Das Array ist gesetzt, aber `dsn` fehlt oder ist leer.
+**`MANAGE_BACKUP_DATABASE benötigt einen DSN.`** *("MANAGE_BACKUP_DATABASE
+requires a DSN.")*
+The array is set, but `dsn` is missing or empty.
 
-**`Datenbank-Dump fehlgeschlagen: <Meldung>`**
-Meist fehlende Rechte: Der Benutzer braucht `SELECT` und `SHOW VIEW` auf allen
-Tabellen. Der unvollständige Dump wird gelöscht, das Backup bricht ab.
+**`Datenbank-Dump fehlgeschlagen: <Meldung>`** *("Database dump failed:
+<message>")*
+Usually missing permissions: the user needs `SELECT` and `SHOW VIEW` on all
+tables. The incomplete dump is deleted; the backup aborts.
 
-## Zusätzliche Ziele
+## Extra targets
 
-**`Die PHP-SSH2-Erweiterung ist nicht verfügbar.`**
-SFTP braucht `ext-ssh2`. Ohne sie schlägt nur dieses Ziel fehl; das lokale Archiv
-und alle anderen Ziele bleiben davon unberührt.
+**`Die PHP-SSH2-Erweiterung ist nicht verfügbar.`** *("The PHP SSH2
+extension is not available.")*
+SFTP needs `ext-ssh2`. Without it, only this target fails; the local
+archive and every other target are unaffected.
 
 **`SFTP-Zieldatei konnte nicht geöffnet werden. Existiert das Verzeichnis?`**
-Das entfernte Verzeichnis muss vorhanden und beschreibbar sein; es wird nicht angelegt.
+*("SFTP target file could not be opened. Does the directory exist?")*
+The remote directory must exist and be writable; it isn't created.
 
-**`S3-Ziel ist unvollständig konfiguriert.`**
-`bucket`, `region`, `access_key` und `secret_key` sind alle Pflicht.
+**`S3-Ziel ist unvollständig konfiguriert.`** *("S3 target is incompletely
+configured.")*
+`bucket`, `region`, `access_key` and `secret_key` are all required.
 
-**`S3-Upload fehlgeschlagen (HTTP 403)`**
-Bei `SignatureDoesNotMatch` stimmen Region oder Secret Key nicht. Bei `AccessDenied`
-passt meist die Adressierungsart nicht – für S3-kompatible Anbieter `endpoint` setzen.
+**`S3-Upload fehlgeschlagen (HTTP 403)`** *("S3 upload failed (HTTP
+403)")*
+With `SignatureDoesNotMatch`, region or secret key are wrong. With
+`AccessDenied`, the addressing style is usually wrong — set `endpoint` for
+S3-compatible providers.
 
-**`Unbekannter Backup-Zieltyp: <typ>`**
-`type` muss `s3`, `sftp` oder `custom` sein. Der frühere Typ `managed` entfällt: Der
-Upload zum Manage-Server ist eingebaut und wird über `MANAGE_BACKUP_UPLOAD` gesteuert.
+**`Unbekannter Backup-Zieltyp: <typ>`** *("Unknown backup target type:
+<type>")*
+`type` must be `s3`, `sftp` or `custom`. The earlier type `managed` is
+gone: the upload to the Manage server is built in and controlled via
+`MANAGE_BACKUP_UPLOAD`.
 
-## Rechte auf einen Blick
+## Permissions at a glance
 
 ```bash
-# Schreibrechte für PHP prüfen
+# Check PHP write access
 php -r '
 foreach (["data/manage/backups", "data/manage/work", "data/manage/updates", "."] as $d) {
-    printf("%-26s %s\n", $d, is_writable($d) ? "beschreibbar" : "NICHT beschreibbar");
+    printf("%-26s %s\n", $d, is_writable($d) ? "writable" : "NOT writable");
 }'
 ```
 
-Der letzte Eintrag `.` ist der Anwendungsstamm – ohne Schreibrecht dort sind keine
-Updates möglich.
+The last entry, `.`, is the application root — without write access there,
+no updates are possible.
 
-## Weiter
+## Next
 
-- [03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md) – alle Konstanten
-- [08_PROTOCOL](08_PROTOCOL.md) – Anfragen mit `curl` nachstellen
+- [03_CONFIG_REFERENCE](03_CONFIG_REFERENCE.md) – every constant
+- [08_PROTOCOL](08_PROTOCOL.md) – reproducing requests with `curl`

+ 99 - 94
client-package/docs/10_SECURITY.md

@@ -1,139 +1,144 @@
-# Sicherheit
+# Security
 
-## Überblick
+## Overview
 
-Was der Client tut, wem er vertraut und was in der Verantwortung des Projekts bleibt.
+What the client does, who it trusts, and what stays the project's
+responsibility.
 
-## Das Token
+## The token
 
-Das Instanz-Token ist ein 64-stelliger Hexadezimalwert aus 32 zufälligen Bytes. Es
-ist das einzige Geheimnis zwischen Instanz und Server.
+The instance token is a 64-character hexadecimal value from 32 random
+bytes. It's the only secret between instance and server.
 
-- Es wird **einmalig** beim Anlegen der Instanz angezeigt. Der Server speichert nur
-  den SHA-256-Hash und kann es nicht wieder ausgeben.
-- Es steht ausschließlich in `manage-client/config.php`.
-- Diese Datei gehört **nicht ins Repository** und **nicht ins Release-Paket**.
-- Bei Verdacht auf Kompromittierung im Manage-Server unter **Instanzen** →
-  "Token erneuern". Das alte Token ist damit sofort ungültig.
+- It's shown **once** when the instance is created. The server stores only
+  the SHA-256 hash and cannot display it again.
+- It lives exclusively in `manage-client/config.php`.
+- This file does **not** belong in the repository and does **not** belong
+  in the release package.
+- If compromise is suspected, in the Manage server go to **Instances** →
+  "Rotate token". The old token becomes invalid immediately.
 
 ```gitignore
 manage-client/config.php
 data/manage/
 ```
 
-Ein kompromittiertes Token erlaubt: Releases herunterzuladen und Backups
-hochzuladen – also Zugriff auf den Anwendungscode und das Belegen von
-Speicherplatz. Es erlaubt **nicht**, Backups herunterzuladen oder Releases zu
-verändern; beides geht nur über die Anmeldung am Manage-Server.
+A compromised token allows: downloading releases and uploading backups —
+so, access to the application code and the ability to use up storage. It
+does **not** allow downloading backups or changing releases; both require
+logging into the Manage server.
 
 ## Transport
 
-Alle Anfragen laufen über HTTPS. Der Client verwendet die PHP-Standardeinstellungen
-für die Zertifikatsprüfung; diese wird **nicht** abgeschaltet. Ein Server mit
-selbstsigniertem Zertifikat funktioniert deshalb nicht ohne passendes CA-Bundle im
-System – das ist Absicht.
+Every request runs over HTTPS. The client uses PHP's default certificate
+verification; it is **not** disabled. A server with a self-signed
+certificate therefore doesn't work without a matching CA bundle on the
+system — that's intentional.
 
-Umleitungen werden nicht verfolgt (`follow_location => 0`). Eine umgeleitete
-Anfrage schlägt fehl, statt Zugangsdaten an ein anderes Ziel zu senden.
+Redirects are not followed (`follow_location => 0`). A redirected request
+fails instead of sending credentials to a different destination.
 
-## Vertrauen ins Release-Paket
+## Trust in the release package
 
-Ein Update rollt fremden Code auf dem Server aus. Abgesichert ist das durch:
+An update deploys someone else's code on the server. That's secured by:
 
-1. **TLS** zum Manage-Server.
-2. **Token-Pflicht** für Manifest und Paket – beides ist nicht öffentlich abrufbar.
-3. **SHA-256-Prüfung** von Größe und Inhalt gegen das Manifest. Bei Abweichung wird
-   die Datei gelöscht und nichts ausgerollt.
-4. **Pfadprüfung** jedes ZIP-Eintrags gegen Ausbruch aus dem Zielverzeichnis.
-5. **Plausibilitätsprüfung** über `MANAGE_UPDATE_SANITY_PATHS`.
+1. **TLS** to the Manage server.
+2. **Token required** for manifest and package — neither is publicly
+   retrievable.
+3. **SHA-256 check** of size and content against the manifest. On mismatch,
+   the file is deleted and nothing is deployed.
+4. **Path check** on every ZIP entry against escaping the target directory.
+5. **Sanity check** via `MANAGE_UPDATE_SANITY_PATHS`.
 
-Die Grenze dieses Modells: Die Prüfsumme kommt vom selben Server wie das Paket. Wer
-den Manage-Server übernimmt, kann ein Paket **und** die passende Prüfsumme
-veröffentlichen. Eine Signatur mit einem im Client hinterlegten öffentlichen
-Schlüssel gibt es bewusst nicht – der Manage-Server muss entsprechend abgesichert
-werden.
+The limit of this model: the checksum comes from the same server as the
+package. Whoever takes over the Manage server can publish a package **and**
+the matching checksum. A signature backed by a public key held in the
+client deliberately doesn't exist — the Manage server has to be secured
+accordingly.
 
-## Backups enthalten Betriebsdaten
+## Backups contain operational data
 
-Ein Backup wird an den Manage-Server übertragen, dort gespeichert und kann von jedem
-heruntergeladen werden, der sich am Manage-Server anmeldet. Daraus folgt:
+A backup gets transferred to the Manage server, stored there, and can be
+downloaded by anyone who logs into the Manage server. It follows that:
 
-- **Keine Zugangsdaten ins Backup.** `config.php` mit Datenbankpasswörtern oder
-  API-Schlüsseln gehört nicht in `MANAGE_BACKUP_SOURCES`.
-- Enthält das Backup personenbezogene Daten – bei Bestell- oder Kundendaten die
-  Regel – gelten für den Manage-Server dieselben Anforderungen wie für die
-  Anwendung selbst: Zugriffsschutz, Verschlüsselung im Transport, Löschfristen.
-  Die Aufbewahrung auf dem Server ist einstellbar.
-- Das S3-Ziel legt Archive unverschlüsselt im Bucket ab. Der Bucket muss privat sein.
+- **No credentials in the backup.** `config.php` with database passwords or
+  API keys doesn't belong in `MANAGE_BACKUP_SOURCES`.
+- If the backup contains personal data — the rule for order or customer
+  data — the same requirements apply to the Manage server as to the
+  application itself: access control, transport encryption, deletion
+  deadlines. Retention on the server is configurable.
+- The S3 target stores archives unencrypted in the bucket. The bucket must
+  be private.
 
-## Lokale Verzeichnisse
+## Local directories
 
-Diese Verzeichnisse dürfen nicht über das Web erreichbar sein:
+These directories must not be reachable over the web:
 
-| Pfad | Inhalt |
+| Path | Content |
 |---|---|
-| `data/manage/backups/` | vollständige Betriebsdaten |
-| `data/manage/updates/` | Kopien der überschriebenen Anwendungsdateien |
-| `data/manage/work/` | entpackte Pakete während eines Updates |
-| `manage-client/config.php` | Instanz-Token |
+| `data/manage/backups/` | complete operational data |
+| `data/manage/updates/` | copies of the application files an update overwrote |
+| `data/manage/work/` | extracted packages during an update |
+| `manage-client/config.php` | instance token |
 
-Das mitgelieferte `manage-client/.htaccess` sperrt `config.php`, `lib/` und `bin/`.
-Für `data/` ist die `.htaccess` des Projekts zuständig. Auf nginx müssen die
-entsprechenden `location`-Regeln von Hand gesetzt werden – dort greift keine
-`.htaccess`.
+The bundled `manage-client/.htaccess` locks `config.php`, `lib/` and
+`bin/`. For `data/`, the project's own `.htaccess` is responsible. On
+nginx, the matching `location` rules have to be set by hand — `.htaccess`
+has no effect there.
 
-Prüfen lässt sich das direkt:
+This can be checked directly:
 
 ```bash
-curl -s -o /dev/null -w "%{http_code}\n" https://meinprojekt.example.org/manage-client/config.php
-curl -s -o /dev/null -w "%{http_code}\n" https://meinprojekt.example.org/data/manage/backups/
+curl -s -o /dev/null -w "%{http_code}\n" https://myproject.example.org/manage-client/config.php
+curl -s -o /dev/null -w "%{http_code}\n" https://myproject.example.org/data/manage/backups/
 ```
 
-Beides muss `403` oder `404` liefern, niemals `200`.
+Both must return `403` or `404`, never `200`.
 
-## Die Oberfläche
+## The UI
 
-`ui/panel.php` erlaubt es, Updates auszurollen und Backups herunterzuladen – es ist
-die mächtigste Seite der Anwendung. Deshalb:
+`ui/panel.php` allows deploying updates and downloading backups — it's the
+application's most powerful page. Because of that:
 
-- Das Projekt muss seine Anmeldung **vor** dem Einbinden prüfen.
-- Das Panel bringt eine zusätzliche Prüfung auf `$_SESSION["admin_logged_in"]` mit.
-- `MANAGE_PANEL_SKIP_AUTH_GUARD` deaktiviert nur diese zusätzliche Prüfung. Wer sie
-  setzt, ohne selbst zu prüfen, veröffentlicht Update- und Backup-Funktionen im Netz.
-- Alle Formulare sind CSRF-geschützt.
-- Der Download validiert den Dateinamen streng, damit kein beliebiger Pfad
-  ausgeliefert werden kann.
+- The project must check its login **before** including it.
+- The panel brings an extra check on `$_SESSION["admin_logged_in"]`.
+- `MANAGE_PANEL_SKIP_AUTH_GUARD` disables only this extra check. Setting it
+  without checking the login yourself publishes update and backup functions
+  on the network.
+- All forms are CSRF-protected.
+- The download strictly validates the filename, so no arbitrary path can be
+  served.
 
-Nach Möglichkeit sollte die Seite nur Administratoren zugänglich sein, nicht allen
-angemeldeten Benutzern.
+Where possible, the page should be accessible only to administrators, not
+to every logged-in user.
 
-## Kommandozeile
+## Command line
 
-`bin/manage-client.php` verweigert die Ausführung über HTTP (`PHP_SAPI`-Prüfung) und
-ist zusätzlich per `.htaccess` gesperrt. Auf dem Server sollte die Datei trotzdem
-nicht im öffentlichen Verzeichnisbaum liegen, wenn sich das vermeiden lässt.
+`bin/manage-client.php` refuses to run over HTTP (a `PHP_SAPI` check) and
+is additionally locked via `.htaccess`. On the server, the file should
+still avoid living in the public directory tree where that can be helped.
 
-## Protokolle
+## Logs
 
-Das Client-Protokoll enthält Dateinamen, Versionen, HTTP-Status und Fehlermeldungen.
-Zugangsdaten werden ausgefiltert: Aus Zielkonfigurationen übernimmt der Client nur
-eine Positivliste unkritischer Schlüssel; `access_key`, `secret_key`, `password` und
-das Instanz-Token erscheinen nie im Protokoll.
+The client log contains filenames, versions, HTTP status and error
+messages. Credentials are filtered out: from target configurations, the
+client only picks up an allowlist of non-sensitive keys; `access_key`,
+`secret_key`, `password` and the instance token never appear in the log.
 
-Antwortauszüge von fehlgeschlagenen Uploads werden auf 500 Zeichen gekürzt.
+Response excerpts from failed uploads are truncated to 500 characters.
 
-## Checkliste vor dem Produktivgang
+## Checklist before going live
 
-- [ ] `manage-client/config.php` ist in `.gitignore`
-- [ ] `config.php` und `manage-client/config.php` sind aus dem Release-Paket ausgeschlossen
-- [ ] `data/manage/` ist über das Web nicht erreichbar (geprüft mit `curl`)
-- [ ] `manage-client/config.php` ist über das Web nicht erreichbar (geprüft mit `curl`)
-- [ ] Die Panel-Seite verlangt eine Administrator-Anmeldung
-- [ ] `MANAGE_SERVER_URL` verwendet `https://`
-- [ ] Im Backup stecken keine Zugangsdaten
-- [ ] Ein Backup wurde einmal heruntergeladen und der Inhalt geprüft
+- [ ] `manage-client/config.php` is in `.gitignore`
+- [ ] `config.php` and `manage-client/config.php` are excluded from the release package
+- [ ] `data/manage/` is not reachable over the web (checked with `curl`)
+- [ ] `manage-client/config.php` is not reachable over the web (checked with `curl`)
+- [ ] The panel page requires an administrator login
+- [ ] `MANAGE_SERVER_URL` uses `https://`
+- [ ] No credentials are inside the backup
+- [ ] A backup has been downloaded once and its content checked
 
-## Weiter
+## Next
 
-- [08_PROTOCOL](08_PROTOCOL.md) – Authentifizierung im Detail
-- [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md) – was ins Archiv gehört
+- [08_PROTOCOL](08_PROTOCOL.md) – authentication in detail
+- [05_BACKUP_SOURCES](05_BACKUP_SOURCES.md) – what belongs in the archive

+ 10 - 10
client-package/docs/index.php

@@ -41,18 +41,18 @@ $docsTitle = basename(dirname($docsDir)) === "client-package"
 $requested = isset($_GET["doc"]) ? (string) $_GET["doc"] : "";
 $activeDoc = null;
 $markdown = null;
-$pageTitle = "Dokumentation";
+$pageTitle = "Documentation";
 
 if ($requested !== "") {
     if (!isset($docMap[$requested])) {
         http_response_code(404);
-        $pageTitle = "Nicht gefunden";
+        $pageTitle = "Not Found";
     } else {
         $activeDoc = $requested;
         $markdown = file_get_contents($docMap[$requested]);
         if ($markdown === false) {
             http_response_code(500);
-            $pageTitle = "Fehler";
+            $pageTitle = "Error";
             $markdown = null;
         } else {
             $pageTitle = docTitle($activeDoc);
@@ -68,7 +68,7 @@ if ($baseHref === "") {
 }
 ?>
 <!DOCTYPE html>
-<html lang="de">
+<html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -82,8 +82,8 @@ if ($baseHref === "") {
     </div>
 </header>
 <div class="docs-layout">
-    <nav class="docs-nav" aria-label="Dokumentation">
-        <p class="docs-nav-title">Inhalt</p>
+    <nav class="docs-nav" aria-label="Documentation">
+        <p class="docs-nav-title">Contents</p>
         <ul>
             <?php foreach ($docMap as $key => $_path): ?>
                 <li>
@@ -97,8 +97,8 @@ if ($baseHref === "") {
     </nav>
     <main class="docs-main">
         <?php if ($requested === ""): ?>
-            <h1>Dokumentation</h1>
-            <p>Alle Dokumente in empfohlener Lesereihenfolge.</p>
+            <h1>Documentation</h1>
+            <p>All documents in the recommended reading order.</p>
             <ul class="docs-index-list">
                 <?php foreach ($docMap as $key => $_path): ?>
                     <li>
@@ -110,8 +110,8 @@ if ($baseHref === "") {
             </ul>
         <?php elseif ($markdown === null): ?>
             <h1><?php echo docEscape($pageTitle); ?></h1>
-            <p>Das angeforderte Dokument ist nicht verfügbar.</p>
-            <p><a href="<?php echo docEscape($baseHref); ?>index.php">Zur Übersicht</a></p>
+            <p>The requested document is not available.</p>
+            <p><a href="<?php echo docEscape($baseHref); ?>index.php">Back to the overview</a></p>
         <?php else: ?>
             <article id="doc-content" class="markdown-body"></article>
             <script type="application/json" id="doc-source"><?php

+ 15 - 16
client-package/examples/after-update.php

@@ -2,53 +2,52 @@
 
 declare(strict_types=1);
 
-// Vorlage für MANAGE_UPDATE_POST_HOOK.
+// Template for MANAGE_UPDATE_POST_HOOK.
 //
-// Wird nach jedem erfolgreichen Update ausgeführt – auch dann, wenn keine
-// Migration gelaufen ist. Läuft NACH den Migrationen und wird übersprungen,
-// wenn eine Migration fehlgeschlagen ist.
+// Runs after every successful update - even when no migration ran. Runs
+// AFTER the migrations and is skipped if a migration failed.
 //
-// In manage-client/config.php eintragen:
+// Register it in manage-client/config.php:
 //
 //     define("MANAGE_UPDATE_POST_HOOK", [
 //         "file"     => MANAGE_APP_ROOT . "/includes/after-update.php",
 //         "callback" => "myProjectAfterUpdate",
 //     ]);
 //
-// Diese Datei muss Teil des Release-Pakets sein, sonst fehlt sie nach dem
-// ersten Update.
+// This file must be part of the release package, otherwise it's missing
+// after the first update.
 
 /**
  * @param array $context app_root, instance, from_version, to_version,
- *                       backup_dir, run_id, migrations, ggf. pdo
+ *                       backup_dir, run_id, migrations, pdo if configured
  */
 function myProjectAfterUpdate(array $context): void
 {
-    // 1. Kompilierte Templates und Caches verwerfen.
+    // 1. Discard compiled templates and caches.
     foreach (glob($context["app_root"] . "/data/cache/*.php") ?: [] as $file) {
         @unlink($file);
     }
 
-    // 2. Verzeichnisse anlegen, die ein neues Release voraussetzt.
+    // 2. Create directories a new release requires.
     $newDir = $context["app_root"] . "/data/exports";
     if (!is_dir($newDir) && !mkdir($newDir, 02775, true) && !is_dir($newDir)) {
-        throw new RuntimeException("Verzeichnis konnte nicht angelegt werden: " . $newDir);
+        throw new RuntimeException("Could not create directory: " . $newDir);
     }
 
-    // 3. Für die Nachvollziehbarkeit protokollieren.
+    // 3. Log for traceability.
     @file_put_contents(
         $context["app_root"] . "/data/update-history.log",
         sprintf(
-            "%s  %s -> %s  (Migrationen: %s)%s",
+            "%s  %s -> %s  (migrations: %s)%s",
             date(DATE_ATOM),
-            $context["from_version"] !== "" ? $context["from_version"] : "unbekannt",
+            $context["from_version"] !== "" ? $context["from_version"] : "unknown",
             $context["to_version"],
-            $context["migrations"] === [] ? "keine" : implode(", ", $context["migrations"]),
+            $context["migrations"] === [] ? "none" : implode(", ", $context["migrations"]),
             PHP_EOL,
         ),
         FILE_APPEND | LOCK_EX,
     );
 
-    // Fehler melden: Exception werfen, false zurückgeben oder
+    // To report failure: throw an exception, return false, or
     // return ["success" => false, "error" => "…"].
 }

+ 19 - 19
client-package/examples/cron/manage-client.cron

@@ -1,28 +1,28 @@
-# Manage-Client – Cron-Beispiele
+# Manage Client - cron examples
 #
-# Pfade anpassen und mit `crontab -e` eintragen.
-# PHP-Pfad prüfen mit: which php
+# Adjust the paths and add with `crontab -e`.
+# Check the PHP path with: which php
 #
-# --quiet unterdrückt die normale Ausgabe. Fehler gehen weiterhin auf STDERR
-# und werden von Cron per Mail zugestellt.
+# --quiet suppresses normal output. Errors still go to STDERR and are
+# delivered by cron via mail.
 
 MAILTO=admin@example.org
 
-# Nächtliches Backup um 03:20 Uhr.
-20 3 * * * /usr/bin/php /var/www/meinprojekt/manage-client/bin/manage-client.php backup --trigger=cron --quiet
+# Nightly backup at 03:20.
+20 3 * * * /usr/bin/php /var/www/myproject/manage-client/bin/manage-client.php backup --trigger=cron --quiet
 
-# Statusmeldung an den Manage-Server, stündlich zur Minute 7.
-7 * * * * /usr/bin/php /var/www/meinprojekt/manage-client/bin/manage-client.php heartbeat --quiet
+# Status report to the Manage server, hourly at minute 7.
+7 * * * * /usr/bin/php /var/www/myproject/manage-client/bin/manage-client.php heartbeat --quiet
 
-# Update-Prüfung werktags um 08:00 Uhr.
-# Exit-Code 2 bedeutet "Update verfügbar" – Cron meldet das nicht von sich aus,
-# deshalb hier eine ausdrückliche Mail.
-0 8 * * 1-5 /usr/bin/php /var/www/meinprojekt/manage-client/bin/manage-client.php check --quiet || true
+# Update check on weekdays at 08:00.
+# Exit code 2 means "update available" - cron doesn't report that on its
+# own, hence the explicit mail here.
+0 8 * * 1-5 /usr/bin/php /var/www/myproject/manage-client/bin/manage-client.php check --quiet || true
 
-# Updates werden bewusst NICHT automatisch eingespielt.
-# Ein Update überschreibt Dateien im laufenden Betrieb und kann Migrationen
-# auslösen; das gehört unter Aufsicht. Falls es dennoch automatisiert werden
-# soll, vorher ein Backup erzwingen:
+# Updates are deliberately NOT installed automatically.
+# An update overwrites files while the application is live and can trigger
+# migrations; that belongs under supervision. If it should be automated
+# anyway, force a backup first:
 #
-# 30 2 * * 0 /usr/bin/php /var/www/meinprojekt/manage-client/bin/manage-client.php backup --trigger=update --quiet && \
-#            /usr/bin/php /var/www/meinprojekt/manage-client/bin/manage-client.php update --yes --quiet
+# 30 2 * * 0 /usr/bin/php /var/www/myproject/manage-client/bin/manage-client.php backup --trigger=update --quiet && \
+#            /usr/bin/php /var/www/myproject/manage-client/bin/manage-client.php update --yes --quiet

+ 8 - 8
client-package/examples/flat-file-project/migrations/2026-08-20-01-add-category-id.php

@@ -2,11 +2,11 @@
 
 declare(strict_types=1);
 
-// Beispielmigration für ein Projekt mit JSON-Dateien:
-// ergänzt ein neues Feld in allen bestehenden Datensätzen.
+// Example migration for a project with JSON files: adds a new field to
+// every existing record.
 //
-// Idempotent: bereits ergänzte Datensätze bleiben unverändert, damit die
-// Migration nach einem Teilfehler gefahrlos erneut laufen kann.
+// Idempotent: already-updated records stay unchanged, so the migration can
+// safely run again after a partial failure.
 
 return function (array $context): void {
     $file = $context["app_root"] . "/data/products.json";
@@ -16,7 +16,7 @@ return function (array $context): void {
 
     $data = json_decode((string) file_get_contents($file), true);
     if (!is_array($data)) {
-        throw new RuntimeException("products.json ist nicht lesbar oder kein gültiges JSON.");
+        throw new RuntimeException("products.json is not readable or not valid JSON.");
     }
 
     $changed = false;
@@ -36,13 +36,13 @@ return function (array $context): void {
         JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
     );
     if ($json === false) {
-        throw new RuntimeException("products.json konnte nicht kodiert werden.");
+        throw new RuntimeException("products.json could not be encoded.");
     }
 
-    // Atomar schreiben, damit ein Abbruch keine halbe Datei hinterlässt.
+    // Write atomically, so an interruption never leaves a half-written file.
     $tmpFile = $file . ".tmp";
     if (file_put_contents($tmpFile, $json, LOCK_EX) === false || !rename($tmpFile, $file)) {
         @unlink($tmpFile);
-        throw new RuntimeException("products.json konnte nicht geschrieben werden.");
+        throw new RuntimeException("products.json could not be written.");
     }
 };

+ 13 - 13
client-package/examples/integration-snippet.php

@@ -1,17 +1,17 @@
 <?php
 
-// Die Zeilen, die ein bestehendes Projekt braucht.
-// Zum Kopieren gedacht, nicht zum direkten Ausführen.
+// The lines an existing project needs.
+// Meant to be copied, not run directly.
 
 // ---------------------------------------------------------------------------
-// 1. Adminseite für Update und Backup: <projekt>/admin/manage.php
+// 1. Admin page for update and backup: <project>/admin/manage.php
 // ---------------------------------------------------------------------------
 
 require_once __DIR__ . "/../config.php";
 require_once __DIR__ . "/../includes/functions.php";
 
-// Anmeldung des Projekts zuerst prüfen – das Panel ist die mächtigste Seite
-// der Anwendung.
+// Check the project's own login first - the panel is the application's
+// most powerful page.
 if (empty($_SESSION["admin_logged_in"])) {
     header("Location: login.php");
     exit;
@@ -20,35 +20,35 @@ if (empty($_SESSION["admin_logged_in"])) {
 require __DIR__ . "/../manage-client/ui/panel.php";
 
 // ---------------------------------------------------------------------------
-// 2. Statusblock auf einer bestehenden Einstellungsseite
+// 2. Status block on an existing settings page
 // ---------------------------------------------------------------------------
 
 $manageStatusPanelUrl = "manage.php";
 include __DIR__ . "/../manage-client/ui/status-partial.php";
 
 // ---------------------------------------------------------------------------
-// 3. Automatisches Backup ohne Cron, z. B. auf dem Admin-Dashboard
+// 3. Automatic backup without cron, e.g. on the admin dashboard
 // ---------------------------------------------------------------------------
 
 require_once __DIR__ . "/../manage-client/lib/client.php";
 
 try {
-    // Legt nur an, wenn MANAGE_BACKUP_AUTO_INTERVAL_SECONDS abgelaufen ist.
+    // Only creates one once MANAGE_BACKUP_AUTO_INTERVAL_SECONDS has elapsed.
     manageBackupCreateAutomaticIfDue();
 } catch (Throwable $exception) {
-    // Ein fehlgeschlagenes Backup darf das Dashboard nicht blockieren.
-    error_log("Automatisches Backup fehlgeschlagen: " . $exception->getMessage());
+    // A failed backup must not block the dashboard.
+    error_log("Automatic backup failed: " . $exception->getMessage());
 }
 
 // ---------------------------------------------------------------------------
-// 4. Update-Hinweis in der eigenen Navigation
+// 4. Update notice in the project's own navigation
 // ---------------------------------------------------------------------------
 
 require_once __DIR__ . "/../manage-client/lib/client.php";
 
-$manageStatus = manageClientStatus(); // wirft nie
+$manageStatus = manageClientStatus(); // never throws
 if ($manageStatus["update"] !== null && $manageStatus["update"]["available"]) {
     echo '<a href="manage.php">Update ' .
         htmlspecialchars($manageStatus["update"]["latest"], ENT_QUOTES, "UTF-8") .
-        " verfügbar</a>";
+        " available</a>";
 }

+ 5 - 5
client-package/examples/mysql-project/migrations/2026-08-20-01-add-orders-index.php

@@ -2,16 +2,16 @@
 
 declare(strict_types=1);
 
-// Beispielmigration mit Datenbank.
+// Example migration with a database.
 //
-// $context["pdo"] ist die Verbindung aus MANAGE_BACKUP_DATABASE.
-// Idempotent: prüft erst, ob der Index bereits existiert. MySQL kennt kein
-// "ADD INDEX IF NOT EXISTS", deshalb die Abfrage über information_schema.
+// $context["pdo"] is the connection from MANAGE_BACKUP_DATABASE.
+// Idempotent: checks first whether the index already exists. MySQL has no
+// "ADD INDEX IF NOT EXISTS", hence the query against information_schema.
 
 return function (array $context): void {
     if (!isset($context["pdo"])) {
         throw new RuntimeException(
-            "Diese Migration benötigt eine Datenbank. MANAGE_BACKUP_DATABASE ist nicht konfiguriert.",
+            "This migration needs a database. MANAGE_BACKUP_DATABASE is not configured.",
         );
     }
 

+ 79 - 77
docs/ARCHITECTURE.md

@@ -1,65 +1,66 @@
-# Architektur
+# Architecture
 
-## Überblick
+## Overview
 
-`manage` besteht aus zwei Hälften: dem Server in diesem Repository und dem
-Client-Paket, das in jedes betreute Projekt kopiert wird.
+`manage` consists of two halves: the server in this repository, and the
+client package that gets copied into each served project.
 
-Eine Server-Installation betreut **ein Produkt** mit einer überschaubaren Zahl von
-Instanzen. Für ein weiteres Produkt wird `manage` erneut ausgerollt.
+One server installation serves **one product** with a manageable number of
+instances. For another product, `manage` is deployed again.
 
-Relevante Verzeichnisse:
+Relevant directories:
 
-- `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
+- `admin/` – UI
+- `api/v1/` – client interface
+- `includes/` – shared library
+- `storage/` – all state, not reachable over the web
+- `client-package/` – the redistributable folder for projects
 
-## Datenfluss
+## Data flow
 
 ```text
-Projekt (Instanz)                        Manage-Server
-─────────────────                        ─────────────
+Project (instance)                       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/<instanz>/
+  lib/*.php              ──── backup ──►  api/v1/backup.php    ──► storage/backups/<instance>/
                          ──── status ──►  api/v1/heartbeat.php ──► storage/instances.json
                                                                         │
                                                                         ▼
-                                                            admin/  (Anmeldung mit Passwort)
+                                                            admin/  (password login)
 ```
 
-Der Client zieht; der Server schiebt nie. Es gibt keine Verbindung vom Server zur
-Instanz, was den Betrieb hinter NAT und Firewalls unkompliziert macht.
+The client pulls; the server never pushes. There is no connection from the
+server to the instance, which makes running it behind NAT and firewalls
+straightforward.
 
-## Authentifizierung
+## Authentication
 
-Zwei getrennte Wege:
+Two separate paths:
 
-| Weg | Wer | Mittel |
+| Path | Who | Means |
 |---|---|---|
-| `admin/` | Menschen | ein Passwort, Sitzung, CSRF, Ratenbegrenzung |
-| `api/v1/` | Instanzen | Instanz-Kennung + Token in zwei Headern, zustandslos |
+| `admin/` | humans | a password, session, CSRF, rate limiting |
+| `api/v1/` | instances | instance id + token in two headers, stateless |
 
-Tokens werden als SHA-256-Hash gespeichert und in konstanter Zeit verglichen. Das
-Klartext-Token erscheint genau einmal, beim Anlegen oder Erneuern.
+Tokens are stored as a SHA-256 hash and compared in constant time. The
+plaintext token appears exactly once, when created or rotated.
 
 Details: [client-package/docs/08_PROTOCOL.md](../client-package/docs/08_PROTOCOL.md).
 
-## Instanzregister
+## Instance registry
 
-`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.
+`storage/instances.json` is the link between both modules. It replaces two
+separate mechanisms from the predecessor solution: the backup server's name
+list, and the update server's completely missing client identity.
 
 ```json
 {
     "instances": [
         {
             "id": "example-prod",
-            "label": "Stadt Freising Produktiv",
+            "label": "City of Freising Production",
             "enabled": true,
             "token_hash": "…",
             "created_at": "…",
@@ -78,16 +79,17 @@ und die vollständig fehlende Client-Identität des Update-Servers.
 }
 ```
 
-Jede erfolgreich authentifizierte Anfrage aktualisiert `last_seen_at` und `last_ip`.
-Die Übersicht bleibt dadurch aktuell, auch ohne eigenen Heartbeat.
+Every successfully authenticated request updates `last_seen_at` and
+`last_ip`. The overview stays current this way, even without its own
+heartbeat.
 
-Eine gelöschte Instanz kann nichts mehr hochladen; ihre bereits gespeicherten
-Backups bleiben aber erhalten und in der Oberfläche sichtbar.
+A deleted instance can no longer upload anything; its already-stored backups
+remain and stay visible in the UI.
 
 ## Releases
 
-`storage/releases/manifest.json` ist die Release-Datenbank, die Pakete liegen
-daneben in `packages/`:
+`storage/releases/manifest.json` is the release database; the packages sit
+next to it in `packages/`:
 
 ```json
 {
@@ -104,65 +106,65 @@ daneben in `packages/`:
 }
 ```
 
-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`.
+Checksum and size are always computed by the server after upload; they are
+never taken from the uploader. An upload automatically sets the release as
+`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.
+The download URL is built from `MANAGE_PUBLIC_URL`, **not** from the `Host`
+header. The predecessor solution derived it from `HTTP_HOST` — a value
+controlled by the client.
 
 ## Backups
 
 ```text
 storage/backups/
-  index.json                     Metadaten aller Backups
-  <instanz>/backup-YYYYmmdd-HHMMSS[-N].zip
+  index.json                     metadata of all backups
+  <instance>/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.
+After storing a file the server re-checks the checksum and deletes it on
+mismatch. An existing filename is never overwritten.
 
-### Zwei Aufbewahrungsstufen
+### Two retention tiers
 
-Bei aktivem S3-Archiv arbeitet die lokale Platte als schneller Zwischenspeicher und
-der Bucket als vollständiges Archiv:
+With the S3 archive active, the local disk works as a fast staging area and
+the bucket as the complete archive:
 
-- **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.
+- **S3**: keeps the most recent `s3_retention` backups per instance (default 365).
+- **Local**: keeps the most recent `retention` backups (default 30), but
+  **never** deletes a file while its S3 upload is still pending.
 
-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.
+If S3 is unreachable, the local copies grow past their retention instead of
+losing the only copy. Failed S3 uploads are retried on the next upload from
+the same instance, or via the button in the UI.
 
-S3-Fehler lassen einen Client-Upload nie fehlschlagen: Die lokale Kopie liegt bereits
-vor. Protokolliert werden sie in `storage/logs/s3.log`.
+S3 errors never fail a client upload: the local copy already exists. They
+are logged to `storage/logs/s3.log`.
 
-## Speicherung
+## Storage
 
-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.
+Flat files only, no database server. Every write goes through
+`manageWriteJsonFile()`: first into a `.tmp` file, then `rename()`. That way
+an aborted request can never leave a half-written index behind.
 
-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.
+Known limit: concurrent uploads from the same instance can collide while
+writing `index.json`. With a handful of instances doing nightly backups this
+is practically excluded; with many concurrent uploads a lock would be
+needed.
 
-## Protokolle
+## Logs
 
-| Datei | Inhalt |
+| File | Content |
 |---|---|
-| `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 |
+| `storage/logs/access.log` | JSONL: logins, releases, received backups, downloads |
+| `storage/logs/error.log` | JSONL: failed logins, rejected uploads, internal errors |
+| `storage/logs/s3.log` | plain text: S3 diagnostics with status, redirects and request id |
 
-Die JSONL-Protokolle rotieren nach `MANAGE_LOG_MAX_BYTES` und werden nach
-`MANAGE_LOG_MAX_AGE_SECONDS` entfernt.
+The JSONL logs rotate past `MANAGE_LOG_MAX_BYTES` and are removed after
+`MANAGE_LOG_MAX_AGE_SECONDS`.
 
-## Weiter
+## Next
 
-- [SERVER_SETUP](SERVER_SETUP.md) – Installation
-- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – Instanzen und Tokens
-- [RELEASING](RELEASING.md) – Releases veröffentlichen
+- [SERVER_SETUP](SERVER_SETUP.md) – installation
+- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – instances and tokens
+- [RELEASING](RELEASING.md) – publishing releases

+ 76 - 74
docs/CONFIG_REFERENCE.md

@@ -1,122 +1,124 @@
-# Konfigurationsreferenz (Server)
+# Configuration Reference (Server)
 
-## Überblick
+## Overview
 
-Alle Konstanten stehen in `config.php`, kopiert aus `config.sample.php`. Jede hat
-einen Standardwert in `includes/bootstrap.php`; eine minimale `config.php` braucht
-nur `MANAGE_PUBLIC_URL` und `MANAGE_ADMIN_PASSWORD_HASH`.
+All constants live in `config.php`, copied from `config.sample.php`. Each has
+a default in `includes/bootstrap.php`; a minimal `config.php` only needs
+`MANAGE_PUBLIC_URL` and `MANAGE_ADMIN_PASSWORD_HASH`.
 
-Die Konstanten des **Clients** stehen in
+The **client's** constants are in
 [../client-package/docs/03_CONFIG_REFERENCE.md](../client-package/docs/03_CONFIG_REFERENCE.md).
 
-## Produkt
+## Product
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_PRODUCT_NAME` | `"Managed Application"` | Anzeigename in der Oberfläche |
-| `MANAGE_PACKAGE_PREFIX` | `"release"` | Dateinamenspräfix gespeicherter Pakete: `<prefix>-vX.Y.Z.zip` |
+| `MANAGE_PRODUCT_NAME` | `"Managed Application"` | display name in the UI |
+| `MANAGE_PACKAGE_PREFIX` | `"release"` | filename prefix of stored packages: `<prefix>-vX.Y.Z.zip` |
 
-## Öffentliche URL
+## Public URL
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_PUBLIC_URL` | `""` | Absolute Basis-URL dieser Installation, ohne Schrägstrich am Ende |
+| `MANAGE_PUBLIC_URL` | `""` | absolute base URL of this installation, without a trailing slash |
 
-Aus diesem Wert wird die Download-URL gebildet, die Clients im Manifest erhalten.
-Er wird bewusst nicht aus dem `Host`-Header abgeleitet: Ein gefälschter Header
-könnte einen Client sonst auf einen fremden Server umlenken.
+This value builds the download URL clients receive in the manifest. It is
+deliberately not derived from the `Host` header: a spoofed header could
+otherwise redirect a client to a foreign server.
 
-Ist der Wert leer, meldet die Übersicht eine Warnung und `manifest.php` antwortet
-mit einem Fehler.
+If the value is empty, the overview reports a warning and `manifest.php`
+responds with an error.
 
-## Anmeldung
+## Login
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_ADMIN_PASSWORD_HASH` | – | bcrypt-Hash, mit `password_verify()` geprüft |
-| `MANAGE_ADMIN_PASSWORD` | – | Klartext-Alternative, mit `hash_equals()` geprüft |
+| `MANAGE_ADMIN_PASSWORD_HASH` | – | bcrypt hash, checked with `password_verify()` |
+| `MANAGE_ADMIN_PASSWORD` | – | plaintext alternative, checked with `hash_equals()` |
 
-Der Hash hat Vorrang. Er wird ignoriert, solange er noch den Platzhalter aus
-`config.sample.php` enthält – so scheitert eine unfertige Konfiguration sichtbar,
-statt eine offene Anmeldung zu erlauben.
+The hash takes precedence. It is ignored as long as it still holds the
+placeholder from `config.sample.php` — so an unfinished configuration fails
+visibly instead of allowing an open login.
 
 ```bash
-php -r 'echo password_hash("ein-langes-passwort", PASSWORD_DEFAULT), PHP_EOL;'
+php -r 'echo password_hash("a-long-password", PASSWORD_DEFAULT), PHP_EOL;'
 ```
 
-In einfache Anführungszeichen setzen, ein bcrypt-Hash enthält `$`.
+Use single quotes; a bcrypt hash contains `$`.
 
-## Speicher
+## Storage
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_STORAGE_DIR` | `__DIR__ . "/storage/"` | Wurzel für Instanzen, Releases, Backups, Protokolle |
+| `MANAGE_STORAGE_DIR` | `__DIR__ . "/storage/"` | root for instances, releases, backups, logs |
 
-Alle weiteren Pfade leiten sich davon ab und müssen nicht einzeln gesetzt werden:
-`storage/instances.json`, `storage/settings.json`, `storage/releases/manifest.json`,
-`storage/releases/packages/`, `storage/backups/`, `storage/logs/`.
+All other paths derive from this one and don't need to be set individually:
+`storage/instances.json`, `storage/settings.json`,
+`storage/releases/manifest.json`, `storage/releases/packages/`,
+`storage/backups/`, `storage/logs/`.
 
-Das Verzeichnis darf nicht über das Web erreichbar sein.
+The directory must not be reachable over the web.
 
 ## Backups
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_BACKUP_RETENTION` | `30` | Lokale Backups pro Instanz. Minimum 1 |
-| `MANAGE_BACKUP_MAX_UPLOAD_BYTES` | `0` | Zusätzliches Größenlimit; `0` deaktiviert |
+| `MANAGE_BACKUP_RETENTION` | `30` | local backups per instance. Minimum 1 |
+| `MANAGE_BACKUP_MAX_UPLOAD_BYTES` | `0` | extra size limit; `0` disables it |
 
-Der in der Oberfläche gespeicherte Wert (`storage/settings.json`) hat Vorrang vor
-`MANAGE_BACKUP_RETENTION`, sobald er einmal gespeichert wurde.
+The value stored in the UI (`storage/settings.json`) takes precedence over
+`MANAGE_BACKUP_RETENTION` once it has been saved.
 
-`MANAGE_BACKUP_MAX_UPLOAD_BYTES` liegt **über** den PHP-Grenzen: `upload_max_filesize`
-und `post_max_size` greifen ohnehin und sind meist niedriger.
+`MANAGE_BACKUP_MAX_UPLOAD_BYTES` sits **above** the PHP limits:
+`upload_max_filesize` and `post_max_size` apply regardless and are usually
+lower.
 
-## S3-Archiv
+## S3 archive
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_S3_ENABLED` | `false` | Archivierung einschalten |
-| `MANAGE_S3_ENDPOINT` | `""` | z. B. `https://fsn1.your-objectstorage.com` |
-| `MANAGE_S3_REGION` | `""` | Region für die Signatur |
-| `MANAGE_S3_BUCKET` | `""` | Bucket-Name |
-| `MANAGE_S3_PREFIX` | `""` | Schlüsselpräfix im Bucket, darf leer sein |
-| `MANAGE_S3_ACCESS_KEY` | `""` | Access Key |
-| `MANAGE_S3_SECRET_KEY` | `""` | Secret Key |
+| `MANAGE_S3_ENABLED` | `false` | turn archiving on |
+| `MANAGE_S3_ENDPOINT` | `""` | e.g. `https://fsn1.your-objectstorage.com` |
+| `MANAGE_S3_REGION` | `""` | region for the signature |
+| `MANAGE_S3_BUCKET` | `""` | bucket name |
+| `MANAGE_S3_PREFIX` | `""` | key prefix in the bucket, may be empty |
+| `MANAGE_S3_ACCESS_KEY` | `""` | access key |
+| `MANAGE_S3_SECRET_KEY` | `""` | secret key |
 | `MANAGE_S3_PATH_STYLE` | `false` | `false` = virtual-hosted, `true` = path-style |
-| `MANAGE_S3_TIMEOUT` | `120` | Sekunden je HTTP-Anfrage |
-| `MANAGE_S3_RETENTION` | `365` | S3-Backups pro Instanz |
+| `MANAGE_S3_TIMEOUT` | `120` | seconds per HTTP request |
+| `MANAGE_S3_RETENTION` | `365` | S3 backups per instance |
 
-Das Archiv gilt nur als aktiv, wenn `MANAGE_S3_ENABLED` gesetzt **und** Endpunkt,
-Region, Bucket, Access Key und Secret Key gefüllt sind. Eine halbe Konfiguration
-bleibt wirkungslos statt bei jedem Upload zu scheitern.
+The archive only counts as active when `MANAGE_S3_ENABLED` is set **and**
+endpoint, region, bucket, access key and secret key are all filled in. A
+half-done configuration stays inert instead of failing on every upload.
 
-Verhalten und Fehlersuche: [SERVER_SETUP](SERVER_SETUP.md).
+Behavior and troubleshooting: [SERVER_SETUP](SERVER_SETUP.md).
 
-## Ratenbegrenzung
+## Rate limiting
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_LOGIN_RATE_LIMIT_MAX` | `10` | Fehlversuche an der Oberfläche je Zeitfenster und IP |
-| `MANAGE_LOGIN_RATE_LIMIT_WINDOW` | `900` | Zeitfenster in Sekunden |
-| `MANAGE_API_RATE_LIMIT_MAX` | `240` | Fehlgeschlagene API-Authentifizierungen je Fenster und IP |
-| `MANAGE_API_RATE_LIMIT_WINDOW` | `300` | Zeitfenster in Sekunden |
+| `MANAGE_LOGIN_RATE_LIMIT_MAX` | `10` | failed UI attempts per window and IP |
+| `MANAGE_LOGIN_RATE_LIMIT_WINDOW` | `900` | window in seconds |
+| `MANAGE_API_RATE_LIMIT_MAX` | `240` | failed API authentications per window and IP |
+| `MANAGE_API_RATE_LIMIT_WINDOW` | `300` | window in seconds |
 
-Der Zustand liegt in `storage/ratelimit/`. Eine erfolgreiche Authentifizierung
-setzt den Zähler der IP zurück. Ist der Zustand nicht schreibbar, lässt die
-Begrenzung bewusst durch, statt alle auszusperren.
+State lives in `storage/ratelimit/`. A successful authentication resets the
+IP's counter. If the state directory isn't writable, the limiter
+deliberately lets requests through instead of locking everyone out.
 
-## Protokolle
+## Logs
 
-| Konstante | Standard | Bedeutung |
+| Constant | Default | Meaning |
 |---|---|---|
-| `MANAGE_LOG_MAX_BYTES` | `1048576` | Rotation ab dieser Größe |
-| `MANAGE_LOG_KEEP_FILES` | `5` | Anzahl rotierter Dateien |
-| `MANAGE_LOG_MAX_AGE_SECONDS` | `2592000` | Rotierte Dateien danach löschen (30 Tage) |
+| `MANAGE_LOG_MAX_BYTES` | `1048576` | rotate once a log reaches this size |
+| `MANAGE_LOG_KEEP_FILES` | `5` | number of rotated files kept |
+| `MANAGE_LOG_MAX_AGE_SECONDS` | `2592000` | delete rotated files after this (30 days) |
 
-Gilt für `access.log` und `error.log`. `s3.log` wächst unbegrenzt und wird bei
-Bedarf von Hand gekürzt.
+Applies to `access.log` and `error.log`. `s3.log` grows unbounded and is
+trimmed by hand when needed.
 
-## Beispiel für eine minimale config.php
+## Example minimal config.php
 
 ```php
 <?php
@@ -127,4 +129,4 @@ define("MANAGE_PUBLIC_URL", "https://manage.example.org");
 define("MANAGE_ADMIN_PASSWORD_HASH", '$2y$12$…');
 ```
 
-Alles Weitere übernimmt die Standardwerte.
+Everything else takes the default values.

+ 75 - 71
docs/INSTANCE_MANAGEMENT.md

@@ -1,107 +1,111 @@
-# Instanzen verwalten
+# Managing Instances
 
-## Überblick
+## Overview
 
-Eine Instanz ist eine Installation des betreuten Produkts – etwa "Produktiv",
-"Test" oder die Installation eines bestimmten Kunden. Jede Instanz hat eine
-Kennung und ein geheimes Token.
+An instance is one installation of the served product — for example
+"Production", "Test", or a specific customer's installation. Every instance
+has an id and a secret token.
 
-Alles dazu unter **Instanzen** in der Oberfläche.
+Everything related lives under **Instances** in the UI.
 
-## Instanz anlegen
+## Creating an instance
 
-1. **Instanzen** öffnen, Kennung eintragen, optional Bezeichnung und Notiz.
-2. Anlegen. Direkt danach wird das Token angezeigt – **einmalig**, zusammen mit
-   einem fertigen Konfigurationsblock zum Kopieren.
-3. Den Block in die `manage-client/config.php` der Instanz eintragen.
+1. Open **Instances**, enter an id, optionally a label and a note.
+2. Create. Right after that the token is shown — **once**, together with a
+   ready-made configuration block to copy.
+3. Paste the block into the instance's `manage-client/config.php`.
 
-Kennungen dürfen Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten,
-müssen mit einem Buchstaben oder einer Zahl beginnen und höchstens 120 Zeichen lang
-sein. Sie erscheinen im Dateipfad der Backups; sprechende Namen wie `example-prod` und
-`example-test` zahlen sich aus.
+Ids may contain letters, digits, dot, underscore and hyphen, must start with
+a letter or a digit, and can be at most 120 characters long. They appear in
+the backup file path; descriptive names such as `example-prod` and
+`example-test` pay off.
 
-## Das Token
+## The token
 
-- 32 zufällige Bytes, als 64 Hex-Zeichen dargestellt.
-- Der Server speichert nur den SHA-256-Hash. Es gibt keinen Weg, ein Token später
-  wieder anzuzeigen.
-- Geht es verloren, wird ein neues erzeugt – das alte wird dabei sofort ungültig.
+- 32 random bytes, represented as 64 hex characters.
+- The server stores only the SHA-256 hash. There is no way to display a
+  token again later.
+- If it's lost, a new one is generated — the old one becomes invalid
+  immediately.
 
-Mit dem Token kann eine Instanz Releases herunterladen und Backups hochladen. Sie
-kann **nicht** Backups herunterladen und **nicht** Releases verändern; beides
-erfordert die Anmeldung an der Oberfläche.
+The token lets an instance download releases and upload backups. It
+**cannot** download backups or change releases; both require logging into
+the UI.
 
-### Token erneuern
+### Rotating the token
 
-**Instanzen → Token erneuern**. Das alte Token verliert sofort seine Gültigkeit;
-die Instanz meldet danach `Authentifizierung fehlgeschlagen`, bis das neue Token
-eingetragen ist. Kurze Ausfälle von Cron-Jobs sind also einzuplanen.
+**Instances → Rotate token**. The old token loses its validity immediately;
+the instance then reports `Authentifizierung fehlgeschlagen` ("authentication
+failed" — the literal, still German, text the API returns) until the new
+token is entered. Plan for short outages of cron jobs accordingly.
 
-Anlässe: Verdacht auf Kompromittierung, Personalwechsel, Übergabe eines Projekts.
+Reasons to rotate: suspected compromise, staff changes, handing over a
+project.
 
-## Deaktivieren statt löschen
+## Deactivate instead of delete
 
-**Deaktivieren** lässt die Instanz bestehen, weist aber jede API-Anfrage mit `403`
-ab. Der richtige Weg, wenn eine Installation vorübergehend stillgelegt wird oder
-etwas unklar ist – das Token bleibt gültig und die Instanz ist mit einem Klick
-wieder betriebsbereit.
+**Deactivate** leaves the instance in place but rejects every API request
+with `403`. The right move when an installation is temporarily shut down or
+something is unclear — the token stays valid and the instance is back in
+service with one click.
 
-**Entfernen** löscht den Registereintrag. Die gespeicherten Backups bleiben
-erhalten und unter **Backups** sichtbar und herunterladbar; sie werden dort als
-"nicht mehr registriert" gekennzeichnet. Neue Uploads sind nicht mehr möglich.
+**Remove** deletes the registry entry. The stored backups remain and stay
+visible and downloadable under **Backups**; there they are marked as "no
+longer registered". New uploads are no longer possible.
 
-## Statusanzeige
+## Status display
 
-Die Übersicht zeigt für jede Instanz:
+The overview shows, for each instance:
 
-| Feld | Herkunft |
+| Field | Source |
 |---|---|
-| Status | `aktiv`, `inaktiv` (länger als 7 Tage nicht gesehen) oder `deaktiviert` |
-| Version | letzte Meldung der Instanz |
-| Update | Vergleich dieser Version mit dem aktuellen Release |
-| Zuletzt gesehen | jede authentifizierte Anfrage aktualisiert diesen Wert |
-| Letztes Backup | Zeitpunkt des letzten empfangenen Backups |
-| Offene Migrationen | aus dem Heartbeat der Instanz |
+| Status | `active`, `inactive` (not seen for more than 7 days) or `deactivated` |
+| Version | the instance's latest report |
+| Update | comparison of this version against the current release |
+| Last seen | every authenticated request updates this value |
+| Last backup | timestamp of the last received backup |
+| Pending migrations | from the instance's heartbeat |
 
-`inaktiv` bei einer laufenden Installation bedeutet meist, dass der Cron-Job für
-den Heartbeat nicht läuft. Ohne Cron meldet sich eine Instanz nur, wenn jemand die
-Oberfläche im Projekt benutzt.
+`inactive` on a running installation usually means the heartbeat cron job
+isn't running. Without cron, an instance only checks in when someone uses
+the UI in the project.
 
-Offene Migrationen sind das wichtigste Warnsignal: Sie bedeuten, dass ein Update
-zwar ausgerollt wurde, ein Teil des Post-Update-Schritts aber fehlgeschlagen ist.
+Pending migrations are the most important warning sign: they mean an update
+was rolled out, but part of the post-update step failed.
 
-## Client-Paket übergeben
+## Handing over the client package
 
-Der Ordner `client-package/` enthält den Client **und** dessen vollständige
-Dokumentation. Zum Weitergeben:
+The `client-package/` folder contains the client **and** its complete
+documentation. To hand it over:
 
 ```bash
 ./scripts/build-client-package.sh --server-url https://manage.example.org
 ```
 
-Das Ergebnis liegt unter `build/manage-client-<datum>.zip`. Mit `--server-url` ist
-die Server-Adresse in der mitgelieferten `config.sample.php` bereits eingetragen;
-die empfangende Seite ergänzt nur noch Kennung und Token.
+The result lands under `build/manage-client-<date>.zip`. With
+`--server-url`, the server address is already filled into the bundled
+`config.sample.php`; the receiving side only has to add the id and token.
 
-Das Skript entfernt vor dem Packen jede `config.php` und alle Protokolldateien,
-damit kein Token aus einer Testinstallation mitgeliefert wird.
+The script removes every `config.php` and all log files before packing, so
+no token from a test installation is shipped along.
 
-## Mehrere Umgebungen
+## Multiple environments
 
-Übliches Vorgehen für ein Produkt mit Test- und Produktivsystem:
+Common setup for a product with a test and a production system:
 
-| Instanz | Zweck |
+| Instance | Purpose |
 |---|---|
-| `produkt-test` | bekommt neue Releases zuerst |
-| `produkt-prod` | folgt nach erfolgreichem Test |
+| `product-test` | gets new releases first |
+| `product-prod` | follows after a successful test |
 
-Beide holen sich dasselbe `latest`. Wer ein Release nur auf dem Testsystem haben
-will, veröffentlicht es und setzt vorübergehend das ältere wieder als aktuell –
-oder rollt auf dem Testsystem mit `update --force` gezielt aus. Getrennte Kanäle
-pro Instanz gibt es bewusst nicht; dafür wird ein zweiter Manage-Server ausgerollt.
+Both fetch the same `latest`. To keep a release on the test system only,
+publish it and temporarily set the older one back as current — or roll it
+out on the test system deliberately with `update --force`. Separate
+channels per instance are deliberately absent; a second Manage server is
+deployed for that.
 
-## Weiter
+## Next
 
-- [SERVER_SETUP](SERVER_SETUP.md) – Installation
-- [RELEASING](RELEASING.md) – Releases veröffentlichen
-- [../client-package/docs/08_PROTOCOL.md](../client-package/docs/08_PROTOCOL.md) – Authentifizierung im Detail
+- [SERVER_SETUP](SERVER_SETUP.md) – installation
+- [RELEASING](RELEASING.md) – publishing releases
+- [../client-package/docs/08_PROTOCOL.md](../client-package/docs/08_PROTOCOL.md) – authentication in detail

+ 61 - 59
docs/RELEASING.md

@@ -1,96 +1,98 @@
-# Releases veröffentlichen
+# Publishing Releases
 
-## Überblick
+## Overview
 
-Ein Release ist ein ZIP, dessen Wurzel der Anwendungsstamm des Projekts ist. Der
-Client rollt es über die bestehende Installation aus.
+A release is a ZIP whose root is the project's application root. The client
+rolls it out over the existing installation.
 
-Ablauf: bauen → hochladen → als aktuell setzen → Instanzen holen es ab.
+Flow: build → upload → set as current → instances fetch it.
 
-## 1. Paket bauen
+## 1. Build the package
 
-`client-package/scripts/create-release-zip.sh` ist die Vorlage. Sie wird mit dem
-Client-Paket ausgeliefert, einmal pro Projekt angepasst (Produktname,
-Versionsdatei, Ausschlussliste), ins Projekt kopiert und dort aufgerufen:
+`client-package/scripts/create-release-zip.sh` is the template. It ships
+with the client package, gets adjusted once per project (product name,
+version file, exclusion list), gets copied into the project, and is run
+there:
 
 ```bash
 ./scripts/create-release-zip.sh v1.3.0
 ```
 
-Das Skript
+The script
 
-1. schreibt die Version in die Versionsdatei,
-2. prüft nach, dass das Schreiben tatsächlich funktioniert hat,
-3. packt alle von Git verfolgten Dateien abzüglich der Ausschlussliste,
-4. gibt Dateizahl, Größe und SHA-256 aus.
+1. writes the version into the version file,
+2. verifies that the write actually took effect,
+3. packs every file tracked by Git, minus the exclusion list,
+4. prints file count, size and SHA-256.
 
-Es warnt, wenn die Arbeitskopie ungesicherte Änderungen enthält: Weil die
-Dateiliste aus `git ls-files` stammt, landen nicht eingecheckte Änderungen sonst
-still nicht im Paket.
+It warns if the working copy has uncommitted changes: since the file list
+comes from `git ls-files`, uncommitted changes would otherwise silently be
+left out of the package.
 
-Ausführlich – Aufbau des Pakets, Ausschlüsse, häufige Fehler:
+Details — package layout, exclusions, common mistakes:
 [../client-package/docs/06_UPDATE_PACKAGING.md](../client-package/docs/06_UPDATE_PACKAGING.md).
 
-## 2. Hochladen
+## 2. Upload
 
-**Releases** öffnen, Version im Format `vX.Y.Z` eintragen, ZIP auswählen,
-hochladen.
+Open **Releases**, enter a version in `vX.Y.Z` format, choose the ZIP,
+upload.
 
-Der Server
+The server
 
-- prüft die Endung und die ZIP-Signatur der Datei,
-- speichert sie als `<MANAGE_PACKAGE_PREFIX>-<version>.zip`,
-- berechnet SHA-256 und Größe **selbst** und trägt sie ins Manifest ein,
-- setzt das Release als aktuell.
+- checks the file's extension and ZIP signature,
+- stores it as `<MANAGE_PACKAGE_PREFIX>-<version>.zip`,
+- computes SHA-256 and size **itself** and records them in the manifest,
+- sets the release as current.
 
-Die angezeigte Prüfsumme sollte mit der des Build-Skripts übereinstimmen. Tut sie
-das nicht, wurde eine andere Datei hochgeladen.
+The checksum shown should match the one from the build script. If it
+doesn't, a different file was uploaded.
 
-Schlägt der Upload ohne erkennbaren Grund fehl, ist meist das PHP-Upload-Limit
-kleiner als das Paket. Die geltenden Werte stehen auf der Releases-Seite und unter
-**Einstellungen → Diagnose**.
+If the upload fails for no apparent reason, the PHP upload limit is usually
+smaller than the package. The current values are shown on the Releases page
+and under **Settings → Diagnostics**.
 
-## 3. Aktuelles Release wählen
+## 3. Choose the current release
 
-Ein Upload setzt das neue Release automatisch als aktuell. Über **Als aktuell
-setzen** kann jederzeit ein anderes gewählt werden – das ist auch der Weg, um nach
-einem missglückten Release wieder auf die vorherige Fassung zu zeigen.
+An upload automatically sets the new release as current. **Set as current**
+can select a different one at any time — this is also the way to point back
+at the previous version after a botched release.
 
-Wichtig: Das ändert nur, was Instanzen künftig herunterladen. Bereits ausgerollte
-Instanzen bleiben, wo sie sind – der Client kennt keine Rücknahme. Eine Instanz auf
-die ältere Fassung zurückzubringen heißt, sie mit `update --force` erneut ausrollen
-zu lassen; Datenänderungen aus Migrationen macht das nicht rückgängig.
+Important: this only changes what instances download from now on. Already
+deployed instances stay where they are — the client has no rollback. Bringing
+an instance back to an older version means rolling it out again with
+`update --force`; that does not undo data changes made by migrations.
 
-## 4. Ausrollen
+## 4. Deploy
 
-Auf der Instanz:
+On the instance:
 
 ```bash
-php manage-client/bin/manage-client.php check     # Exit 2 = Update verfügbar
-php manage-client/bin/manage-client.php backup    # vorher sichern
+php manage-client/bin/manage-client.php check     # exit 2 = update available
+php manage-client/bin/manage-client.php backup    # back up first
 php manage-client/bin/manage-client.php update
 ```
 
-Oder über die Oberfläche im Adminbereich des Projekts.
+Or via the UI in the project's admin area.
 
-Updates laufen nicht automatisch. Sie überschreiben Dateien im laufenden Betrieb
-und können Migrationen auslösen; das gehört unter Aufsicht.
+Updates don't run automatically. They overwrite files while the application
+is live and can trigger migrations; that belongs under supervision.
 
-## Release löschen
+## Deleting a release
 
-**Löschen** entfernt den Manifest-Eintrag und die ZIP-Datei. War es das aktuelle
-Release, hat der Server danach keines – Instanzen melden dann `Es ist kein gültiges
-Release veröffentlicht`. Vorher ein anderes als aktuell setzen.
+**Delete** removes the manifest entry and the ZIP file. If it was the
+current release, the server then has none — instances report `Es ist kein
+gültiges Release veröffentlicht` ("no valid release is published" — the
+literal, still German, API text). Set another one as current first.
 
-## Versionsnummern
+## Version numbers
 
-Format `vX.Y.Z`, sonst nichts. Client und Server lehnen alles andere ab. Eine
-bereits veröffentlichte Version erneut hochzuladen überschreibt das Paket – bei
-einem fehlerhaften Release ist eine neue Patch-Version die sauberere Wahl, weil
-Instanzen sonst je nach Zeitpunkt Unterschiedliches installiert haben.
+Format `vX.Y.Z`, nothing else. Both client and server reject anything else.
+Re-uploading an already-published version overwrites the package — for a
+broken release, a new patch version is the cleaner choice, because otherwise
+instances end up with different installs depending on when they updated.
 
-## Weiter
+## Next
 
-- [../client-package/docs/06_UPDATE_PACKAGING.md](../client-package/docs/06_UPDATE_PACKAGING.md) – Paketaufbau
-- [../client-package/docs/07_POST_UPDATE_HOOKS.md](../client-package/docs/07_POST_UPDATE_HOOKS.md) – Migrationen im Paket
-- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – Instanzen
+- [../client-package/docs/06_UPDATE_PACKAGING.md](../client-package/docs/06_UPDATE_PACKAGING.md) – package layout
+- [../client-package/docs/07_POST_UPDATE_HOOKS.md](../client-package/docs/07_POST_UPDATE_HOOKS.md) – migrations in the package
+- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – instances

+ 83 - 81
docs/SERVER_SETUP.md

@@ -1,74 +1,74 @@
-# Server einrichten
+# Setting Up the Server
 
-## Überblick
+## Overview
 
-Installation und Betrieb des Manage-Servers. Voraussetzungen: PHP 8.0 oder neuer
-und ein Webserver. Kein Datenbankserver, kein Composer, kein Build-Schritt.
+Installation and operation of the Manage server. Requirements: PHP 8.0 or
+newer and a web server. No database server, no Composer, no build step.
 
 ## Installation
 
-1. Repository in ein Verzeichnis des Webservers legen, zum Beispiel
+1. Put the repository into a directory of the web server, for example
    `/var/www/manage`.
 
-2. Konfiguration anlegen:
+2. Create the configuration:
 
    ```bash
    cp config.sample.php config.php
    ```
 
-3. Passwort-Hash erzeugen und eintragen:
+3. Generate a password hash and enter it:
 
    ```bash
-   php -r 'echo password_hash("ein-langes-passwort", PASSWORD_DEFAULT), PHP_EOL;'
+   php -r 'echo password_hash("a-long-password", PASSWORD_DEFAULT), PHP_EOL;'
    ```
 
    ```php
    define("MANAGE_ADMIN_PASSWORD_HASH", '$2y$12$…');
    ```
 
-   Einfache Anführungszeichen verwenden – ein bcrypt-Hash enthält `$`.
+   Use single quotes — a bcrypt hash contains `$`.
 
-4. Öffentliche URL setzen. Ohne diesen Wert kann kein Client ein Paket
-   herunterladen:
+4. Set the public URL. Without this value, no client can download a
+   package:
 
    ```php
    define("MANAGE_PUBLIC_URL", "https://manage.example.org");
    ```
 
-   Absolut, ohne Schrägstrich am Ende. Liegt die Installation in einem
-   Unterverzeichnis, gehört es dazu: `https://example.org/manage`.
+   Absolute, without a trailing slash. If the installation lives in a
+   subdirectory, that belongs too: `https://example.org/manage`.
 
-5. Produkt benennen. `MANAGE_PACKAGE_PREFIX` bestimmt den Dateinamen der
-   gespeicherten Pakete und sollte zum Build-Skript des Projekts passen:
+5. Name the product. `MANAGE_PACKAGE_PREFIX` determines the filename of
+   stored packages and should match the project's build script:
 
    ```php
    define("MANAGE_PRODUCT_NAME", "Example Orderform");
    define("MANAGE_PACKAGE_PREFIX", "example-orderform");
    ```
 
-6. Schreibrechte auf `storage/` sicherstellen. Das Verzeichnis wird bei Bedarf
-   selbst angelegt:
+6. Make sure `storage/` is writable. The directory is created on demand if
+   needed:
 
    ```bash
    mkdir -p storage && chown www-data:www-data storage && chmod 2775 storage
    ```
 
-7. Oberfläche öffnen: `https://manage.example.org/admin/login.php`.
+7. Open the UI: `https://manage.example.org/admin/login.php`.
 
-Unter **Einstellungen → Diagnose** steht danach, ob alles Wesentliche stimmt:
-öffentliche URL, Schreibrechte, Passwort, Upload-Limits.
+Under **Settings → Diagnostics** you can then see whether everything
+essential checks out: public URL, write access, password, upload limits.
 
-## Webserver
+## Web server
 
 ### Apache
 
-Die mitgelieferte `.htaccess` sperrt `storage/`, `includes/`, `client-package/`
-sowie `config.php` und setzt Sicherheits-Header. Sie funktioniert nur, wenn
-`AllowOverride All` für das Verzeichnis gesetzt ist.
+The bundled `.htaccess` locks `storage/`, `includes/`, `client-package/` and
+`config.php`, and sets security headers. It only works when
+`AllowOverride All` is set for the directory.
 
 ### nginx
 
-Für nginx greift keine `.htaccess`. Die Sperren müssen von Hand gesetzt werden:
+`.htaccess` has no effect under nginx. The locks must be set by hand:
 
 ```nginx
 location ^~ /storage/       { deny all; return 404; }
@@ -78,19 +78,20 @@ location = /config.php      { deny all; return 404; }
 location ~ /\.              { deny all; return 404; }
 ```
 
-Prüfen, dass die Sperren greifen:
+Verify the locks are effective:
 
 ```bash
 curl -s -o /dev/null -w "%{http_code}\n" https://manage.example.org/config.php
 curl -s -o /dev/null -w "%{http_code}\n" https://manage.example.org/storage/instances.json
 ```
 
-Beides muss `403` oder `404` liefern.
+Both must return `403` or `404`.
 
-## Upload-Limits
+## Upload limits
 
-Backups und Release-Pakete werden per HTTP hochgeladen. Beide PHP-Grenzen müssen
-groß genug sein, `post_max_size` mindestens so groß wie `upload_max_filesize`:
+Backups and release packages are uploaded over HTTP. Both PHP limits need to
+be large enough, with `post_max_size` at least as large as
+`upload_max_filesize`:
 
 ```ini
 upload_max_filesize = 256M
@@ -99,83 +100,84 @@ max_execution_time = 300
 memory_limit = 256M
 ```
 
-Die aktuellen Werte zeigt die Diagnose-Seite. Ist der Wert zu klein, meldet der
-Client eine Fehlermeldung, die die Ursache ausdrücklich benennt.
+The current values are shown on the diagnostics page. If a value is too
+small, the client reports an error message that names the cause explicitly.
 
-`memory_limit` wird relevant, wenn das S3-Archiv aktiv ist: Ein Upload zu S3 hält
-die Datei vollständig im Speicher.
+`memory_limit` becomes relevant when the S3 archive is active: an upload to
+S3 holds the file in memory in full.
 
-## Aufbewahrung
+## Retention
 
-Unter **Einstellungen** einstellbar, gespeichert in `storage/settings.json`. Die
-Werte dort haben Vorrang vor den Konstanten in `config.php`.
+Configurable under **Settings**, stored in `storage/settings.json`. The
+values there take precedence over the constants in `config.php`.
 
-- **Lokale Backups pro Instanz** – Standard 30, Minimum 1
-- **S3-Backups pro Instanz** – Standard 365, nur bei aktivem S3-Archiv
+- **Local backups per instance** – default 30, minimum 1
+- **S3 backups per instance** – default 365, only with the S3 archive active
 
-Änderungen werden sofort angewendet, nicht erst beim nächsten Upload.
+Changes apply immediately, not only on the next upload.
 
-## S3-Archiv (optional)
+## S3 archive (optional)
 
-Ohne S3 liegen alle Backups auf der lokalen Platte. Mit S3 wird jedes empfangene
-Backup zusätzlich in ein S3-kompatibles Objektspeicher-System geschoben; lokal
-bleiben nur die neuesten Kopien.
+Without S3, all backups sit on the local disk. With S3, every received
+backup is additionally pushed to an S3-compatible object storage system;
+locally, only the newest copies remain.
 
 ```php
 define("MANAGE_S3_ENABLED", true);
 define("MANAGE_S3_ENDPOINT", "https://fsn1.your-objectstorage.com");
 define("MANAGE_S3_REGION", "fsn1");
-define("MANAGE_S3_BUCKET", "mein-backup-bucket");
+define("MANAGE_S3_BUCKET", "my-backup-bucket");
 define("MANAGE_S3_PREFIX", "manage-backups");
 define("MANAGE_S3_ACCESS_KEY", "…");
 define("MANAGE_S3_SECRET_KEY", "…");
 ```
 
-Objekte liegen unter `<prefix>/<instanz>/<dateiname>`.
+Objects live under `<prefix>/<instance>/<filename>`.
 
-Adressierung: Standard ist virtual-hosted (`https://<bucket>.<endpoint>/<key>`),
-was Hetzner und die meisten Anbieter erwarten. Verlangt der Anbieter path-style,
-`MANAGE_S3_PATH_STYLE` auf `true` setzen.
+Addressing: the default is virtual-hosted
+(`https://<bucket>.<endpoint>/<key>`), which Hetzner and most providers
+expect. If the provider requires path-style, set `MANAGE_S3_PATH_STYLE` to
+`true`.
 
-Verhalten:
+Behavior:
 
-- S3-Fehler lassen einen Client-Upload nie fehlschlagen.
-- Eine lokale Kopie wird erst gelöscht, wenn sie aus der lokalen Aufbewahrung
-  gefallen **und** die S3-Kopie bestätigt ist.
-- Fehlgeschlagene Uploads werden beim nächsten Upload derselben Instanz oder über
-  "S3-Uploads jetzt nachholen" wiederholt.
-- Der Bucket kann und soll privat bleiben: Downloads laufen über die Oberfläche.
-- Beim Aktivieren auf einer bestehenden Installation einmal "S3-Uploads jetzt
-  nachholen" drücken, damit das Archiv aufgeholt wird.
+- S3 errors never fail a client upload.
+- A local copy is only deleted once it has fallen out of local retention
+  **and** the S3 copy is confirmed.
+- Failed uploads are retried on the next upload from the same instance, or
+  via "Catch up S3 uploads now".
+- The bucket can and should stay private: downloads go through the UI.
+- When enabling this on an existing installation, press "Catch up S3
+  uploads now" once so the archive catches up.
 
-Fehlersuche über `storage/logs/s3.log` und den Auszug unter **Einstellungen**:
-`AccessDenied` oder eine Umleitung in der Statuskette deuten fast immer auf die
-falsche Adressierungsart hin, `SignatureDoesNotMatch` auf falsche Region oder
-falschen Secret Key. Umleitungen werden bewusst nicht verfolgt, damit eine
-Fehlkonfiguration sichtbar wird.
+Troubleshooting via `storage/logs/s3.log` and the excerpt under
+**Settings**: `AccessDenied` or a redirect in the status chain almost always
+points to the wrong addressing style; `SignatureDoesNotMatch` to a wrong
+region or secret key. Redirects are deliberately not followed, so a
+misconfiguration stays visible.
 
-## Sicherung des Servers
+## Backing up the server
 
-Der Manage-Server hält Release-Pakete und die Backups aller Instanzen – er ist
-selbst sicherungswürdig. Zu sichern sind:
+The Manage server holds release packages and every instance's backups — it
+is itself worth backing up. To back up:
 
-- `storage/` – Instanzen, Manifest, Pakete, Backups, Einstellungen
-- `config.php` – Zugangsdaten
+- `storage/` – instances, manifest, packages, backups, settings
+- `config.php` – credentials
 
-Bei aktivem S3-Archiv liegen die Backups zusätzlich im Bucket; `storage/instances.json`
-und `storage/releases/` aber nicht.
+With the S3 archive active, backups additionally live in the bucket; but
+`storage/instances.json` and `storage/releases/` do not.
 
-## Betrieb
+## Operation
 
-- Die Anmeldung ist pro IP begrenzt (Standard 10 Versuche je 15 Minuten).
-- Ratenbegrenzung gilt auch für fehlgeschlagene API-Authentifizierungen.
-- Protokolle liegen unter `storage/logs/` und rotieren automatisch.
-- Ein Passwortwechsel erfolgt in `config.php`; angemeldete Sitzungen bleiben bis
-  zum Ablauf bestehen. Sollen sie sofort enden, `session.save_path` leeren oder
-  `session_name` in `includes/auth.php` ändern.
+- Login is rate-limited per IP (default 10 attempts per 15 minutes).
+- Rate limiting also applies to failed API authentications.
+- Logs live under `storage/logs/` and rotate automatically.
+- A password change happens in `config.php`; logged-in sessions remain
+  valid until they expire. To end them immediately, clear
+  `session.save_path` or change `session_name` in `includes/auth.php`.
 
-## Weiter
+## Next
 
-- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – Instanzen anlegen und Tokens vergeben
-- [RELEASING](RELEASING.md) – Releases bauen und veröffentlichen
-- [CONFIG_REFERENCE](CONFIG_REFERENCE.md) – alle Serverkonstanten
+- [INSTANCE_MANAGEMENT](INSTANCE_MANAGEMENT.md) – creating instances and issuing tokens
+- [RELEASING](RELEASING.md) – building and publishing releases
+- [CONFIG_REFERENCE](CONFIG_REFERENCE.md) – every server constant

+ 10 - 10
docs/index.php

@@ -41,18 +41,18 @@ $docsTitle = basename(dirname($docsDir)) === "client-package"
 $requested = isset($_GET["doc"]) ? (string) $_GET["doc"] : "";
 $activeDoc = null;
 $markdown = null;
-$pageTitle = "Dokumentation";
+$pageTitle = "Documentation";
 
 if ($requested !== "") {
     if (!isset($docMap[$requested])) {
         http_response_code(404);
-        $pageTitle = "Nicht gefunden";
+        $pageTitle = "Not Found";
     } else {
         $activeDoc = $requested;
         $markdown = file_get_contents($docMap[$requested]);
         if ($markdown === false) {
             http_response_code(500);
-            $pageTitle = "Fehler";
+            $pageTitle = "Error";
             $markdown = null;
         } else {
             $pageTitle = docTitle($activeDoc);
@@ -68,7 +68,7 @@ if ($baseHref === "") {
 }
 ?>
 <!DOCTYPE html>
-<html lang="de">
+<html lang="en">
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -82,8 +82,8 @@ if ($baseHref === "") {
     </div>
 </header>
 <div class="docs-layout">
-    <nav class="docs-nav" aria-label="Dokumentation">
-        <p class="docs-nav-title">Inhalt</p>
+    <nav class="docs-nav" aria-label="Documentation">
+        <p class="docs-nav-title">Contents</p>
         <ul>
             <?php foreach ($docMap as $key => $_path): ?>
                 <li>
@@ -97,8 +97,8 @@ if ($baseHref === "") {
     </nav>
     <main class="docs-main">
         <?php if ($requested === ""): ?>
-            <h1>Dokumentation</h1>
-            <p>Alle Dokumente in empfohlener Lesereihenfolge.</p>
+            <h1>Documentation</h1>
+            <p>All documents in the recommended reading order.</p>
             <ul class="docs-index-list">
                 <?php foreach ($docMap as $key => $_path): ?>
                     <li>
@@ -110,8 +110,8 @@ if ($baseHref === "") {
             </ul>
         <?php elseif ($markdown === null): ?>
             <h1><?php echo docEscape($pageTitle); ?></h1>
-            <p>Das angeforderte Dokument ist nicht verfügbar.</p>
-            <p><a href="<?php echo docEscape($baseHref); ?>index.php">Zur Übersicht</a></p>
+            <p>The requested document is not available.</p>
+            <p><a href="<?php echo docEscape($baseHref); ?>index.php">Back to the overview</a></p>
         <?php else: ?>
             <article id="doc-content" class="markdown-body"></article>
             <script type="application/json" id="doc-source"><?php