# Backup and Update The shop talks to a central **Manage server** (`MANAGE_SERVER_URL`) for three things: it uploads backup archives there, it fetches release packages from there, and it reports its status there. Everything is triggered from **Admin → Einstellungen** (`admin/settings.php`); this host has no cron. ## Files | File | Content | |---|---| | `includes/manage.php` | entry point: config defaults, HTTP transport, logging, version, `manageClientStatus()` | | `includes/manage-zip.php` | pure-PHP ZIP writer used for backups | | `includes/manage-backup.php` | collecting sources, writing archives, retention, upload | | `includes/manage-update.php` | manifest, download, verification, deployment, migrations | | `includes/manage-heartbeat.php` | status report to the Manage server | | `includes/version.php` | the installed version, `APP_VERSION` | | `includes/after-update.php` | `shopAfterUpdate()`, the post-update callback | | `migrations/` | migration scripts shipped inside a release, see `migrations/README.md` | | `scripts/create-release-zip.sh` | builds a release package | | `admin/settings.php` | the UI for all of it, plus admin account management | Runtime state lives under `data/manage/` and is gitignored: `backups/` (local archives + `backup-index.json`), `updates/` (files the last update overwrote), `work/` (update staging, cleared after each run), `manage-client.log` (JSONL), `migrations.json`, `heartbeat.json`. `config.php` carries every `MANAGE_*` constant. See `docs/CONFIG_REFERENCE.md`; defaults live in `includes/manage.php`, so `config.php` only has to set what differs. ## Backup An archive contains `data/*.json` and `assets/images/` — the operational data and the product images uploaded through the admin UI, which exist nowhere else. `config.php` is deliberately **not** in it: archives can be downloaded by anyone with a Manage server login, and that file holds the legacy `ADMIN_USERS` hashes, the order-history cookie secret and the instance token. Three ways in: - **Button** on the settings page — trigger `manual`. - **Automatically** from `admin/index.php` once `MANAGE_BACKUP_AUTO_INTERVAL_SECONDS` (7 days) has passed since the last automatic run — trigger `automatic`. The call returns immediately when nothing is due, so the dashboard is only slow on the rare load that actually backs up. - **Directly**, `manageBackupCreate('manual')` after `require_once includes/manage.php`. Each archive is written to `data/manage/backups/`, then uploaded. The last `MANAGE_BACKUP_LOCAL_RETENTION` (4) archives stay on disk; retention on the Manage server is configured there and is usually much higher. **A failed upload does not invalidate the archive.** The error is stored in the index record, shown in the *Upload* column and logged; the local ZIP is complete either way. A failure that does throw means the archive never came into being. Concurrent runs are prevented by a lock file — a second one fails immediately with *Es läuft bereits ein Backup.* ### There is no restore Backups are created and transferred, never played back. Restoring means downloading the ZIP from the settings page or the Manage server and unpacking it over `data/` and `assets/images/` by hand. This is deliberate: an automated restore button that runs while the shop is live is a footgun. ## Update The settings page shows the installed version against the current release. The *Update ausrollen* button then: 1. fetches and validates the manifest, 2. downloads the package and checks **size and SHA-256** against it, deleting the file on mismatch, 3. extracts it, rejecting any entry with `..`, an absolute path, a drive letter or a null byte, 4. checks the package contains at least one `MANAGE_UPDATE_SANITY_PATHS` entry, so an unrelated ZIP cannot be rolled over the shop, 5. copies every file over the shop root, **copying each overwritten file aside** into `data/manage/updates/` first, skipping `MANAGE_UPDATE_PROTECTED_PATHS`, 6. runs pending migrations, then `shopAfterUpdate()`. Take a backup first. The button does not do it for you — that stays a visible, deliberate act. ### Limits, all deliberate - **No rollback.** The aside copies in `data/manage/updates/` are for manual recovery, and only the most recent run is kept. - **Deleted files are not removed.** Deployment is an overlay; a file no longer in the new release stays behind. Removing it belongs in a migration. - **No maintenance mode.** The shop stays reachable while files are copied. Roll out during a quiet period. - **Files can be live while the post-update step failed.** The settings page reports the two separately. A failed migration leaves the rest pending; fix the cause and press *Migrationen ausführen*. ### Protected paths `MANAGE_UPDATE_PROTECTED_PATHS` is `config.php`, `data/`, `.git/`. Note that `assets/images/` is **not** protected, and does not need to be: the updater only touches paths contained in the package, so uploaded images survive on their own, while a release can still ship or update its own images. ## Cutting a release ```bash ./scripts/create-release-zip.sh v1.1.0 ``` The script writes the version into `includes/version.php`, packs every **git-tracked** file minus `config.php`, `data/`, `build/`, `scripts/` and `.codex/`, and prints size and SHA-256. Commit the version bump, then upload the ZIP in the Manage server under *Releases*. Two things to get right: - `PRODUCT` at the top of the script must match `MANAGE_PACKAGE_PREFIX` on the Manage server. - The version format is strictly `vX.Y.Z`. Forgetting to bump it means the instance keeps reporting the old version and keeps being offered the same update. Only committed content is packaged — the script warns on a dirty tree rather than refusing, because building from one is occasionally deliberate. ## Heartbeat Reports version, PHP version, free disk space, pending migrations and the time of the last backup. Without cron it rides along with page loads: at most once an hour on the settings page (`manageHeartbeatSendIfDue()`), and immediately after a backup or update, when those values have just changed. *Status melden* forces one. It never breaks a page render — `manageHeartbeatSendQuietly()` swallows every error into the log. ## Security notes - The token is the only secret between shop and server. It lives in `config.php`, which is gitignored and excluded from release packages. Rotate it on the Manage server if it leaks. - `data/manage/` must not be web-readable. The root `.htaccess` denies all of `data/`; verify with `curl -o /dev/null -w "%{http_code}\n" https:///shop/data/manage/backups/` — anything but `403`/`404` is a problem. On nginx this needs a `location` rule instead. - Backup downloads validate the filename against `backup-\d{8}-\d{6}(-\d+)?\.zip`, so the form cannot be made to serve another path. - Every form on the settings page carries a CSRF token, and the page requires an admin session before anything else runs. - Transport is HTTPS with PHP's normal certificate verification, and redirects are not followed — a redirected request fails rather than sending the token somewhere else. - The checksum comes from the same server as the package. Whoever controls the Manage server can publish a package **and** its checksum; there is no signature layer. Secure the Manage server accordingly. ## Troubleshooting | Message | Cause | |---|---| | *Der Manage-Client ist nicht konfiguriert.* | `MANAGE_SERVER_URL`, `MANAGE_INSTANCE` or `MANAGE_TOKEN` empty in `config.php` | | *Es ist kein gültiges Release veröffentlicht.* | no release published on the Manage server yet | | *Authentifizierung fehlgeschlagen.* | wrong or rotated token; instance id typo | | *Die Prüfsumme des Pakets stimmt nicht überein.* | corrupted download; nothing was deployed | | *Das Paket sieht nicht wie ein Release dieses Shops aus* | packaged with a wrapping directory, or the wrong ZIP | | *Die Datei konnte nicht ausgerollt werden: …* | PHP lacks write access in the shop root; the update stopped partway | | *Es läuft bereits ein Backup.* | a second run while one is in progress | `data/manage/manage-client.log` holds one JSON object per line for every backup, update, migration and failure.