From 020bfa5a336252cce9405c53ad7a5fed1682e9d4 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Tue, 24 Mar 2026 15:47:08 +0100 Subject: [PATCH] admin: add server log viewer; fix curl_close() PHP 8.5 deprecation in status.php - public/admin/logs.php: new page tailing nginx error/access + PHP-FPM logs. Selector for log file and line count (50/100/200/500, default 100). Lines reversed (newest first), colour-coded by severity, numbered gutter. Graceful degradation when exec() unavailable or file unreadable (dev msg). - templates/admin/head.php: 'Journaux' nav link added after 'Statut'. - public/admin/status.php: remove curl_close() call deprecated in PHP 8.5 (no-op since PHP 8.0); replace with unset($ch) to silence the warning that was leaking raw text above the page output. --- TODO.md | 2 +- public/admin/logs.php | 279 +++++++++++++++++++++++++++++++++++++++ public/admin/status.php | 3 +- templates/admin/head.php | 1 + 4 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 public/admin/logs.php diff --git a/TODO.md b/TODO.md index 21d7893..c5a8cf5 100644 --- a/TODO.md +++ b/TODO.md @@ -331,6 +331,6 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis - [x] Exclude `.claude` and `.pi` from rsync deploy - [x] Update `docs/SERVER_SETUP.md` with correct permissions rationale and troubleshooting - [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) +- [x] 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/logs.php b/public/admin/logs.php new file mode 100644 index 0000000..3ef0859 --- /dev/null +++ b/public/admin/logs.php @@ -0,0 +1,279 @@ + ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/posterg_error.log'], + 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/posterg_access.log'], + 'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'], +]; + +const ALLOWED_LINES = [50, 100, 200, 500]; + +// ── Input validation ────────────────────────────────────────────────────────── +$selectedKey = $_GET['log'] ?? 'nginx_error'; +$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100; + +if (!array_key_exists($selectedKey, LOG_FILES)) { + $selectedKey = 'nginx_error'; +} +if (!in_array($selectedN, ALLOWED_LINES, true)) { + $selectedN = 100; +} + +$logDef = LOG_FILES[$selectedKey]; +$logPath = $logDef['path']; + +// ── Read log ────────────────────────────────────────────────────────────────── +$lines = null; // null = unavailable, [] = empty, [...] = lines +$readError = null; + +if (!function_exists('exec')) { + $readError = "exec() est désactivé sur ce serveur."; +} elseif (!file_exists($logPath)) { + $readError = "Fichier introuvable : " . htmlspecialchars($logPath); +} elseif (!is_readable($logPath)) { + $readError = "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($logPath); +} else { + $output = []; + $rc = 0; + // tail -n N ensures we never load the whole file into memory + exec('tail -n ' . intval($selectedN) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc); + if ($rc !== 0) { + $readError = "Erreur lors de la lecture du fichier journal."; + } else { + // Reverse so newest lines appear first + $lines = array_reverse($output); + } +} + +// ── File metadata ───────────────────────────────────────────────────────────── +$fileMeta = null; +if (file_exists($logPath)) { + $size = filesize($logPath); + $mtime = filemtime($logPath); + $fileMeta = [ + 'size' => $size > 1048576 + ? number_format($size / 1048576, 2) . ' MB' + : number_format($size / 1024, 1) . ' KB', + 'mtime' => date('d/m/Y H:i:s', $mtime), + ]; +} + +// ── Log-level classifier (for nginx error log coloring) ─────────────────────── +/** + * Returns a CSS class based on the log level token in a log line. + * nginx error lines look like: 2024/01/15 12:34:56 [error] 1234#1234: … + * nginx access lines: 1.2.3.4 - - [15/Jan/2024:12:34:56 +0000] "GET / HTTP/1.1" 200 … + */ +function logLineClass(string $line): string { + // nginx error log: [crit], [error], [warn], [notice], [info], [debug] + if (preg_match('/\[(crit|emerg|alert)\]/', $line)) return 'log-crit'; + if (preg_match('/\[error\]/', $line)) return 'log-error'; + if (preg_match('/\[warn\]/', $line)) return 'log-warn'; + if (preg_match('/\[notice\]/', $line)) return 'log-notice'; + // access log: highlight HTTP 4xx / 5xx + if (preg_match('/" [45]\d\d /', $line)) return 'log-error'; + if (preg_match('/" 3\d\d /', $line)) return 'log-notice'; + return ''; +} + +require_once APP_ROOT . '/templates/admin/head.php'; +?> + + + +
+

Journaux serveur

+ +

+ Affiché le — + Rafraîchir +

+ + +
+ + + + + + + 0): ?> + ligne(s) + +
+ + + +
+ + + +
+ + + + +
+ Journaux non disponibles +
+ +
+ En environnement de développement, les logs nginx ne sont pas disponibles. + Cette page est pleinement fonctionnelle sur le serveur de production. +
+ +
+ + +
Le fichier journal est vide.
+ + +
+ $line): ?> + + +
+ + +
+ + diff --git a/public/admin/status.php b/public/admin/status.php index cfe1c46..b066a4b 100644 --- a/public/admin/status.php +++ b/public/admin/status.php @@ -50,7 +50,8 @@ function localHttpCheck(string $url): ?array { curl_exec($ch); $ms = (int) round((microtime(true) - $start) * 1000); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); + // curl_close() is a no-op since PHP 8.0 and deprecated in PHP 8.5; omitted. + unset($ch); return $code > 0 ? [$code, $ms] : null; } diff --git a/templates/admin/head.php b/templates/admin/head.php index 8e0fd6c..7245c69 100644 --- a/templates/admin/head.php +++ b/templates/admin/head.php @@ -30,6 +30,7 @@ Pages statiques Mots-clés Statut + Journaux Modifier