Add server-side health checks for service online status
- update-health.js: pings every service URL from script.js (single source of truth) in parallel every minute via cron and writes health.json; any HTTP response below 500 counts as online, 5xx as error, timeouts/DNS failures as offline; TLS validity ignored - Card badges now show real state instead of a static Online label: grey Checking, green Online (with latency tooltip), amber Error (HTTP code), red Offline (reason); refreshed every 30 s - Entrypoint runs a first sweep at boot; nginx serves /health.json with no-cache; README documents the behavior
This commit is contained in:
@@ -424,7 +424,10 @@ function cardTemplate(s, idx) {
|
||||
style="--cat:${meta.color}; animation-delay:${Math.min(idx * 35, 400)}ms">
|
||||
<div class="card-head">
|
||||
<div class="card-icon" aria-hidden="true">${s.icon || "🔗"}</div>
|
||||
<span class="status"><span class="dot"></span>Online</span>
|
||||
<span class="status" data-status-url="${escapeHtml(s.url)}">
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<span class="status-text">Checking…</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<span class="card-cat">${escapeHtml(meta.label)}</span>
|
||||
@@ -555,6 +558,51 @@ function bindOpenWebUIChat() {
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service health badges (server-side checks via health.json)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function applyHealth(health) {
|
||||
const services = health?.services || {};
|
||||
document.querySelectorAll(".status[data-status-url]").forEach((badge) => {
|
||||
const url = badge.getAttribute("data-status-url");
|
||||
const h = services[url];
|
||||
const text = badge.querySelector(".status-text");
|
||||
if (!text) return;
|
||||
|
||||
if (!h) {
|
||||
badge.className = "status unknown";
|
||||
text.textContent = "Unknown";
|
||||
badge.title = "No health data yet";
|
||||
return;
|
||||
}
|
||||
|
||||
if (h.status === "online") {
|
||||
badge.className = "status online";
|
||||
text.textContent = "Online";
|
||||
badge.title = h.latencyMs != null ? `${h.latencyMs} ms` : "";
|
||||
} else if (h.status === "error") {
|
||||
badge.className = "status error";
|
||||
text.textContent = "Error";
|
||||
badge.title = `HTTP ${h.code}`;
|
||||
} else {
|
||||
badge.className = "status offline";
|
||||
text.textContent = "Offline";
|
||||
badge.title = h.error || "Unreachable";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadHealth() {
|
||||
try {
|
||||
const res = await fetch("health.json", { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
applyHealth(await res.json());
|
||||
} catch {
|
||||
// keep the current badges; the next poll will retry
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search / filter
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -614,3 +662,5 @@ loadGpuStats();
|
||||
setInterval(loadGpuStats, 5000); // live GPU stats every 5 s
|
||||
loadOllamaStats();
|
||||
setInterval(loadOllamaStats, 5000); // live Ollama loaded models every 5 s
|
||||
loadHealth();
|
||||
setInterval(loadHealth, 30000); // service health badges every 30 s
|
||||
|
||||
Reference in New Issue
Block a user