Add custom homelab dashboard stack

This commit is contained in:
2026-04-05 13:43:03 +02:00
parent 450a04a7d3
commit 89b9173c25
38 changed files with 3539 additions and 0 deletions
@@ -0,0 +1,90 @@
function serviceTone(status) {
if (status === "offline") return "offline";
if (status === "degraded") return "warning";
return "online";
}
function healthLabel(status) {
if (status === "offline") return "Offline";
if (status === "degraded") return "Degraded";
return "Healthy";
}
function sourceLabel(source) {
if (source === "home_assistant") return "Core Automation Hub";
if (source === "uptime_kuma") return "External availability and latency surface.";
if (source === "docker") return "Container runtime state without external monitor data.";
return "Service state from aggregator.";
}
function formatTimestamp(value) {
if (!value) return "n/a";
const date = new Date(value);
return new Intl.DateTimeFormat("de-DE", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(date);
}
export function renderServices(state) {
const { services, overview } = state.data;
const dockerPill = document.getElementById("docker-summary-pill");
dockerPill.className = `status-pill ${services.summary.docker.source_status === "online" ? "online" : "offline"}`;
dockerPill.textContent = services.summary.docker.source_status === "online" ? "Online" : "Offline";
document.getElementById("docker-running").textContent = String(services.summary.docker.running);
document.getElementById("docker-stopped").textContent = String(services.summary.docker.stopped);
document.getElementById("docker-unhealthy").textContent = String(services.summary.docker.unhealthy);
const kumaPill = document.getElementById("kuma-summary-pill");
kumaPill.className = `status-pill ${services.summary.uptime_kuma.source_status === "online" ? "online" : "offline"}`;
kumaPill.textContent = services.summary.uptime_kuma.source_status === "online" ? "Synced" : "Offline";
document.getElementById("kuma-up").textContent = String(services.summary.uptime_kuma.monitors_up);
document.getElementById("kuma-down").textContent = String(services.summary.uptime_kuma.monitors_down);
document.getElementById("kuma-paused").textContent = String(services.summary.uptime_kuma.monitors_paused);
const ha = services.services.find((service) => service.id === "homeassistant");
if (ha) {
const haPill = document.getElementById("service-ha-pill");
const tone = serviceTone(ha.status);
haPill.className = `status-pill ${tone}`;
haPill.textContent = ha.status === "online" ? "Reachable" : ha.status === "degraded" ? "Degraded" : "Offline";
document.getElementById("service-ha-version").textContent = overview.home_assistant.version ?? "unknown";
document.getElementById("service-ha-version").className = "info";
document.getElementById("service-ha-latency").textContent = ha.latency_ms != null ? `${ha.latency_ms} MS` : "N/A";
document.getElementById("service-ha-latency").className = tone === "offline" ? "offline" : tone === "warning" ? "warning" : "online";
document.getElementById("service-ha-last-check").textContent = formatTimestamp(ha.last_checked);
}
const dynamicServices = services.services.filter((service) => service.id !== "homeassistant").slice(0, 3);
const existingFallbacks = [
document.getElementById("service-card-fallback-1"),
document.getElementById("service-card-fallback-2"),
document.getElementById("service-card-fallback-3"),
];
dynamicServices.forEach((service, index) => {
const node = existingFallbacks[index];
if (!node) return;
node.style.display = "";
const tone = serviceTone(service.status);
const pillClass = tone === "warning" ? "warning" : tone === "offline" ? "offline" : "online";
node.querySelector(".card-label").textContent = service.name;
node.querySelector(".status-pill").className = `status-pill ${pillClass}`;
node.querySelector(".status-pill").textContent = healthLabel(service.status);
node.querySelector(".card-title").textContent = service.name === "Immich" ? "Photo Pipeline" : service.name === "Gitea" ? "Git Platform" : `${service.name} Service`;
node.querySelector(".card-copy").textContent = sourceLabel(service.source);
const rows = node.querySelectorAll(".service-meta-row strong");
rows[0].textContent = service.latency_ms != null ? `${service.latency_ms} MS` : "N/A";
rows[0].className = tone === "offline" ? "offline" : tone === "warning" ? "warning" : "online";
rows[1].textContent = String(service.docker_state).toUpperCase();
rows[1].className = service.docker_state === "stopped" ? "offline" : service.docker_state === "unhealthy" ? "warning" : "online";
});
for (let index = dynamicServices.length; index < existingFallbacks.length; index += 1) {
existingFallbacks[index].style.display = "none";
}
}