diff --git a/TODO.md b/TODO.md index 9a5fe5a..3861185 100644 --- a/TODO.md +++ b/TODO.md @@ -58,20 +58,22 @@ The admin system page (`/admin/system.php`) runs expensive operations on every l - Nginx config file reading ### Solution: `system_cache` table + background refresh -- [ ] **Add `system_cache` table** to schema: `CREATE TABLE system_cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL)` — stores JSON-encoded status snapshots keyed by section -- [ ] **Add migration** `storage/migrations/007_system_cache.sql` to create the table -- [ ] **Add `SystemCache` class** (`src/SystemCache.php`) with methods: +- [x] **Add `system_cache` table** to schema: `CREATE TABLE system_cache (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL)` — stores JSON-encoded status snapshots keyed by section +- [x] **Add migration** `storage/migrations/007_system_cache.sql` to create the table +- [x] **Add `SystemCache` class** (`src/SystemCache.php`) with methods: - `get(string $key, int $maxAgeSec = 60): ?array` — returns cached JSON data if fresh, null if stale - `set(string $key, array $data): void` — upserts cache row - `isStale(string $key, int $maxAgeSec = 60): bool` -- [ ] **Refactor `system.php` status section** to: + - `ageSeconds(string $key): ?int` — returns age of cached entry in seconds + - `invalidate(string $key): void` — force-deletes a cache entry +- [x] **Refactor `system.php` status section** to: 1. Check `SystemCache::get('system_status', 120)` — 2-minute TTL 2. If cache hit → render from cache, show "mis en cache il y a X sec" label 3. If cache miss → run checks, store in cache, render 4. Add `?refresh=1` GET param to force-bypass cache -- [ ] **Refactor `system.php` log sections** to not cache (logs should always be live) but avoid re-reading on every tab switch — only read the active tab's log -- [ ] **Cache disk info** separately with 5-minute TTL: `SystemCache::get('disk_info', 300)` -- [ ] **Cache PHP info** separately with 1-hour TTL: `SystemCache::get('php_info', 3600)` — PHP config doesn't change at runtime +- [x] **Refactor `system.php` log sections** to not cache (logs should always be live) but avoid re-reading on every tab switch — only read the active tab's log +- [x] **Cache disk info** separately with 5-minute TTL: `SystemCache::get('disk_info', 300)` +- [x] **Cache PHP info** separately with 1-hour TTL: `SystemCache::get('php_info', 3600)` — PHP config doesn't change at runtime ## In Progress (from previous plan) - [ ] Extract `SearchController` — most complex public page (§2 step 4) diff --git a/public/admin/system.php b/public/admin/system.php index 8c3245f..6d0458f 100644 --- a/public/admin/system.php +++ b/public/admin/system.php @@ -1,10 +1,24 @@ 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 // ═══════════════════════════════════════════════════════════════════════════════ @@ -71,111 +85,141 @@ function statusClass(string $status): string { }; } -$checks = []; +// ── system_status cache (2-minute TTL: systemctl + curl checks) ───────────── +$statusCacheAge = $_cache->ageSeconds('system_status'); +$checksFromCache = $_cache->get('system_status', 120); -// nginx -$nginxStatus = systemdStatus('nginx'); -$nginxVersion = safeExec('nginx -v 2>&1 | head -1'); -$checks['nginx'] = [ - 'label' => 'nginx', - 'status' => $nginxStatus, - 'detail' => $nginxVersion, -]; +if ($checksFromCache !== null) { + $checks = $checksFromCache; + $statusCached = true; +} else { + $statusCached = false; + $checks = []; -// 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; + // 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, -]; + $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', -]; + // 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(); - $dbRowCount = $db->getThesisCount(); - } catch (Throwable $e) { - $dbRowCount = null; + // 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; } -$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', -]; +// ── 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); +} -// 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; +// ── 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 @@ -377,7 +421,8 @@ require_once APP_ROOT . '/templates/head.php';
Affiché le = date('d/m/Y à H:i:s') ?> — - Rafraîchir + Rafraîchir — + Forcer actualisation
@@ -399,7 +444,17 @@ require_once APP_ROOT . '/templates/head.php'; STATUS PANEL ════════════════════════════════════════════════════════════════════ --> -