From 044546ad00af7ab760e9dc4dad68e646b399a23e Mon Sep 17 00:00:00 2001 From: Fabian Schieder Date: Sun, 1 Mar 2026 14:52:49 +0100 Subject: [PATCH] Implement async server status badge updates with a new JSON endpoint and improved caching --- api/server_status.php | 56 ++++++++++++++++++++++++++++++++++++++++++ assets/js/main.js | 57 +++++++++++++++---------------------------- index.php | 54 +--------------------------------------- 3 files changed, 76 insertions(+), 91 deletions(-) create mode 100644 api/server_status.php diff --git a/api/server_status.php b/api/server_status.php new file mode 100644 index 0000000..9c616e4 --- /dev/null +++ b/api/server_status.php @@ -0,0 +1,56 @@ += 0 && $age <= $cacheTtlSeconds) { + $serverStatus = $decoded['data']; + } + } + } +} + +if (!is_array($serverStatus)) { + $serverStatus = build_server_status($serverStatusTargets); + @file_put_contents($cacheFile, json_encode(['ts' => time(), 'data' => $serverStatus], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); +} + +$byUrlNormalized = []; +foreach ($serverStatus as $svc) { + if (!empty($svc['url']) && is_string($svc['url'])) { + $byUrlNormalized[rtrim($svc['url'], '/')] = $svc; + } +} + +$payload = json_encode([ + 'ts' => time(), + 'byUrlNormalized' => $byUrlNormalized, +], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + +if ($payload === false) { + http_response_code(500); + $payload = '{"error":"json_encode_failed"}'; +} + +echo $payload; + diff --git a/assets/js/main.js b/assets/js/main.js index 5c00330..24383d0 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -94,41 +94,37 @@ document.querySelectorAll('.card').forEach(card => { }); }); -// ── Server Status Badges (async) ─────────────────────────── +// ── Server Status Badges (async, no reload) ─────────────── (async function () { const cards = Array.from(document.querySelectorAll('.card[data-status-url]')); if (cards.length === 0) return; - const endpoint = './server_status.php'; + const endpoint = '/api/server_status.php'; const normalize = (url) => { if (!url) return ''; return String(url).replace(/\/+$/, ''); }; - const parsePossiblyUtf16Json = (text) => { - if (!text) return null; - const cleaned = String(text) - .replace(/^?\uFEFF/, '') + const safeJsonParse = (text) => { + const cleaned = String(text || '') + .replace(/^\uFEFF/, '') .replace(/\u0000/g, '') .trim(); + if (!cleaned) return null; return JSON.parse(cleaned); }; - const applyPayload = (payload) => { - const byUrl = payload && payload.byUrl ? payload.byUrl : {}; - const byUrlNormalized = payload && payload.byUrlNormalized ? payload.byUrlNormalized : {}; - - let applied = 0; + const apply = (byUrl) => { for (const card of cards) { const key = normalize(card.getAttribute('data-status-url')); - const svc = byUrl[key] || byUrl[key + '/'] || byUrlNormalized[key] || null; + const svc = byUrl[key] || byUrl[key + '/'] || null; if (!svc) continue; - const state = svc.state || 'unknown'; const badge = card.querySelector('[data-status-badge="1"]'); if (!badge) continue; + const state = svc.state || 'unknown'; badge.classList.remove('status-badge--unknown', 'status-badge--up', 'status-badge--down'); badge.classList.add('status-badge--' + state); @@ -142,37 +138,22 @@ document.querySelectorAll('.card').forEach(card => { badge.textContent = 'Unbekannt'; badge.title = svc.detail || 'Unbekannt'; } - applied++; } - - console.debug('[server-status] applied badges:', applied); }; - const fetchAndApply = async () => { + // Exactly ~1s delay, then fetch. + await new Promise(r => setTimeout(r, 1000)); + + try { const res = await fetch(endpoint, { cache: 'no-store' }); - if (!res.ok) throw new Error('status_fetch_failed_' + res.status); + if (!res.ok) return; const text = await res.text(); - let payload; - try { - payload = parsePossiblyUtf16Json(text); - } catch (e) { - console.warn('[server-status] JSON parse failed, first 120 chars:', text.slice(0, 120)); - throw e; - } - if (!payload) throw new Error('status_json_empty'); + const payload = safeJsonParse(text); + if (!payload || !payload.byUrlNormalized) return; - applyPayload(payload); - }; - - const delays = [1000, 1000, 1500]; - for (const d of delays) { - await new Promise(r => setTimeout(r, d)); - try { - await fetchAndApply(); - break; - } catch (e) { - console.warn('[server-status] update failed:', e); - } + apply(payload.byUrlNormalized); + } catch (e) { + // keep "Unbekannt" } })(); diff --git a/index.php b/index.php index 16d001b..2bf4e91 100644 --- a/index.php +++ b/index.php @@ -15,65 +15,13 @@ require_once __DIR__ . '/includes/lib/server_status.php'; require_once __DIR__ . '/includes/lib/view_helpers.php'; // ── Serverstatus ───────────────────────────────────────────────────────── -// Performance: Beim ersten Rendern sofort antworten (=> Unbekannt). -// Nach ~1s lädt die Seite einmal neu mit ?status=1 und rendert dann serverseitig Online/Offline. -$shouldComputeStatus = isset($_GET['status']) && $_GET['status'] === '1'; - +// Performance: initial sofort rendern (=> Unbekannt). Badges werden clientseitig nachgeladen. $serverStatusByUrl = []; -if ($shouldComputeStatus) { - $cacheTtlSeconds = 30; - $cacheKey = 'server_status_v5'; - $cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json'; - - $serverStatus = null; - if (is_file($cacheFile)) { - $raw = @file_get_contents($cacheFile); - if ($raw !== false) { - $decoded = json_decode($raw, true); - if (is_array($decoded) && isset($decoded['ts'], $decoded['data']) && is_array($decoded['data'])) { - $age = time() - (int)$decoded['ts']; - if ($age >= 0 && $age <= $cacheTtlSeconds) { - $serverStatus = $decoded['data']; - } - } - } - } - - if (!is_array($serverStatus)) { - $serverStatus = build_server_status($serverStatusTargets); - @file_put_contents($cacheFile, json_encode(['ts' => time(), 'data' => $serverStatus], JSON_UNESCAPED_SLASHES)); - } - - foreach ($serverStatus as $svc) { - if (!empty($svc['url']) && is_string($svc['url'])) { - $serverStatusByUrl[$svc['url']] = $svc; - } - } -} require __DIR__ . '/includes/views/layout_head.php'; require __DIR__ . '/includes/views/header.php'; ?> - - - -
$items): ?>