diff --git a/README.md b/README.md
index 968972d..c01edc9 100644
--- a/README.md
+++ b/README.md
@@ -307,6 +307,22 @@ http://guide.your-domain.com/health.php
If you enable demo mode with a `.demo` file, visitors can open the page without the key.
+For monitoring tools such as Uptime Kuma, GridTV also exposes a lightweight probe endpoint:
+
+```text
+http://guide.your-domain.com/health.php?format=probe
+```
+
+- returns HTTP `200` when the instance looks healthy
+- returns HTTP `503` when config, PHP extensions, or the first XMLTV source fail
+- returns compact JSON suitable for external monitoring
+
+If you prefer a plain text response:
+
+```text
+http://guide.your-domain.com/health.php?format=probe&plain=1
+```
+
---
diff --git a/health.php b/health.php
index 66d68b7..7e95ee8 100644
--- a/health.php
+++ b/health.php
@@ -1,10 +1,6 @@
format('d/m/Y H:i');
}
+function health_probe_payload(): array {
+ $config_path = gridtv_config_path();
+ $config_exists = file_exists($config_path);
+ $config_valid = false;
+ $config = [];
+
+ if ($config_exists) {
+ $decoded = json_decode(file_get_contents($config_path), true);
+ if (is_array($decoded)) {
+ $config = $decoded;
+ $config_valid = true;
+ }
+ }
+
+ $sources = $config_valid ? array_values($config['epg_sources'] ?? []) : [];
+ $first_source = $sources[0] ?? null;
+
+ $checks = [
+ 'config_exists' => $config_exists,
+ 'config_valid' => $config_valid,
+ 'php_curl' => extension_loaded('curl'),
+ 'php_xml' => class_exists('DOMDocument'),
+ 'source_configured' => !empty($sources),
+ ];
+
+ $details = [
+ 'group_name' => $config['group_name'] ?? 'GridTV',
+ 'source_count' => count($sources),
+ ];
+
+ if ($first_source && !empty($first_source['epg_url'])) {
+ $fetch = gridtv_fetch_url((string) $first_source['epg_url'], 12);
+ $checks['xmltv_reachable'] = $fetch['ok'];
+ $details['xmltv_status'] = $fetch['status'];
+ $details['xmltv_time_total'] = round((float) $fetch['time_total'], 3);
+ if ($fetch['ok']) {
+ $stats = health_parse_xmltv($fetch['body']);
+ $checks['xmltv_valid'] = !empty($stats['ok']);
+ if (!empty($stats['ok'])) {
+ $details['xmltv_channels'] = $stats['channels'];
+ $details['xmltv_programmes'] = $stats['programmes'];
+ } else {
+ $details['xmltv_error'] = $stats['error'] ?? 'Invalid XMLTV';
+ }
+ } else {
+ $details['xmltv_error'] = $fetch['error'] ?: ('HTTP ' . $fetch['status']);
+ }
+ } else {
+ $checks['xmltv_reachable'] = false;
+ $details['xmltv_error'] = 'No XMLTV source configured';
+ }
+
+ $ok = !in_array(false, $checks, true);
+
+ return [
+ 'ok' => $ok,
+ 'status' => $ok ? 'ok' : 'degraded',
+ 'timestamp' => gmdate('c'),
+ 'checks' => $checks,
+ 'details' => $details,
+ ];
+}
+
function health_build_payload(array $config, string $locale): array {
$config_path = gridtv_config_path();
$config_exists = file_exists($config_path);
@@ -164,6 +223,23 @@ function health_build_payload(array $config, string $locale): array {
];
}
+if (isset($_GET['format']) && $_GET['format'] === 'probe') {
+ $payload = health_probe_payload();
+ http_response_code($payload['ok'] ? 200 : 503);
+ if (isset($_GET['plain']) && $_GET['plain'] === '1') {
+ header('Content-Type: text/plain; charset=UTF-8');
+ echo $payload['ok'] ? 'OK' : 'KO';
+ } else {
+ header('Content-Type: application/json; charset=UTF-8');
+ echo json_encode($payload, JSON_UNESCAPED_SLASHES);
+ }
+ exit;
+}
+
+$config = gridtv_load_config();
+gridtv_require_admin($config, 'Health');
+[$locale, $L] = gridtv_load_locale($config);
+
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);
@@ -184,6 +260,12 @@ if (isset($_GET['format']) && $_GET['format'] === 'json') {
.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)}
+.probe-box{margin-bottom:20px;padding:18px 20px;background:rgba(19,22,27,.95);border:1px solid var(--border)}
+.probe-box h2{margin:0 0 10px;font-size:18px;letter-spacing:.06em;text-transform:uppercase}
+.probe-box p{margin:0 0 12px;color:var(--muted);font-size:13px;line-height:1.5}
+.probe-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
+.probe-item{padding:12px;background:var(--surface2);border:1px solid var(--border)}
+.probe-item .eyebrow{margin-bottom:6px}
.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)}
@@ -218,6 +300,21 @@ if (isset($_GET['format']) && $_GET['format'] === 'json') {
+ Use these public endpoints with Uptime Kuma or any HTTP monitoring tool. They return Monitoring probe
+ 200 when the instance is healthy and 503 when a critical check fails.