- Journaux serveur
-
-
- Affiché le = date('d/m/Y à H:i:s') ?> —
- Rafraîchir
-
-
-
-
-
-
-
-
- = htmlspecialchars($logPath) ?>
- = $fileMeta['size'] ?>
- = $fileMeta['mtime'] ?>
-
-
-
-
-
-
-
Journaux non disponibles
-
= $readError ?>
-
-
- 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): ?>
- = htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?>
-
-
-
-
-
-
-
+// Redirects legacy /admin/logs.php → /admin/system.php?tab=nginx_access
+header('Location: /admin/system.php?tab=nginx_access', true, 301);
+exit;
diff --git a/public/admin/status.php b/public/admin/status.php
index b066a4b..a75231e 100644
--- a/public/admin/status.php
+++ b/public/admin/status.php
@@ -1,350 +1,4 @@
/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() is a no-op since PHP 8.0 and deprecated in PHP 8.5; omitted.
- unset($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 = date('d/m/Y à H:i:s') ?> —
- Rafraîchir
-
-
-
- Services
-
-
-
-
-
-
-
= htmlspecialchars($check['detail']) ?>
-
-
-
-
-
-
- Environnement PHP
-
- $val): ?>
-
-
= htmlspecialchars($key) ?>
-
= htmlspecialchars($val) ?>
-
-
-
-
-
- Espace disque
-
-
-
- = humanBytes($diskUsed) ?> utilisé (= $diskPct ?>%)
- = humanBytes($diskFree) ?> libre / = humanBytes($diskTotal) ?>
-
-
-
-
-
-
+// Redirects legacy /admin/status.php → /admin/system.php?tab=status
+header('Location: /admin/system.php?tab=status', true, 301);
+exit;
diff --git a/public/admin/system.php b/public/admin/system.php
new file mode 100644
index 0000000..0723050
--- /dev/null
+++ b/public/admin/system.php
@@ -0,0 +1,698 @@
+/dev/null', $output, $rc);
+ return $rc === 0 ? trim(implode("\n", $output)) : null;
+}
+
+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';
+}
+
+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);
+ unset($ch);
+ return $code > 0 ? [$code, $ms] : null;
+}
+
+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';
+}
+
+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',
+ };
+}
+
+$checks = [];
+
+// nginx
+$nginxStatus = systemdStatus('nginx');
+$nginxVersion = safeExec('nginx -v 2>&1 | head -1');
+$checks['nginx'] = [
+ 'label' => 'nginx',
+ 'status' => $nginxStatus,
+ 'detail' => $nginxVersion,
+];
+
+// 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,
+];
+
+// 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',
+];
+
+// Database
+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';
+
+$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',
+];
+
+// 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',
+];
+
+// 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é',
+];
+
+// 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',
+];
+
+// Disk
+$diskTotal = disk_total_space(APP_ROOT);
+$diskFree = disk_free_space(APP_ROOT);
+$diskUsed = $diskTotal - $diskFree;
+$diskPct = $diskTotal > 0 ? (int) round($diskUsed / $diskTotal * 100) : 0;
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// SECTION 2 — LOGS DATA
+// ═══════════════════════════════════════════════════════════════════════════════
+
+const LOG_FILES = [
+ 'nginx_access' => ['label' => 'nginx — accès', 'path' => '/var/log/nginx/posterg_access.log'],
+ 'nginx_error' => ['label' => 'nginx — erreurs', 'path' => '/var/log/nginx/posterg_error.log'],
+ 'php_error' => ['label' => 'PHP-FPM — erreurs', 'path' => '/var/log/php8.4-fpm.log'],
+];
+
+const ALLOWED_LINES = [50, 100, 200, 500];
+
+// Active tab: 'status' or a log key
+$activeTab = $_GET['tab'] ?? 'nginx_access';
+if ($activeTab === 'status') {
+ $activeTab = 'status';
+} elseif (!array_key_exists($activeTab, LOG_FILES)) {
+ $activeTab = 'nginx_access';
+}
+
+$selectedN = isset($_GET['n']) ? (int) $_GET['n'] : 100;
+if (!in_array($selectedN, ALLOWED_LINES, true)) {
+ $selectedN = 100;
+}
+
+/**
+ * Read the tail of a log file. Returns null on error, [] if empty, or string[].
+ * $errorMsg is set on failure.
+ */
+function readLogTail(string $logPath, int $n, ?string &$errorMsg): ?array {
+ $errorMsg = null;
+ if (!function_exists('exec')) {
+ $errorMsg = "exec() est désactivé sur ce serveur.";
+ return null;
+ }
+ if (!file_exists($logPath)) {
+ $errorMsg = "Fichier introuvable : " . htmlspecialchars($logPath);
+ return null;
+ }
+ if (!is_readable($logPath)) {
+ $errorMsg = "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($logPath);
+ return null;
+ }
+ $output = [];
+ $rc = 0;
+ exec('tail -n ' . intval($n) . ' ' . escapeshellarg($logPath) . ' 2>/dev/null', $output, $rc);
+ if ($rc !== 0) {
+ $errorMsg = "Erreur lors de la lecture du fichier journal.";
+ return null;
+ }
+ return array_reverse($output); // newest first
+}
+
+function logLineClass(string $line): string {
+ 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';
+ if (preg_match('/" [45]\d\d /', $line)) return 'log-error';
+ if (preg_match('/" 3\d\d /', $line)) return 'log-notice';
+ return '';
+}
+
+// Pre-load the active log tab if it's a log key
+$logLines = null;
+$logError = null;
+$logFileMeta = null;
+
+if ($activeTab !== 'status') {
+ $logPath = LOG_FILES[$activeTab]['path'];
+ $logLines = readLogTail($logPath, $selectedN, $logError);
+ if (file_exists($logPath)) {
+ $sz = filesize($logPath);
+ $logFileMeta = [
+ 'size' => $sz > 1048576
+ ? number_format($sz / 1048576, 2) . ' MB'
+ : number_format($sz / 1024, 1) . ' KB',
+ 'mtime' => date('d/m/Y H:i:s', filemtime($logPath)),
+ ];
+ }
+}
+
+require_once APP_ROOT . '/templates/admin/head.php';
+?>
+
+
+
+
+ Système
+
+
+ Affiché le = date('d/m/Y à H:i:s') ?> —
+ Rafraîchir
+
+
+
+
+
+
+
+
+ Services
+
+
+
+
+
+
+
= htmlspecialchars($check['detail']) ?>
+
+
+
+
+
+ Environnement PHP
+
+ $val): ?>
+
+
= htmlspecialchars($key) ?>
+
= htmlspecialchars($val) ?>
+
+
+
+
+ Espace disque
+
+
+
+ = humanBytes($diskUsed) ?> utilisé (= $diskPct ?>%)
+ = humanBytes($diskFree) ?> libre / = humanBytes($diskTotal) ?>
+
+
+
+
+
+
+
+
+
+
+ 0): ?>
+ = count($logLines) ?> ligne(s)
+
+
+
+
+
+
+ = htmlspecialchars(LOG_FILES[$activeTab]['path']) ?>
+ = $logFileMeta['size'] ?>
+ = $logFileMeta['mtime'] ?>
+
+
+
+
+
+
+
Journaux non disponibles
+
= $logError ?>
+
+
+ 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): ?>
+ = htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE) ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/admin/head.php b/templates/admin/head.php
index 309662c..74e287e 100644
--- a/templates/admin/head.php
+++ b/templates/admin/head.php
@@ -29,8 +29,7 @@