From 3f8aab5e70ad8ac8457594a755b72a8902e0cd03 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Sun, 15 Mar 2026 02:08:22 +0100 Subject: [PATCH] Fix: Error 500 on export. Fix: Show health page immediately then load the diag data. --- README.md | 8 +- export.php | 9 +- health.php | 362 ++++++++++++++++++++++++++++++--------------------- version.json | 2 +- 4 files changed, 222 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index e13a9d3..91d1996 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This demo runs with sample XMLTV feeds to showcase the interface. ### Requirements -- A web server running **PHP 8.0+** with **php-curl** extension +- A web server running **PHP 8.0+** with **php-curl** and **php-xml** extensions - **Apache** or **Nginx** — or just use **Docker** - An **EPG source in XMLTV format** (e.g. Tunarr, Jellyfin, xTeVe...) - *(Optional)* An **M3U playlist** — required for the built-in player @@ -100,13 +100,13 @@ chmod 775 /var/www/gridtv chown -R www-data:www-data /var/www/gridtv ``` -#### 3. Install php-curl +#### 3. Install php-curl + php-xml -The built-in player uses `proxy.php` to relay HTTP streams over HTTPS. This requires the **php-curl** extension: +The built-in player uses `proxy.php` to relay HTTP streams over HTTPS, and the admin tools parse XMLTV directly. This requires **php-curl** and **php-xml**: ```bash # Debian/Ubuntu — adjust version to match your PHP -apt install php8.4-curl +apt install php8.4-curl php8.4-xml systemctl reload apache2 # or: systemctl reload nginx ``` diff --git a/export.php b/export.php index 8a8fe41..6f3a54c 100644 --- a/export.php +++ b/export.php @@ -123,10 +123,13 @@ $sources = array_values($config['epg_sources'] ?? []); $source_index = max(0, min((int) ($_GET['source'] ?? 0), max(count($sources) - 1, 0))); $source = $sources[$source_index] ?? null; $target_day = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) ($_GET['day'] ?? '')) ? (string) $_GET['day'] : (new DateTimeImmutable('today'))->format('Y-m-d'); -$layout = in_array((string) ($_GET['layout'] ?? 'cards'), ['cards', 'timeline'], true) ? (string) $_GET['layout'] : 'cards'; -$limit = in_array((string) ($_GET['limit'] ?? '8'), ['4', '8', 'all'], true) ? (string) $_GET['limit'] : '8'; +$layout_input = isset($_GET['layout']) ? (string) $_GET['layout'] : 'cards'; +$limit_input = isset($_GET['limit']) ? (string) $_GET['limit'] : '8'; +$window_input = isset($_GET['window']) ? (string) $_GET['window'] : 'full'; +$layout = in_array($layout_input, ['cards', 'timeline'], true) ? $layout_input : 'cards'; +$limit = in_array($limit_input, ['4', '8', 'all'], true) ? $limit_input : '8'; $windows = export_window_definitions($target_day); -$window_key = array_key_exists((string) ($_GET['window'] ?? 'full'), $windows) ? (string) $_GET['window'] : 'full'; +$window_key = array_key_exists($window_input, $windows) ? $window_input : 'full'; $window = $windows[$window_key]; $slots = export_hour_slots($window['start'], $window['end']->modify('+1 second')); diff --git a/health.php b/health.php index 786790d..66d68b7 100644 --- a/health.php +++ b/health.php @@ -9,10 +9,6 @@ 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.']; @@ -84,64 +80,95 @@ function health_format_xmltv_stamp(?string $stamp): string { 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'] ?? []); +function health_build_payload(array $config, string $locale): array { + $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', - ], -]; + $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' => 'php-xml / DOM', + 'ok' => class_exists('DOMDocument'), + 'detail' => class_exists('DOMDocument') ? 'Extension loaded' : 'Missing php-xml / DOM', + ], + [ + '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'] ?? '')); + $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']]; + $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; + $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_host' => gridtv_extract_host($epg_url), + 'm3u_host' => gridtv_extract_host($m3u_url), + 'epg_fetch' => $epg_fetch, + 'epg_stats' => $epg_stats, + 'm3u_fetch' => $m3u_fetch, + 'm3u_stats' => $m3u_stats, + ]; } - $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, + return [ + 'summary' => [ + 'group_name' => $config['group_name'] ?? 'GridTV', + 'source_count' => count($sources), + 'locale' => strtoupper($locale), + 'config_size' => $config_exists ? gridtv_format_bytes((int) filesize($config_path)) : '0 B', + 'config_path' => $config_path, + ], + 'checks' => $checks, + 'sources' => $source_reports, + 'helpers' => [ + 'status_ok' => 'OK', + 'status_bad' => 'Issue', + ], ]; } + +if (isset($_GET['format']) && $_GET['format'] === 'json') { + header('Content-Type: application/json; charset=UTF-8'); + echo json_encode(health_build_payload($config, $locale), JSON_UNESCAPED_SLASHES); + exit; +} ?> @@ -156,33 +183,17 @@ foreach ($sources as $index => $source) { .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}} +.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,.checks,.source-list{display:grid;gap:14px}.grid{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin-bottom:22px}.checks{grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin-bottom:24px}.source-list{gap:18px} +.card,.source,.check{background:rgba(19,22,27,.95);border:1px solid var(--border)}.card,.check,.source{padding:16px}.source{padding:20px} +.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)} +.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-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} +.skeleton{position:relative;overflow:hidden}.skeleton::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent);transform:translateX(-100%);animation:shine 1.5s infinite}.skeleton-line{height:16px;background:#1c2430;margin-top:8px}.skeleton-line.short{width:42%}.splash{display:flex;align-items:center;gap:14px;padding:18px 20px;margin-bottom:20px;background:rgba(19,22,27,.95);border:1px solid var(--border)}.spinner{width:22px;height:22px;border:3px solid rgba(94,192,255,.18);border-top-color:var(--accent2);border-radius:50%;animation:spin 1s linear infinite} +.hidden{display:none!important} +@keyframes spin{to{transform:rotate(360deg)}}@keyframes shine{100%{transform:translateX(100%)}}@media (max-width:700px){.hero{flex-direction:column;align-items:flex-start}.kv{grid-template-columns:1fr}} @@ -190,7 +201,7 @@ foreach ($sources as $index => $source) {
-
Page de diagnostic admin. Elle vérifie l’état de l’instance, les flux XMLTV/M3U configurés et les signaux qui aident à comprendre vite ce qui coince.
+
Page de diagnostic admin. L’écran apparaît tout de suite, puis les checks XMLTV/M3U se remplissent en arrière-plan.
Export 24h @@ -199,79 +210,128 @@ foreach ($sources as $index => $source) {
-
-
Group
Configured TV group name
-
Sources
Configured XMLTV sources
-
Locale
Detected UI locale
-
Config Size
-
- -
- -
-
-
-
+
+
+
+
Loading diagnostics…
+
Configuration, XMLTV et M3U sont testés en arrière-plan.
-
-
- -
-
-
-
-
EPG host:
-
- -
-
-
-

XMLTV

-
-
Status
-
URL
-
HTTP
-
Response time
s
- -
Channels
-
Programmes
-
Sub-titles
-
Ratings
-
Categories
-
Date tags
-
Window
-
Content-Type
- -
Parse
- -
-
-
-

M3U

- -
No M3U URL configured for this source.
- -
-
Status
-
URL
-
HTTP
-
Response time
s
- -
Streams
-
#EXTINF
-
Content-Type
- -
Parse
Unable to load playlist.
- -
- -
-
-
- +
+
Group
+
Sources
+
Locale
+
Config Size
+
+ +
+
+
+
+
+ +
+
+ + diff --git a/version.json b/version.json index 08df5a2..d5d8ca8 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "1.5.0" + "version": "1.6.0" }