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; } /** An interval as something readable: 604800 -> "every 7 days". */ function maintenance_interval_text(int $seconds): string { if ($seconds % 86400 === 0) { $days = intdiv($seconds, 86400); return $days === 1 ? 'daily' : 'every ' . $days . ' days'; } if ($seconds % 3600 === 0) { $hours = intdiv($seconds, 3600); return $hours === 1 ? 'hourly' : 'every ' . $hours . ' hours'; } return 'every ' . max(1, intdiv($seconds, 60)) . ' minutes'; } // The one place that asks the manage server whether a release is waiting: it is // a network round trip, so it happens when this page is opened and nowhere else. // Never throws — an unreachable server still renders the page. $status = manageClientStatus(); // State of the schedule in app/manage.php. $autoInterval = (int)MANAGE_BACKUP_AUTO_INTERVAL_SECONDS; $due = manage_due(); $lastAutoBackup = 0; foreach ($status['backups'] as $backup) { if (in_array($backup['trigger'] ?? '', ['automatic', 'cron'], true)) { $lastAutoBackup = max($lastAutoBackup, strtotime((string)($backup['created_at'] ?? '')) ?: 0); } } $lastHeartbeat = (int)(json_read(manage_state_file())['heartbeat_at'] ?? 0); // State of the optional cronjob in app/cron.php. Its schedule lives in the // crontab, so the only thing worth showing is whether it is actually running. $cron = cron_state(); $cronNote = null; if (!empty($cron['last_run_at'])) { $cronNote = 'Last ' . date('Y-m-d H:i', (int)$cron['last_run_at']) . ', ' . count($cron['jobs'] ?? []) . ' job(s) in ' . (float)($cron['duration'] ?? 0) . 's'; if ((int)($cron['remaining'] ?? 0) > 0) { $cronNote .= ', ' . (int)$cron['remaining'] . ' gallery(s) still queued'; } } // An instance whose server has no release yet answers the manifest with 404. // That is a normal state — a new project, nothing published — and reads far // too much like a broken connection when it is shown as a failed check. $noReleaseYet = $status['update_error'] !== null && str_contains($status['update_error'], 'HTTP 404'); $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 — No release has been published on the manage server yet. 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.

Schedule

This host is assumed to have no cron, so the backoffice drives both jobs: opening any admin page past the interval starts them in the background (manage-worker.php). Updates are never part of that, and the release check runs only when this page is opened.

Automatic backup 0 ? e(maintenance_interval_text($autoInterval)) : 'off' ?> MANAGE_BACKUP_AUTO_INTERVAL_SECONDS is 0 — only cron or the button above make backups. Due now — starts on the next admin page load. Next .
Heartbeat Not sent yet. Last .
Cronjob Optional. The jobs above and the gallery archives run without it. Enabled, but cron.php has not run yet — check the crontab line. .

If the host does offer cron, one line is enough: cron.php?key=… every five minutes, or the same file from the shell. It drains every due gallery archive and whatever the schedule owes, and the web fallback above simply finds nothing left. The key is in data/worker-key.json; set cron.enabled in config/config.php and see scripts/manage-client.cron.

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.