admin/system: move status panel above tabs, add collapse toggle

Status (services, PHP env, disk) is now always visible above the log/config
tab bar rather than being one of the tab targets:

- Status section rendered unconditionally above <nav class="sys-tabs">.
- Services grid, PHP info grid and disk bar grouped inside a collapsible
  <section> with a header row containing the cache-freshness badge and a
  toggle button (▲ Réduire / ▼ Développer).
- Collapse state persisted in localStorage so the preference survives
  page reloads (e.g. when switching log tabs).
- Tab bar now only contains the three log tabs + nginx config; the 'Statut'
  tab is removed. Legacy ?tab=status URLs fall through to nginx_access.
- PHP/disk sub-sections laid out in a 2-col grid inside the status panel;
  responsive single-col below 700px.
- system.css: new .sys-status-section / .sys-status-header /
  .sys-status-toggle / .sys-status-meta rules added.
- aria-current="page" added to active tab links.
- todo/03-system-cache.md: all items marked done; notes added explaining
  why log caching was deliberately omitted.
This commit is contained in:
Pontoporeia
2026-04-02 18:31:38 +02:00
parent e1ce900113
commit c86781b9be
3 changed files with 155 additions and 76 deletions

View File

@@ -237,11 +237,12 @@ const ALLOWED_LINES = [50, 100, 200, 500];
const NGINX_CONFIG_LIVE = '/etc/nginx/sites-available/posterg';
const NGINX_CONFIG_LOCAL = APP_ROOT . '/nginx/posterg.conf';
// Active tab: 'status', 'nginx_config', or a log key
// Active tab: 'nginx_config', or a log key (status is now always shown above tabs)
$activeTab = $_GET['tab'] ?? 'nginx_access';
if ($activeTab === 'status' || $activeTab === 'nginx_config') {
// valid as-is
} elseif (!array_key_exists($activeTab, LOG_FILES)) {
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';
}
@@ -293,7 +294,7 @@ $logLines = null;
$logError = null;
$logFileMeta = null;
if ($activeTab !== 'status' && $activeTab !== 'nginx_config') {
if ($activeTab !== 'nginx_config') {
$logPath = LOG_FILES[$activeTab]['path'];
$logLines = readLogTail($logPath, $selectedN, $logError);
if (file_exists($logPath)) {
@@ -361,6 +362,27 @@ $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) {
@@ -425,26 +447,12 @@ require_once APP_ROOT . '/templates/head.php';
<a href="?tab=<?= htmlspecialchars($activeTab) ?>&amp;n=<?= $selectedN ?>&amp;refresh=1">Forcer actualisation</a>
</p>
<!-- ── Tab bar ─────────────────────────────────────────────────────── -->
<nav class="sys-tabs" aria-label="Sections système">
<a href="?tab=status"
class="sys-tab <?= $activeTab === 'status' ? 'active' : '' ?>">Statut</a>
<?php foreach (LOG_FILES as $key => $def): ?>
<a href="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>">
<?= htmlspecialchars($def['label']) ?>
</a>
<?php endforeach; ?>
<a href="?tab=nginx_config"
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>">nginx — config</a>
</nav>
<?php if ($activeTab === 'status'): ?>
<!-- ════════════════════════════════════════════════════════════════════
STATUS PANEL
STATUS SECTION — always visible above tabs
════════════════════════════════════════════════════════════════════ -->
<h2 class="srv-section-title">Services
<section class="sys-status-section" aria-label="Statut du système">
<div class="sys-status-header">
<h2 class="srv-section-title" style="margin:0;border:none;padding:0;">Statut
<?php if ($statusCached && $statusCacheAge !== null): ?>
<span class="sys-cache-badge sys-cache-badge--hit" title="Données en cache">
⚡ Cache — il y a <?= $statusCacheAge ?>s
@@ -455,6 +463,12 @@ require_once APP_ROOT . '/templates/head.php';
</span>
<?php endif; ?>
</h2>
<button id="sys-status-toggle" class="sys-status-toggle"
aria-expanded="true" aria-controls="sys-status-body"
type="button">▲ Réduire</button>
</div>
<div id="sys-status-body">
<div class="srv-grid">
<?php foreach ($checks as $check): ?>
<?php $st = $check['status'] ?? 'unknown'; ?>
@@ -470,8 +484,10 @@ require_once APP_ROOT . '/templates/head.php';
<?php endforeach; ?>
</div>
<h2 class="srv-section-title">Environnement PHP</h2>
<div class="php-grid">
<div class="sys-status-meta">
<div>
<h3 class="srv-section-title" style="margin-bottom:.75rem;">Environnement PHP</h3>
<div class="php-grid" style="margin-bottom:0;">
<?php foreach ($phpInfo as $key => $val): ?>
<div class="php-item">
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
@@ -479,12 +495,10 @@ require_once APP_ROOT . '/templates/head.php';
</div>
<?php endforeach; ?>
</div>
<h2 class="srv-section-title">Espace disque</h2>
<div style="max-width:420px;margin-bottom:2.5rem;">
<?php
$diskColor = $diskPct > 85 ? '#e05555' : ($diskPct > 70 ? '#ffc107' : '#4caf50');
?>
</div>
<div>
<h3 class="srv-section-title" style="margin-bottom:.75rem;">Espace disque</h3>
<?php $diskColor = $diskPct > 85 ? '#e05555' : ($diskPct > 70 ? '#ffc107' : '#4caf50'); ?>
<div class="disk-bar-wrap">
<div class="disk-bar" style="--disk-pct:<?= $diskPct ?>%;--disk-color:<?= $diskColor ?>"></div>
</div>
@@ -493,8 +507,25 @@ require_once APP_ROOT . '/templates/head.php';
<span><?= humanBytes($diskFree) ?> libre / <?= humanBytes($diskTotal) ?></span>
</div>
</div>
</div>
</div>
</section>
<?php elseif ($activeTab === 'nginx_config'): ?>
<!-- ── Tab bar ─────────────────────────────────────────────────────── -->
<nav class="sys-tabs" aria-label="Journaux et configuration">
<?php foreach (LOG_FILES as $key => $def): ?>
<a href="?tab=<?= htmlspecialchars($key) ?>&amp;n=<?= $selectedN ?>"
class="sys-tab <?= $activeTab === $key ? 'active' : '' ?>"
<?= $activeTab === $key ? 'aria-current="page"' : '' ?>>
<?= htmlspecialchars($def['label']) ?>
</a>
<?php endforeach; ?>
<a href="?tab=nginx_config"
class="sys-tab <?= $activeTab === 'nginx_config' ? 'active' : '' ?>"
<?= $activeTab === 'nginx_config' ? 'aria-current="page"' : '' ?>>nginx — config</a>
</nav>
<?php if ($activeTab === 'nginx_config'): ?>
<!-- ════════════════════════════════════════════════════════════════════
NGINX CONFIG PANEL
════════════════════════════════════════════════════════════════════ -->

View File

@@ -30,6 +30,48 @@
border-bottom-color: var(--accent-primary);
}
/* ── Status section (always-visible panel above tabs) ─────────────────── */
.sys-status-section {
background: #1a1a1a;
border: 1px solid #555;
border-radius: 6px;
padding: 1rem 1.25rem 1.25rem;
margin-bottom: 1.75rem;
}
.sys-status-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.sys-status-toggle {
background: none;
border: 1px solid #555;
color: #969696;
border-radius: 3px;
font-size: .72rem;
font-family: inherit;
padding: .2rem .55rem;
cursor: pointer;
white-space: nowrap;
transition: color .15s, border-color .15s;
}
.sys-status-toggle:hover {
color: #e8e8e8;
border-color: #888;
}
.sys-status-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem 2rem;
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid #333;
}
@media (max-width: 700px) {
.sys-status-meta { grid-template-columns: 1fr; }
}
/* ── Status cards ──────────────────────────────────────────────────────── */
.srv-grid {
display: grid;

View File

@@ -1,4 +1,4 @@
# System Page Caching Database-Backed Status Cache
# System Page Caching - Database-Backed Status Cache
## Problem
@@ -11,17 +11,23 @@ The admin system page (`/admin/system.php`) runs expensive operations on every l
## 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)`
- [ ] **Add migration** `storage/migrations/007_system_cache.sql`
- [ ] **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)`
- [x] **Add migration** `storage/migrations/007_system_cache.sql`
- [x] **Add `SystemCache` class** (`src/SystemCache.php`) with methods:
- `get(string $key, int $maxAgeSec = 60): ?array`
- `set(string $key, array $data): void`
- `isStale(string $key, int $maxAgeSec = 60): bool`
- [ ] **Refactor `system.php` status section**:
- [x] **Refactor `system.php` status section**:
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
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** — 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)`
- [x] **Refactor `system.php` log sections** — avoid re-reading on every tab switch; only read the active tabs 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)`
## Notes
- 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.