mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
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).
350 lines
11 KiB
PHP
350 lines
11 KiB
PHP
<?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'; ?>
|