Files
GridTV/health.php
T

278 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/src/lib/admin.php';
$config = gridtv_load_config();
gridtv_require_admin($config, 'Health');
[$locale, $L] = gridtv_load_locale($config);
function health_status_class(bool $ok): string {
return $ok ? 'ok' : 'ko';
}
function health_status_label(bool $ok): string {
return $ok ? 'OK' : 'Issue';
}
function health_parse_xmltv(string $xml): array {
if (!class_exists('DOMDocument')) {
return ['ok' => false, 'error' => 'PHP DOM/XML extension is not installed.'];
}
if ($xml === '') {
return ['ok' => false, 'error' => 'Empty response'];
}
libxml_use_internal_errors(true);
$dom = new DOMDocument();
$loaded = $dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING);
$errors = array_map(static fn($e) => trim($e->message), libxml_get_errors());
libxml_clear_errors();
if (!$loaded) {
return ['ok' => false, 'error' => $errors[0] ?? 'Invalid XML'];
}
$xp = new DOMXPath($dom);
$programmes = $xp->query('/tv/programme');
$channels = $xp->query('/tv/channel');
$categories = $xp->query('/tv/programme/category');
$subtitles = $xp->query('/tv/programme/sub-title');
$ratings = $xp->query('/tv/programme/rating');
$dates = $xp->query('/tv/programme/date');
$first = null;
$last = null;
foreach ($programmes as $programme) {
$start = $programme->getAttribute('start');
$stop = $programme->getAttribute('stop');
if ($start !== '' && ($first === null || strcmp($start, $first) < 0)) $first = $start;
if ($stop !== '' && ($last === null || strcmp($stop, $last) > 0)) $last = $stop;
}
return [
'ok' => true,
'channels' => $channels->length,
'programmes' => $programmes->length,
'categories' => $categories->length,
'subtitles' => $subtitles->length,
'ratings' => $ratings->length,
'dates' => $dates->length,
'first_start' => $first,
'last_stop' => $last,
];
}
function health_parse_m3u(string $body): array {
$lines = preg_split('/\r\n|\r|\n/', $body);
$streams = 0;
$extinf = 0;
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') continue;
if (stripos($line, '#EXTINF') === 0) {
$extinf++;
continue;
}
if ($line[0] !== '#') $streams++;
}
return ['streams' => $streams, 'extinf' => $extinf];
}
function health_format_xmltv_stamp(?string $stamp): string {
if (!$stamp) return '—';
$dt = DateTime::createFromFormat('YmdHis O', $stamp);
if (!$dt) return $stamp;
return $dt->format('d/m/Y H:i');
}
$config_path = gridtv_config_path();
$config_exists = file_exists($config_path);
$config_writable = $config_exists ? is_writable($config_path) : is_writable(dirname($config_path));
$sources = array_values($config['epg_sources'] ?? []);
$checks = [
[
'label' => 'Config file',
'ok' => $config_exists,
'detail' => $config_exists ? basename($config_path) . ' found' : 'Missing config.json',
],
[
'label' => 'Config writable',
'ok' => $config_writable,
'detail' => $config_writable ? 'Writable by PHP' : 'Not writable by PHP',
],
[
'label' => 'PHP version',
'ok' => version_compare(PHP_VERSION, '8.0.0', '>='),
'detail' => 'PHP ' . PHP_VERSION,
],
[
'label' => 'php-curl',
'ok' => extension_loaded('curl'),
'detail' => extension_loaded('curl') ? 'Extension loaded' : 'Missing php-curl',
],
[
'label' => 'Configured sources',
'ok' => count($sources) > 0,
'detail' => count($sources) . ' source(s) in config',
],
];
$source_reports = [];
foreach ($sources as $index => $source) {
$epg_url = trim((string) ($source['epg_url'] ?? ''));
$m3u_url = trim((string) ($source['m3u_url'] ?? ''));
$epg_fetch = $epg_url !== '' ? gridtv_fetch_url($epg_url, 25) : ['ok' => false, 'status' => 0, 'error' => 'Missing URL', 'body' => '', 'content_type' => '', 'final_url' => '', 'time_total' => 0.0];
$epg_stats = $epg_fetch['ok'] ? health_parse_xmltv($epg_fetch['body']) : ['ok' => false, 'error' => $epg_fetch['error'] ?: 'HTTP ' . $epg_fetch['status']];
$m3u_fetch = null;
$m3u_stats = null;
if ($m3u_url !== '') {
$m3u_fetch = gridtv_fetch_url($m3u_url, 20);
$m3u_stats = $m3u_fetch['ok'] ? health_parse_m3u($m3u_fetch['body']) : null;
}
$source_reports[] = [
'name' => $source['name'] ?: 'Source ' . ($index + 1),
'epg_url' => $epg_url,
'm3u_url' => $m3u_url,
'epg_fetch' => $epg_fetch,
'epg_stats' => $epg_stats,
'm3u_fetch' => $m3u_fetch,
'm3u_stats' => $m3u_stats,
];
}
?><!DOCTYPE html>
<html lang="<?= htmlspecialchars($locale) ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GridTV — Health</title>
<link rel="stylesheet" href="/assets/fonts/fonts.css">
<style>
:root{--bg:#0b0d10;--surface:#13161b;--surface2:#1a1f27;--border:#2a3240;--accent:#e8c842;--accent2:#5ec0ff;--text:#eaf0f6;--muted:#8f9aac;--ok:#4fd18b;--warn:#ff6767}
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top left,#1b2330 0,#0b0d10 48%);color:var(--text);font-family:'Barlow Condensed',sans-serif}
.page{max-width:1200px;margin:0 auto;padding:32px 20px 56px}
.hero{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:24px}
.logo{font-family:'Share Tech Mono',monospace;font-size:26px;letter-spacing:.12em;color:var(--accent);text-transform:uppercase}
.subtitle{color:var(--muted);font-size:14px;max-width:700px;line-height:1.5}
.actions{display:flex;gap:10px;flex-wrap:wrap}
.btn{display:inline-flex;align-items:center;gap:8px;padding:11px 14px;border:1px solid var(--border);background:var(--surface);color:var(--text);text-decoration:none;font-size:13px;letter-spacing:.08em;text-transform:uppercase}
.btn-primary{background:var(--accent);color:#000;border-color:var(--accent)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:22px}
.card,.source{background:rgba(19,22,27,.95);border:1px solid var(--border)}
.card{padding:16px}
.eyebrow{font-family:'Share Tech Mono',monospace;font-size:11px;color:var(--accent2);letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px}
.value{font-size:28px;font-weight:700}
.small{font-size:13px;color:var(--muted)}
.checks{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-bottom:24px}
.check{padding:14px;background:var(--surface);border:1px solid var(--border)}
.status{display:inline-flex;align-items:center;gap:8px;font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase}
.status::before{content:'';width:10px;height:10px;border-radius:50%}
.status.ok{color:var(--ok)}.status.ok::before{background:var(--ok)}
.status.ko{color:var(--warn)}.status.ko::before{background:var(--warn)}
.source-list{display:grid;gap:18px}
.source{padding:20px}
.source-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;margin-bottom:16px}
.source-title{font-size:24px;font-weight:700}
.pill{display:inline-block;padding:6px 10px;font-family:'Share Tech Mono',monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase;background:var(--surface2);border:1px solid var(--border);color:var(--accent2)}
.split{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
.panel{padding:16px;background:var(--surface2);border:1px solid var(--border)}
.panel h3{margin:0 0 10px;font-size:16px;letter-spacing:.06em;text-transform:uppercase}
.kv{display:grid;grid-template-columns:160px 1fr;gap:8px;font-size:14px;line-height:1.45}
.kv div:nth-child(odd){color:var(--muted)}
.mono{font-family:'Share Tech Mono',monospace;font-size:12px;word-break:break-all}
@media (max-width:700px){.hero{flex-direction:column;align-items:flex-start}.kv{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="page">
<div class="hero">
<div>
<div class="logo">GridTV / Health</div>
<div class="subtitle">Page de diagnostic admin. Elle vérifie l’état de linstance, les flux XMLTV/M3U configurés et les signaux qui aident à comprendre vite ce qui coince.</div>
</div>
<div class="actions">
<a class="btn btn-primary" href="/export.php">Export 24h</a>
<a class="btn" href="/setup.php">Setup</a>
<a class="btn" href="/index.php">Guide</a>
</div>
</div>
<div class="grid">
<div class="card"><div class="eyebrow">Group</div><div class="value"><?= htmlspecialchars($config['group_name'] ?? 'GridTV') ?></div><div class="small">Configured TV group name</div></div>
<div class="card"><div class="eyebrow">Sources</div><div class="value"><?= count($sources) ?></div><div class="small">Configured XMLTV sources</div></div>
<div class="card"><div class="eyebrow">Locale</div><div class="value"><?= strtoupper(htmlspecialchars($locale)) ?></div><div class="small">Detected UI locale</div></div>
<div class="card"><div class="eyebrow">Config Size</div><div class="value"><?= $config_exists ? htmlspecialchars(gridtv_format_bytes(filesize($config_path))) : '0 B' ?></div><div class="small"><?= htmlspecialchars($config_path) ?></div></div>
</div>
<div class="checks">
<?php foreach ($checks as $check): ?>
<div class="check">
<div class="status <?= health_status_class($check['ok']) ?>"><?= health_status_label($check['ok']) ?></div>
<div style="font-size:18px;font-weight:700;margin:8px 0 6px"><?= htmlspecialchars($check['label']) ?></div>
<div class="small"><?= htmlspecialchars($check['detail']) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="source-list">
<?php foreach ($source_reports as $report): ?>
<section class="source">
<div class="source-head">
<div>
<div class="source-title"><?= htmlspecialchars($report['name']) ?></div>
<div class="small">EPG host: <?= htmlspecialchars(gridtv_extract_host($report['epg_url'])) ?: '—' ?><?= $report['m3u_url'] ? ' · M3U host: ' . htmlspecialchars(gridtv_extract_host($report['m3u_url'])) : '' ?></div>
</div>
<span class="pill"><?= htmlspecialchars($report['epg_fetch']['ok'] && $report['epg_stats']['ok'] ? 'Healthy XMLTV' : 'Check source') ?></span>
</div>
<div class="split">
<div class="panel">
<h3>XMLTV</h3>
<div class="kv">
<div>Status</div><div><span class="status <?= health_status_class($report['epg_fetch']['ok'] && $report['epg_stats']['ok']) ?>"><?= health_status_label($report['epg_fetch']['ok'] && $report['epg_stats']['ok']) ?></span></div>
<div>URL</div><div class="mono"><?= htmlspecialchars($report['epg_url']) ?></div>
<div>HTTP</div><div><?= htmlspecialchars((string) $report['epg_fetch']['status']) ?><?= $report['epg_fetch']['error'] ? ' · ' . htmlspecialchars($report['epg_fetch']['error']) : '' ?></div>
<div>Response time</div><div><?= htmlspecialchars(number_format($report['epg_fetch']['time_total'], 2)) ?> s</div>
<?php if ($report['epg_fetch']['ok'] && $report['epg_stats']['ok']): ?>
<div>Channels</div><div><?= htmlspecialchars((string) $report['epg_stats']['channels']) ?></div>
<div>Programmes</div><div><?= htmlspecialchars((string) $report['epg_stats']['programmes']) ?></div>
<div>Sub-titles</div><div><?= htmlspecialchars((string) $report['epg_stats']['subtitles']) ?></div>
<div>Ratings</div><div><?= htmlspecialchars((string) $report['epg_stats']['ratings']) ?></div>
<div>Categories</div><div><?= htmlspecialchars((string) $report['epg_stats']['categories']) ?></div>
<div>Date tags</div><div><?= htmlspecialchars((string) $report['epg_stats']['dates']) ?></div>
<div>Window</div><div><?= htmlspecialchars(health_format_xmltv_stamp($report['epg_stats']['first_start'])) ?> → <?= htmlspecialchars(health_format_xmltv_stamp($report['epg_stats']['last_stop'])) ?></div>
<div>Content-Type</div><div><?= htmlspecialchars($report['epg_fetch']['content_type'] ?: '—') ?></div>
<?php else: ?>
<div>Parse</div><div><?= htmlspecialchars($report['epg_stats']['error'] ?? 'Unable to read XMLTV') ?></div>
<?php endif; ?>
</div>
</div>
<div class="panel">
<h3>M3U</h3>
<?php if (!$report['m3u_url']): ?>
<div class="small">No M3U URL configured for this source.</div>
<?php else: ?>
<div class="kv">
<div>Status</div><div><span class="status <?= health_status_class((bool) ($report['m3u_fetch']['ok'] ?? false)) ?>"><?= health_status_label((bool) ($report['m3u_fetch']['ok'] ?? false)) ?></span></div>
<div>URL</div><div class="mono"><?= htmlspecialchars($report['m3u_url']) ?></div>
<div>HTTP</div><div><?= htmlspecialchars((string) ($report['m3u_fetch']['status'] ?? 0)) ?><?= !empty($report['m3u_fetch']['error']) ? ' · ' . htmlspecialchars($report['m3u_fetch']['error']) : '' ?></div>
<div>Response time</div><div><?= htmlspecialchars(number_format((float) ($report['m3u_fetch']['time_total'] ?? 0), 2)) ?> s</div>
<?php if (!empty($report['m3u_fetch']['ok']) && $report['m3u_stats']): ?>
<div>Streams</div><div><?= htmlspecialchars((string) $report['m3u_stats']['streams']) ?></div>
<div>#EXTINF</div><div><?= htmlspecialchars((string) $report['m3u_stats']['extinf']) ?></div>
<div>Content-Type</div><div><?= htmlspecialchars($report['m3u_fetch']['content_type'] ?: '—') ?></div>
<?php else: ?>
<div>Parse</div><div>Unable to load playlist.</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php endforeach; ?>
</div>
</div>
</body>
</html>