Implement async server status badge updates with a new JSON endpoint and improved caching
This commit is contained in:
parent
bcea63dcb8
commit
044546ad00
56
api/server_status.php
Normal file
56
api/server_status.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// JSON endpoint for async status badge updates.
|
||||
// Important: output must be clean UTF-8 JSON.
|
||||
|
||||
require_once __DIR__ . '/../includes/lib/server_status.php';
|
||||
$serverStatusTargets = require __DIR__ . '/../includes/config/server_status_targets.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store, max-age=0');
|
||||
|
||||
$cacheTtlSeconds = 30;
|
||||
$cacheKey = 'server_status_api_v1';
|
||||
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json';
|
||||
|
||||
$noCache = isset($_GET['nocache']) && $_GET['nocache'] === '1';
|
||||
|
||||
$serverStatus = null;
|
||||
if (!$noCache && 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 | 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;
|
||||
|
||||
@ -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"
|
||||
}
|
||||
})();
|
||||
|
||||
54
index.php
54
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';
|
||||
?>
|
||||
|
||||
<?php if (!$shouldComputeStatus): ?>
|
||||
<script>
|
||||
// Einmaliger Reload nach ~1 Sekunde, um dann serverseitig den Status zu laden.
|
||||
setTimeout(function () {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.get('status') === '1') return;
|
||||
url.searchParams.set('status', '1');
|
||||
window.location.replace(url.toString());
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
if (window.location.search.indexOf('status=1') === -1) {
|
||||
window.location.replace(window.location.pathname + '?status=1');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<main>
|
||||
<?php foreach ($projects as $category => $items): ?>
|
||||
<?php require __DIR__ . '/includes/views/projects_section.php'; ?>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user