From b981223ff4a1e0a6e9839ce42d54251b8ffbc3c8 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 2 Apr 2026 18:39:55 +0200 Subject: [PATCH] admin/system: fetch()-based tab switching, no full-page reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add system-fragment.php — a thin authenticated endpoint that returns only the tab-panel HTML (toolbar + meta + log/nginx-config output) for a given ?tab=&n= combination. No page shell, no status section, no DB queries. system.php changes: - Tab elements gain data-tab= attributes used by JS to identify the target without parsing hrefs. - Tab panel content wrapped in
which JS uses as both the swap target and its own state store. - JS rewritten: tab clicks and lines-select changes call loadPanel() which fetch()es system-fragment.php, swaps innerHTML, updates active tab ARIA attributes, and pushes state via history.pushState. - Browser back/forward handled via popstate listener. - bindPanelControls() re-wires the lines-select and copy-to-clipboard button after every innerHTML swap (event delegation not feasible here because log-output is replaced wholesale). - fetch() failure falls back to window.location.href (full page load). - Tabs without JS still work: links go to system.php?tab=… as before. system-fragment.php: - Requires AdminAuth::isAuthenticated(); returns 403 on failure. - Validates tab and n params against the same whitelist as system.php. - All helper functions namespaced with frag_ prefix to avoid redeclaration if PHP ever includes both files in the same process. - Renders identical HTML to the corresponding section in system.php. system.css: - #sys-tab-panel gets min-height:8rem and position:relative to prevent layout jump during fetch. - .sys-panel-loading: opacity 0.4 + pointer-events:none + subtle diagonal-stripe ::after overlay with shimmer animation. --- public/admin/system-fragment.php | 217 +++++++++++++++++++++++++++++++ public/admin/system.php | 155 +++++++++++++++++----- public/assets/css/system.css | 30 +++++ todo/03-system-cache.md | 2 +- 4 files changed, 368 insertions(+), 36 deletions(-) create mode 100644 public/admin/system-fragment.php diff --git a/public/admin/system-fragment.php b/public/admin/system-fragment.php new file mode 100644 index 0000000..67b9240 --- /dev/null +++ b/public/admin/system-fragment.php @@ -0,0 +1,217 @@ + hrefs still + * point at system.php?tab=… so navigation degrades gracefully. + * + * Response: text/html fragment (no // wrapper). + * On any auth failure or bad request: 403 / 400 with a plain-text body. + */ +require_once __DIR__ . "/../../config/bootstrap.php"; +require_once __DIR__ . '/../../src/AdminAuth.php'; + +if (!AdminAuth::isAuthenticated()) { + http_response_code(403); + header('Content-Type: text/plain; charset=utf-8'); + echo 'Non autorisé'; + exit; +} + +// ── Validate inputs ──────────────────────────────────────────────────────── +const LOG_FILES_FRAG = [ + '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_FRAG = [50, 100, 200, 500]; + +$tab = $_GET['tab'] ?? 'nginx_access'; +if ($tab !== 'nginx_config' && !array_key_exists($tab, LOG_FILES_FRAG)) { + $tab = 'nginx_access'; +} + +$n = isset($_GET['n']) ? (int)$_GET['n'] : 100; +if (!in_array($n, ALLOWED_LINES_FRAG, true)) { + $n = 100; +} + +header('Content-Type: text/html; charset=utf-8'); +header('X-Robots-Tag: noindex'); + +// ── Helpers (duplicated from system.php — small enough to inline) ────────── +function frag_readLogTail(string $logPath, int $lines, ?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($lines) . ' ' . 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); +} + +function frag_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 ''; +} + +function frag_nginxLineClass(string $line): string +{ + $trimmed = ltrim($line); + if ($trimmed === '' || str_starts_with($trimmed, '#')) return 'nginx-comment'; + if (preg_match('/^\s*(location|server|upstream|events|http|geo|map|types)\b/', $line)) return 'nginx-block'; + return 'nginx-directive'; +} + +// ── Render ───────────────────────────────────────────────────────────────── +if ($tab === 'nginx_config') { + $livePath = '/etc/nginx/sites-available/posterg'; + $localPath = APP_ROOT . '/nginx/posterg.conf'; + + $lines = null; + $source = null; + $meta = null; + $error = null; + + foreach ([[$livePath, 'live'], [$localPath, 'local']] as [$path, $src]) { + if (file_exists($path) && is_readable($path)) { + $raw = file($path, FILE_IGNORE_NEW_LINES); + if ($raw !== false) { + $lines = $raw; + $source = $src; + $sz = filesize($path); + $meta = [ + 'path' => $path, + 'size' => $sz > 1048576 + ? number_format($sz / 1048576, 2) . ' MB' + : number_format($sz / 1024, 1) . ' KB', + 'mtime' => date('d/m/Y H:i:s', filemtime($path)), + ]; + break; + } + } + } + + if ($lines === null) { + $error = file_exists($livePath) + ? "Fichier non lisible (permissions insuffisantes) : " . htmlspecialchars($livePath) + : "Config live introuvable (" . htmlspecialchars($livePath) . ") et config locale introuvable (" . htmlspecialchars($localPath) . ")."; + } + + if ($meta): ?> +
+ + + + + ● 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): ?> + + +
+ $sz > 1048576 + ? number_format($sz / 1048576, 2) . ' MB' + : number_format($sz / 1024, 1) . ' KB', + 'mtime' => date('d/m/Y H:i:s', filemtime($logPath)), + ]; + } + ?> +
+ + + 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): ?> + + +
+ $def): ?>
> >nginx — config + +
+ + diff --git a/public/assets/css/system.css b/public/assets/css/system.css index 2e2a363..0d4d10c 100644 --- a/public/assets/css/system.css +++ b/public/assets/css/system.css @@ -170,6 +170,36 @@ margin-top: .25rem; } +/* ── Tab panel loading state ──────────────────────────────────────────────── */ +#sys-tab-panel { + min-height: 8rem; /* prevent layout jump while fetching */ + position: relative; +} +#sys-tab-panel.sys-panel-loading { + opacity: 0.4; + pointer-events: none; + transition: opacity 0.1s; +} +#sys-tab-panel.sys-panel-loading::after { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 6px, + rgba(255,255,255,.03) 6px, + rgba(255,255,255,.03) 12px + ); + border-radius: 4px; + animation: sys-panel-shimmer 1s linear infinite; + background-size: 200% 200%; +} +@keyframes sys-panel-shimmer { + 0% { background-position: 0 0; } + 100% { background-position: 100% 100%; } +} + /* ── Log viewer ────────────────────────────────────────────────────────── */ .log-toolbar { display: flex; diff --git a/todo/03-system-cache.md b/todo/03-system-cache.md index 9f66b5b..c9dd0a5 100644 --- a/todo/03-system-cache.md +++ b/todo/03-system-cache.md @@ -30,4 +30,4 @@ The admin system page (`/admin/system.php`) runs expensive operations on every l - Log caching deliberately omitted: `tail` output is inherently real-time and caching even 30s would show stale data during the moments it matters most (deploys, errors). The existing tab guard already ensures only the active log file is read. - nginx config could be cached but `file()` on a small static config file is negligible; not worth the added complexity. -- A future improvement could stream log tabs via `fetch()` to avoid full-page reloads on tab switch. +- [x] Log tab switching and line-count changes now use `fetch()` via `system-fragment.php`; no full page reload on tab switch.