Add admin health page and printable 24h export

- add a protected health.php page for config, XMLTV and M3U diagnostics
- add shared admin/config helpers for authenticated internal tools
- add a styled 24h export page designed for PDF/print output
- add browser-side PNG export for the daily schedule
- update README with the new admin tools
This commit is contained in:
Johnnybegood90
2026-03-15 01:44:22 +01:00
parent c46ca14f4c
commit a81e33ccb8
6 changed files with 708 additions and 2 deletions
+29
View File
@@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/common.php';
function gridtv_require_admin(array $config, string $page_title = 'Admin'): array {
session_start();
$admin_key = (string) ($config['admin_key'] ?? '');
$session_key = $_SESSION['gridtv_admin_unlocked'] ?? '';
$error = '';
if ($admin_key !== '' && hash_equals($admin_key, (string) $session_key)) {
return ['unlocked' => true, 'error' => ''];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['admin_key_input'])) {
$submitted = trim((string) ($_POST['admin_key_input'] ?? ''));
if ($admin_key !== '' && hash_equals($admin_key, $submitted)) {
$_SESSION['gridtv_admin_unlocked'] = $admin_key;
header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
exit;
}
$error = 'Invalid admin key.';
}
http_response_code(401);
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>GridTV — ' . htmlspecialchars($page_title) . '</title><link rel="stylesheet" href="/assets/fonts/fonts.css"><style>:root{--bg:#0a0b0d;--surface:#111318;--surface2:#181b22;--border:#232733;--border-bright:#2e3444;--accent:#e8c842;--text:#c8cdd8;--text-dim:#5a6070;--text-bright:#eef0f5;--error:#ff4444;}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg);color:var(--text);font-family:"Barlow Condensed",sans-serif;padding:24px}.card{width:100%;max-width:460px;background:var(--surface);border:1px solid var(--border-bright);padding:34px}.logo{font-family:"Share Tech Mono",monospace;font-size:22px;color:var(--accent);letter-spacing:.1em;text-transform:uppercase;margin-bottom:8px}.sub{font-size:13px;color:var(--text-dim);line-height:1.5;margin-bottom:24px}.error{background:rgba(255,68,68,.08);border-left:3px solid var(--error);padding:12px 14px;color:var(--error);font-size:13px;margin-bottom:18px}label{display:block;font-family:"Share Tech Mono",monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px}input{width:100%;background:var(--surface2);border:1px solid var(--border-bright);color:var(--text-bright);font-family:"Share Tech Mono",monospace;font-size:13px;padding:11px 12px;margin-bottom:16px}button{width:100%;background:var(--accent);color:#000;border:none;font-family:"Barlow Condensed",sans-serif;font-size:14px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;padding:12px;cursor:pointer}.links{margin-top:16px;font-size:12px;color:var(--text-dim)}.links a{color:var(--accent);text-decoration:none}</style></head><body><div class="card"><div class="logo">GridTV</div><div class="sub">Enter your admin key to access the ' . htmlspecialchars($page_title) . ' page.</div>' . ($error ? '<div class="error">' . htmlspecialchars($error) . '</div>' : '') . '<form method="POST"><label for="admin_key_input">Admin key</label><input id="admin_key_input" type="password" name="admin_key_input" autocomplete="off" autofocus><button type="submit">Unlock</button></form><div class="links"><a href="/setup.php">Open setup</a></div></div></body></html>';
exit;
}
+127
View File
@@ -0,0 +1,127 @@
<?php
function gridtv_config_path(): string {
return dirname(__DIR__, 2) . '/config.json';
}
function gridtv_load_config(): array {
$config_path = gridtv_config_path();
if (!file_exists($config_path)) {
header('Location: /setup.php');
exit;
}
$config = json_decode(file_get_contents($config_path), true);
if (!is_array($config)) {
http_response_code(500);
die('<h2>GridTV — Configuration error</h2><p><code>config.json</code> is invalid or corrupted.<br>Please delete it and re-run <a href="/setup.php">setup</a>, or fix it manually via SSH.</p>');
}
if (isset($config['epg_url']) && !isset($config['epg_sources'])) {
$config['epg_sources'] = [[
'name' => 'Main',
'epg_url' => $config['epg_url'],
'm3u_url' => $config['m3u_url'] ?? '',
]];
$config['allow_personal_epg'] = false;
unset($config['epg_url'], $config['m3u_url']);
file_put_contents($config_path, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
return $config;
}
function gridtv_load_locale(array $config = []): array {
$locale_files = glob(dirname(__DIR__, 2) . '/locales/*.json') ?: [];
$supported_locales = array_map(fn($f) => basename($f, '.json'), $locale_files);
$locale = 'en';
$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
foreach (explode(',', $accept) as $lang) {
$code = strtolower(substr(trim($lang), 0, 2));
if (in_array($code, $supported_locales, true)) {
$locale = $code;
break;
}
}
$locale_path = dirname(__DIR__, 2) . "/locales/{$locale}.json";
$strings = is_file($locale_path) ? json_decode(file_get_contents($locale_path), true) : null;
if (!is_array($strings)) {
$fallback_path = dirname(__DIR__, 2) . '/locales/en.json';
$strings = is_file($fallback_path) ? (json_decode(file_get_contents($fallback_path), true) ?? []) : [];
$locale = 'en';
}
return [$locale, $strings];
}
function gridtv_fetch_url(string $url, int $timeout = 20, bool $head_only = false): array {
if (!function_exists('curl_init')) {
return [
'ok' => false,
'status' => 0,
'error' => 'php-curl extension is not installed.',
'body' => '',
'headers' => [],
'content_type' => '',
'final_url' => $url,
'time_total' => 0.0,
];
}
$headers = [];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'] ?? 'GridTV/health',
CURLOPT_HEADERFUNCTION => static function ($ch, $line) use (&$headers) {
$trimmed = trim($line);
if ($trimmed !== '' && strpos($trimmed, ':') !== false) {
[$name, $value] = explode(':', $trimmed, 2);
$headers[strtolower(trim($name))] = trim($value);
}
return strlen($line);
},
CURLOPT_HTTPHEADER => ['Accept: */*'],
CURLOPT_NOBODY => $head_only,
]);
$body = curl_exec($ch);
$error = curl_error($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$content_type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$final_url = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$time_total = (float) curl_getinfo($ch, CURLINFO_TOTAL_TIME);
curl_close($ch);
return [
'ok' => $error === '' && $status >= 200 && $status < 400,
'status' => $status,
'error' => $error,
'body' => is_string($body) ? $body : '',
'headers' => $headers,
'content_type' => $content_type,
'final_url' => $final_url ?: $url,
'time_total' => $time_total,
];
}
function gridtv_format_bytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$size = (float) $bytes;
$unit = 0;
while ($size >= 1024 && $unit < count($units) - 1) {
$size /= 1024;
$unit++;
}
return number_format($size, $size >= 10 || $unit === 0 ? 0 : 1) . ' ' . $units[$unit];
}
function gridtv_extract_host(string $url): string {
return strtolower(parse_url($url, PHP_URL_HOST) ?? '');
}