mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Add admin server status page
New page /admin/status.php gives a real-time health dashboard: - Services panel: nginx (systemctl), php-fpm (auto-detects versioned unit names), site HTTP ping (curl HEAD with latency), SQLite DB (exists/writable/row count/size), storage directory (writable, banner/cover file counts), maintenance-mode flag. - PHP runtime panel: version, SAPI, memory_limit, upload_max_filesize, post_max_size, max_execution_time. - Disk usage bar for the partition containing APP_ROOT (colour-coded: green/amber/red). - All shell calls go through safeExec() which suppresses stderr and checks exit code; systemctl/curl unavailability degrades gracefully to 'unknown' without fatal errors. - 'Statut' nav link added to templates/admin/head.php (active state on status.php).
This commit is contained in:
349
public/admin/status.php
Normal file
349
public/admin/status.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
$pageTitle = "Statut serveur";
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run a shell command safely and return trimmed stdout.
|
||||
* Returns null if exec is disabled or the command fails.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($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';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.srv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.srv-card {
|
||||
background: var(--admin-bg-alt);
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 5px;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.srv-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.srv-card__name {
|
||||
font-size: .82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
color: var(--admin-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
.srv-card__detail {
|
||||
font-size: .8rem;
|
||||
color: var(--admin-text-muted);
|
||||
margin-top: .25rem;
|
||||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
.status-ok { color: #4caf50; font-weight: 600; font-size: .85rem; }
|
||||
.status-warn { color: #ffc107; font-weight: 600; font-size: .85rem; }
|
||||
.status-err { color: #e05555; font-weight: 600; font-size: .85rem; }
|
||||
.status-unknown { color: #888; font-weight: 600; font-size: .85rem; }
|
||||
|
||||
.srv-section-title {
|
||||
font-size: .82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .1em;
|
||||
color: var(--admin-text-muted);
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
padding-bottom: .4rem;
|
||||
margin: 0 0 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.php-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: .5rem .75rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.php-item {
|
||||
background: var(--admin-bg-alt);
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 4px;
|
||||
padding: .5rem .75rem;
|
||||
}
|
||||
.php-item__key {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
.php-item__val {
|
||||
font-size: .92rem;
|
||||
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
|
||||
color: var(--admin-text);
|
||||
margin-top: .15rem;
|
||||
}
|
||||
.disk-bar-wrap {
|
||||
background: var(--admin-border);
|
||||
border-radius: 3px;
|
||||
height: 6px;
|
||||
margin-top: .5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.disk-bar {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
background: <?= $diskPct > 85 ? '#e05555' : ($diskPct > 70 ? '#ffc107' : '#4caf50') ?>;
|
||||
width: <?= $diskPct ?>%;
|
||||
transition: width .3s;
|
||||
}
|
||||
.disk-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .78rem;
|
||||
color: var(--admin-text-muted);
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.admin-refresh-note {
|
||||
font-size: .78rem;
|
||||
color: var(--admin-text-muted);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.admin-refresh-note a {
|
||||
color: var(--admin-purple);
|
||||
text-decoration: none;
|
||||
}
|
||||
.admin-refresh-note a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
|
||||
<main class="admin-main">
|
||||
<h1 class="admin-page-title">Statut serveur</h1>
|
||||
|
||||
<p class="admin-refresh-note">
|
||||
Vérification effectuée le <?= date('d/m/Y à H:i:s') ?> —
|
||||
<a href="?">Rafraîchir</a>
|
||||
</p>
|
||||
|
||||
<!-- Services -->
|
||||
<h2 class="srv-section-title">Services</h2>
|
||||
<div class="srv-grid">
|
||||
<?php foreach ($checks as $check): ?>
|
||||
<?php $st = $check['status'] ?? 'unknown'; ?>
|
||||
<div class="srv-card">
|
||||
<div class="srv-card__header">
|
||||
<span class="srv-card__name"><?= htmlspecialchars($check['label']) ?></span>
|
||||
<span class="<?= statusClass($st) ?>"><?= statusLabel($st) ?></span>
|
||||
</div>
|
||||
<?php if (!empty($check['detail'])): ?>
|
||||
<div class="srv-card__detail"><?= htmlspecialchars($check['detail']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- PHP runtime -->
|
||||
<h2 class="srv-section-title">Environnement PHP</h2>
|
||||
<div class="php-grid">
|
||||
<?php foreach ($phpInfo as $key => $val): ?>
|
||||
<div class="php-item">
|
||||
<div class="php-item__key"><?= htmlspecialchars($key) ?></div>
|
||||
<div class="php-item__val"><?= htmlspecialchars($val) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<h2 class="srv-section-title">Espace disque</h2>
|
||||
<div style="max-width:420px;margin-bottom:2.5rem;">
|
||||
<div class="disk-bar-wrap"><div class="disk-bar"></div></div>
|
||||
<div class="disk-stats">
|
||||
<span><?= humanBytes($diskUsed) ?> utilisé (<?= $diskPct ?>%)</span>
|
||||
<span><?= humanBytes($diskFree) ?> libre / <?= humanBytes($diskTotal) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<?php require_once APP_ROOT . '/templates/admin/footer.php'; ?>
|
||||
Reference in New Issue
Block a user