Maintenance

' . 'The backup and update client is not installed: manage-client/ ' . 'is missing. See docs/SETUP.md, section 5a.

'; admin_footer(); exit; } require_once APP_ROOT . '/manage-client/lib/client.php'; $messages = []; $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); // Archiving media/ and uploading it runs well past the default limit. @set_time_limit(0); $action = (string)($_POST['action'] ?? ''); try { if ($action === 'download') { // Validates the filename itself and throws on anything that is not // a backup of this installation. $path = manageBackupPath((string)($_POST['filename'] ?? '')); $size = filesize($path); $fh = fopen($path, 'rb'); if ($size === false || $fh === false) { throw new RuntimeException('Backup file could not be opened.'); } header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . addcslashes(basename($path), '"\\') . '"'); header('Content-Length: ' . $size); header('Cache-Control: private, no-store'); header('X-Content-Type-Options: nosniff'); fpassthru($fh); fclose($fh); exit; } if ($action === 'backup') { $messages = array_merge($messages, maintenance_backup('manual')); manageHeartbeatSendQuietly(); } elseif ($action === 'update') { // Deliberately a line here rather than a feature of the client: it // stays visible that the update takes a backup first, and the // update stops if that backup cannot be written. if (!empty($_POST['backup_first'])) { $messages = array_merge($messages, maintenance_backup('update')); } $result = manageUpdateApply(['force' => !empty($_POST['force'])]); $messages[] = sprintf( 'Update deployed: %s → %s. %d files copied, %d replaced files saved to %s.', $result['from_version'] !== '' ? $result['from_version'] : 'unknown', $result['to_version'], $result['copied'], $result['backed_up'], $result['backup_dir'], ); // Files are deployed even when the hook failed; that difference is // the whole point of reporting it separately. $hook = is_array($result['hook'] ?? null) ? $result['hook'] : []; $applied = $hook['migrations']['applied'] ?? []; if ($applied !== []) { $messages[] = 'Migrations run: ' . implode(', ', $applied); } if ($hook !== [] && empty($hook['success'])) { $errors[] = empty($hook['failed_migration']) ? 'The files are deployed, but the post-update step failed: ' . (string)($hook['error'] ?? 'unknown') : 'The files are deployed, but migration "' . (string)$hook['failed_migration'] . '" failed: ' . (string)($hook['error'] ?? 'unknown') . ' Remaining migrations were not attempted — fix the cause, then use "Run migrations" below.'; } manageHeartbeatSendQuietly(); } elseif ($action === 'migrate') { $report = manageUpdateRunMigrations(); if ($report['applied'] !== []) { $messages[] = 'Migrations run: ' . implode(', ', $report['applied']); } if (!$report['success']) { $errors[] = 'Migration "' . (string)$report['failed'] . '" failed: ' . (string)$report['error']; } elseif ($report['applied'] === []) { $messages[] = 'No pending migrations.'; } } elseif ($action === 'heartbeat') { $result = manageHeartbeatSend(); $messages[] = 'Status reported. Current release: ' . (($result['latest'] ?? '') !== '' ? (string)$result['latest'] : 'none') . '.'; } } catch (Throwable $e) { $errors[] = $e->getMessage(); } } /** * One backup, reported as message lines. A failed upload is a warning, not a * failure: the archive exists locally either way. */ function maintenance_backup(string $trigger): array { $record = manageBackupCreate($trigger); $lines = [sprintf( 'Backup created: %s — %d files, %s.', $record['filename'], $record['file_count'], manageFormatBytes((int)$record['size']), )]; foreach ($record['remote_uploads'] as $upload) { if (empty($upload['success'])) { $lines[] = 'Upload to ' . (string)$upload['target'] . ' failed: ' . (string)($upload['error'] ?? 'unknown') . ' The local copy is intact.'; } } return $lines; } // Never throws: an unreachable manage server still renders the page. $status = manageClientStatus(); $update = $status['update']; $capabilities = manageRemoteCapabilities(); admin_header('Maintenance', 'maintenance'); flash_render(); ?>

Maintenance

Not connected to the manage server. Create an instance there, then fill in MANAGE_INSTANCE and MANAGE_TOKEN in manage-client/config.php. Backups can still be made locally.
Installed version PHP
Current release available — Check failed: Back up first, then deploy. Up to date.
Last backup kept locally
Release migrations pending

Backup

Archives data/ and media/ — galleries, showreel, front page, settings — and uploads it to the manage server. Gallery photos are not included: they live in the S3 bucket, which is their own backup. Nor are config/ credentials, because a backup can be downloaded again from the server.

Update

Version is ready (published ) .

Files are replaced while the site stays online, and there is no rollback: the replaced files are copied to data/manage/updates/ for manual recovery. config/, data/ and media/ are never touched. Deleted files are not removed — an update overlays what is there. Afterwards, check Data migration.

Pending release migrations

Shipped with a release and normally run by the update itself. These are left over — usually because one failed, or because the update ran with migrations skipped.

Local backups

No backup has been made yet.

FileCreatedTrigger FilesSizeUpload
—

Kept locally: . Older ones are deleted here after each new backup; the manage server keeps its own, longer history.

$capability) { if ($capability['configured'] && !$capability['available']) { $unsupported[] = $type; } } ?>
Configured backup targets this server cannot use: . Those uploads will fail.

Report status

Sends version, PHP version, free disk space and the time of the last backup to the manage server. Normally an hourly cron job; this is the manual version of it.

Same operations from the shell: php manage-client/bin/manage-client.php status|check|backup|update|migrate|heartbeat. See docs/SETUP.md.