getPDO()); // ?refresh=1 force-busts all cached sections $forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; if ($forceRefresh) { $_cache->invalidate('system_status'); $_cache->invalidate('disk_info'); $_cache->invalidate('php_info'); } // ═══════════════════════════════════════════════════════════════════════════════ // SECTION 1 — STATUS DATA // ═══════════════════════════════════════════════════════════════════════════════ function safeExec(string $cmd): ?string { if (!function_exists('exec')) return null; $output = []; $rc = 0; exec($cmd . ' 2>/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', }; } // ── system_status cache (2-minute TTL: systemctl + curl checks) ───────────── $statusCacheAge = $_cache->ageSeconds('system_status'); $checksFromCache = $_cache->get('system_status', 120); if ($checksFromCache !== null) { $checks = $checksFromCache; $statusCached = true; } else { $statusCached = false; $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 (DB object already created above, reuse it) $dbPath = $_db->getDatabasePath(); $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 { $dbRowCount = $_db->getThesisCount(); } 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é', ]; $_cache->set('system_status', $checks); $statusCacheAge = 0; } // ── php_info cache (1-hour TTL: PHP ini values don't change at runtime) ─────── $phpInfoFromCache = $_cache->get('php_info', 3600); if ($phpInfoFromCache !== null) { $phpInfo = $phpInfoFromCache; } else { $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', ]; $_cache->set('php_info', $phpInfo); } // ── disk_info cache (5-minute TTL) ──────────────────────────────────────────── $diskFromCache = $_cache->get('disk_info', 300); if ($diskFromCache !== null) { $diskTotal = $diskFromCache['total']; $diskFree = $diskFromCache['free']; $diskUsed = $diskFromCache['used']; $diskPct = $diskFromCache['pct']; } else { $diskTotal = disk_total_space(APP_ROOT); $diskFree = disk_free_space(APP_ROOT); $diskUsed = $diskTotal - $diskFree; $diskPct = $diskTotal > 0 ? (int) round($diskUsed / $diskTotal * 100) : 0; $_cache->set('disk_info', [ 'total' => $diskTotal, 'free' => $diskFree, 'used' => $diskUsed, 'pct' => $diskPct, ]); } // ═══════════════════════════════════════════════════════════════════════════════ // 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]; // Nginx config paths (live deployed, then local reference fallback) const NGINX_CONFIG_LIVE = '/etc/nginx/sites-available/posterg'; const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/posterg.conf'; // Active tab: 'nginx_config', or a log key (status is now always shown above tabs) $activeTab = $_GET['tab'] ?? 'nginx_access'; if ($activeTab === 'status') { // legacy URL — redirect to default log tab, status is always visible $activeTab = 'nginx_access'; } elseif ($activeTab !== 'nginx_config' && !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 !== 'nginx_config') { $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)), ]; } } // Pre-load nginx config tab $nginxConfigLines = null; $nginxConfigSource = null; $nginxConfigError = null; $nginxConfigMeta = null; if ($activeTab === 'nginx_config') { // Try live deployed config first, fall back to local reference copy $livePath = NGINX_CONFIG_LIVE; $localPath = NGINX_CONFIG_LOCAL; if (file_exists($livePath) && is_readable($livePath)) { $raw = file($livePath, FILE_IGNORE_NEW_LINES); if ($raw !== false) { $nginxConfigLines = $raw; $nginxConfigSource = 'live'; $sz = filesize($livePath); $nginxConfigMeta = [ 'path' => $livePath, 'size' => $sz > 1048576 ? number_format($sz / 1048576, 2) . ' MB' : number_format($sz / 1024, 1) . ' KB', 'mtime' => date('d/m/Y H:i:s', filemtime($livePath)), ]; } } if ($nginxConfigLines === null && file_exists($localPath) && is_readable($localPath)) { $raw = file($localPath, FILE_IGNORE_NEW_LINES); if ($raw !== false) { $nginxConfigLines = $raw; $nginxConfigSource = 'local'; $sz = filesize($localPath); $nginxConfigMeta = [ 'path' => $localPath, 'size' => $sz > 1048576 ? number_format($sz / 1048576, 2) . ' MB' : number_format($sz / 1024, 1) . ' KB', 'mtime' => date('d/m/Y H:i:s', filemtime($localPath)), ]; } } if ($nginxConfigLines === null) { $nginxConfigError = file_exists($livePath) ? "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($livePath) : "Config live introuvable (" . htmlspecialchars($livePath) . ") et config locale introuvable (" . htmlspecialchars($localPath) . ")."; } } $isAdmin = true; $bodyClass = 'admin-body'; $extraCss = ['/assets/css/system.css']; $extraJsInline = <<<'JS' (function () { // ── Status section toggle ────────────────────────────────────────── var toggleBtn = document.getElementById('sys-status-toggle'); var statusBody = document.getElementById('sys-status-body'); if (toggleBtn && statusBody) { toggleBtn.addEventListener('click', function () { var collapsed = statusBody.hidden; statusBody.hidden = !collapsed; toggleBtn.setAttribute('aria-expanded', collapsed ? 'true' : 'false'); toggleBtn.textContent = collapsed ? '▲ Réduire' : '▼ Développer'; try { localStorage.setItem('sys_status_collapsed', collapsed ? '0' : '1'); } catch(e) {} }); // Restore collapsed state try { if (localStorage.getItem('sys_status_collapsed') === '1') { statusBody.hidden = true; toggleBtn.setAttribute('aria-expanded', 'false'); toggleBtn.textContent = '▼ Développer'; } } catch(e) {} } // ── Instant tab switch on lines-select change ────────────────────── var sel = document.getElementById('lines-select'); if (sel) { sel.addEventListener('change', function () { var url = new URL(window.location.href); url.searchParams.set('n', this.value); window.location.href = url.toString(); }); } // ── Copy-to-clipboard ───────────────────────────────────────────── var copyBtn = document.getElementById('log-copy-btn'); var logOut = document.getElementById('log-output'); if (copyBtn && logOut) { copyBtn.addEventListener('click', function () { // Collect text from all .log-line spans (strip the gutter number // rendered via CSS ::before — it's not in the DOM text content). var lines = Array.from(logOut.querySelectorAll('.log-line')) .map(function (el) { return el.textContent; }) .join('\n'); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(lines).then(function () { copyBtn.textContent = '✓ Copié'; copyBtn.classList.add('copied'); setTimeout(function () { copyBtn.textContent = 'Copier'; copyBtn.classList.remove('copied'); }, 2000); }).catch(function () { fallbackCopy(lines); }); } else { fallbackCopy(lines); } }); } function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (e) {} document.body.removeChild(ta); } })(); JS; require_once APP_ROOT . '/templates/head.php'; ?>

Système

Affiché le RafraîchirForcer actualisation

Statut ⚡ Cache — il y a s ⟳ Actualisé

Environnement PHP

$val): ?>

Espace disque

85 ? '#e05555' : ($diskPct > 70 ? '#ffc107' : '#4caf50'); ?>
utilisé (%) libre /
● Config déployée ⚠ Référence locale (config live inaccessible)
Configuration nginx non disponible
En développement, /etc/nginx/sites-available/posterg n'existe pas. La config de référence se trouve dans nginx/posterg.conf.
Le fichier de configuration est vide.
$line): ?>
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): ?>