From c678b7549471e3e0cbca3a80d9393b575001ef98 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 24 Mar 2026 15:41:30 +0100 Subject: [PATCH] Add admin server status page New page /admin/status.php gives a real-time health dashboard: - Services panel: nginx (systemctl), php-fpm (auto-detects versioned unit names), site HTTP ping (curl HEAD with latency), SQLite DB (exists/writable/row count/size), storage directory (writable, banner/cover file counts), maintenance-mode flag. - PHP runtime panel: version, SAPI, memory_limit, upload_max_filesize, post_max_size, max_execution_time. - Disk usage bar for the partition containing APP_ROOT (colour-coded: green/amber/red). - All shell calls go through safeExec() which suppresses stderr and checks exit code; systemctl/curl unavailability degrades gracefully to 'unknown' without fatal errors. - 'Statut' nav link added to templates/admin/head.php (active state on status.php). --- TODO.md | 2 +- public/admin/status.php | 349 +++++++++++++++++++++++++++++++++++++++ templates/admin/head.php | 1 + 3 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 public/admin/status.php diff --git a/TODO.md b/TODO.md index 1322383..21d7893 100644 --- a/TODO.md +++ b/TODO.md @@ -330,7 +330,7 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis - [x] Add `just setup-server` recipe (rsync + run setup-server.sh on remote) - [x] Exclude `.claude` and `.pi` from rsync deploy - [x] Update `docs/SERVER_SETUP.md` with correct permissions rationale and troubleshooting -- [ ] Add server status view in admin panel (nginx + php-fpm health, site HTTP check) +- [x] Add server status view in admin panel (nginx + php-fpm health, site HTTP check) - [ ] Add server log viewer in admin panel (tail nginx error/access logs via SSH or log endpoint) - [ ] Add nginx config deploy flow to admin panel (upload `scripts/deploy-server.sh`, run remotely) - [ ] Add admin user management UI (wraps `scripts/manage-admin-users.sh` on server) diff --git a/public/admin/status.php b/public/admin/status.php new file mode 100644 index 0000000..cfe1c46 --- /dev/null +++ b/public/admin/status.php @@ -0,0 +1,349 @@ +/dev/null', $output, $rc); + return $rc === 0 ? trim(implode("\n", $output)) : null; +} + +/** + * Check whether a systemd unit is active. + * Returns 'active', 'inactive', 'failed', or null when unavailable. + */ +function systemdStatus(string $unit): ?string { + $raw = safeExec("systemctl is-active " . escapeshellarg($unit)); + if ($raw === null) return null; + return in_array($raw, ['active', 'inactive', 'failed', 'activating', 'deactivating'], true) + ? $raw : 'unknown'; +} + +/** + * Perform a local HTTP HEAD/GET and return [http_code, latency_ms] or null. + */ +function localHttpCheck(string $url): ?array { + if (!function_exists('curl_init')) return null; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_NOBODY => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + ]); + $start = microtime(true); + curl_exec($ch); + $ms = (int) round((microtime(true) - $start) * 1000); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return $code > 0 ? [$code, $ms] : null; +} + +// ── data collection ─────────────────────────────────────────────────────────── + +$checks = []; + +// 1. nginx +$nginxStatus = systemdStatus('nginx'); +$nginxVersion = safeExec('nginx -v 2>&1 | head -1'); +$checks['nginx'] = [ + 'label' => 'nginx', + 'status' => $nginxStatus, + 'detail' => $nginxVersion, +]; + +// 2. php-fpm (try versioned names: php8.2-fpm, php8.3-fpm, php-fpm) +$phpFpmStatus = null; +$phpFpmUnit = null; +foreach (['php8.3-fpm', 'php8.2-fpm', 'php8.1-fpm', 'php-fpm'] as $unit) { + $s = systemdStatus($unit); + if ($s !== null && $s !== 'unknown') { + $phpFpmStatus = $s; + $phpFpmUnit = $unit; + break; + } +} +$checks['php_fpm'] = [ + 'label' => 'php-fpm' . ($phpFpmUnit ? " ($phpFpmUnit)" : ''), + 'status' => $phpFpmStatus, + 'detail' => null, +]; + +// 3. Site HTTP ping +$siteUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/'; +$httpResult = localHttpCheck($siteUrl); +$checks['site_http'] = [ + 'label' => 'Site HTTP', + 'status' => $httpResult !== null ? ($httpResult[0] < 500 ? 'active' : 'failed') : null, + 'detail' => $httpResult !== null ? "HTTP {$httpResult[0]} — {$httpResult[1]} ms" : 'curl indisponible', +]; + +// 4. Database file +require_once APP_ROOT . '/src/Database.php'; +$dbPath = APP_ROOT . '/storage/test.db'; +$dbExists = file_exists($dbPath); +$dbWritable = $dbExists && is_writable($dbPath); +$dbSizeBytes = $dbExists ? filesize($dbPath) : null; +$dbSizeHuman = $dbSizeBytes !== null + ? ($dbSizeBytes > 1048576 + ? number_format($dbSizeBytes / 1048576, 1) . ' MB' + : number_format($dbSizeBytes / 1024, 1) . ' KB') + : 'N/A'; + +// Quick DB sanity: count rows +$dbRowCount = null; +if ($dbExists) { + try { + $db = new Database(); + $stmt = $db->getConnection()->query("SELECT COUNT(*) FROM theses"); + $dbRowCount = (int) $stmt->fetchColumn(); + } catch (Throwable $e) { + $dbRowCount = null; + } +} + +$checks['database'] = [ + 'label' => 'Base de données SQLite', + 'status' => $dbExists ? ($dbWritable ? 'active' : 'inactive') : 'failed', + 'detail' => $dbExists + ? ($dbRowCount !== null ? "$dbRowCount thèses — $dbSizeHuman" : "Lecture impossible — $dbSizeHuman") + : 'Fichier introuvable', +]; + +// 5. Storage directory +$storageDir = APP_ROOT . '/storage'; +$storageWritable = is_dir($storageDir) && is_writable($storageDir); +$bannersDir = $storageDir . '/banners'; +$coversDir = $storageDir . '/covers'; +$checks['storage'] = [ + 'label' => 'Répertoire storage', + 'status' => $storageWritable ? 'active' : ($storageDir ? 'inactive' : 'failed'), + 'detail' => $storageWritable + ? implode(' · ', array_filter([ + is_dir($bannersDir) ? ('banners/ ' . count(array_diff(scandir($bannersDir), ['.','..'])) . ' fichiers') : null, + is_dir($coversDir) ? ('covers/ ' . count(array_diff(scandir($coversDir), ['.','..'])) . ' fichiers') : null, + ])) + : 'Non accessible en écriture', +]; + +// 6. Maintenance mode +$maintenanceOn = file_exists(APP_ROOT . '/storage/maintenance.flag'); +$checks['maintenance'] = [ + 'label' => 'Mode maintenance', + 'status' => $maintenanceOn ? 'warn' : 'active', + 'detail' => $maintenanceOn ? 'Activé — site public inaccessible' : 'Désactivé', +]; + +// 7. PHP info +$phpInfo = [ + 'version' => PHP_VERSION, + 'sapi' => PHP_SAPI, + 'memory_limit' => ini_get('memory_limit'), + 'upload_max' => ini_get('upload_max_filesize'), + 'post_max' => ini_get('post_max_size'), + 'max_exec' => ini_get('max_execution_time') . 's', +]; + +// 8. Disk usage (partition containing APP_ROOT) +$diskTotal = disk_total_space(APP_ROOT); +$diskFree = disk_free_space(APP_ROOT); +$diskUsed = $diskTotal - $diskFree; +function humanBytes(int $bytes): string { + if ($bytes > 1073741824) return number_format($bytes / 1073741824, 1) . ' GB'; + if ($bytes > 1048576) return number_format($bytes / 1048576, 1) . ' MB'; + return number_format($bytes / 1024, 1) . ' KB'; +} +$diskPct = $diskTotal > 0 ? (int) round($diskUsed / $diskTotal * 100) : 0; + +// ── label helpers ───────────────────────────────────────────────────────────── + +function statusLabel(string $status): string { + return match($status) { + 'active' => '● En ligne', + 'inactive' => '○ Inactif', + 'failed' => '✕ Erreur', + 'warn' => '⚠ Attention', + default => '? Inconnu', + }; +} +function statusClass(string $status): string { + return match($status) { + 'active' => 'status-ok', + 'inactive' => 'status-warn', + 'warn' => 'status-warn', + 'failed' => 'status-err', + default => 'status-unknown', + }; +} + +require_once APP_ROOT . '/templates/admin/head.php'; +?> + + + +
+

Statut serveur

+ +

+ Vérification effectuée le — + Rafraîchir +

+ + +

Services

+
+ + +
+
+ + +
+ +
+ +
+ +
+ + +

Environnement PHP

+
+ $val): ?> +
+
+
+
+ +
+ + +

Espace disque

+
+
+
+ utilisé (%) + libre / +
+
+ +
+ + diff --git a/templates/admin/head.php b/templates/admin/head.php index d796e44..8e0fd6c 100644 --- a/templates/admin/head.php +++ b/templates/admin/head.php @@ -29,6 +29,7 @@ Importer une liste de TFE Pages statiques Mots-clés + Statut Modifier