{% raw %}#!/usr/bin/env python3 # Managed by Ansible — do not edit manually # Status dashboard for cloud.ladkau.de # Reads host system stats from /host/proc (CPU, RAM) and /host (disk), # checks service endpoints, and serves a status page on port 8080. import os import time import threading import urllib.request import urllib.error from http.server import HTTPServer, BaseHTTPRequestHandler PROC = "/host/proc" HOST = "/host" BG_IMAGE = "/app/bg.jpg" {% endraw %} SERVICES = [ ("Gitea", "https://{{ domain_gitea }}", 200, "https://{{ domain_gitea }}"), ("Nextcloud", "https://{{ domain_nextcloud }}", 200, "https://{{ domain_nextcloud }}"), ("Keycloak", "https://{{ domain_sso }}/realms/master", 200, "https://{{ domain_sso }}"), ("Roundcube", "https://{{ domain_mail }}", 200, "https://{{ domain_mail }}"), ("Registry", "https://{{ domain_registry }}/v2/", 401, "https://{{ domain_registry }}"), ("k8s", "https://{{ domain_k8s }}", 200, "https://{{ domain_k8s }}"), ("Vaultwarden", "https://{{ domain_vault }}", 200, "https://{{ domain_vault }}"), ("Downloads", "https://{{ domain_dl }}", 200, "https://{{ domain_dl }}"), ] {% raw %} # ── Colour palette ─────────────────────────────────────────────────────────── BG = "#0f172a" CARD = "#1e293b" MUTED = "#64748b" TEXT = "#e2e8f0" SUB = "#94a3b8" GREEN = "#22c55e" AMBER = "#f59e0b" RED = "#ef4444" # ── System stats ───────────────────────────────────────────────────────────── _lock = threading.Lock() _cache = {"cpu": 0.0, "ram_pct": 0.0, "ram_used_gb": 0.0, "ram_total_gb": 0.0, "disk_pct": 0.0, "disk_used_gb": 0.0, "disk_total_gb": 0.0, "services": [], "updated": 0} def _cpu_sample(): with open(PROC + "/stat") as f: parts = list(map(int, f.readline().split()[1:8])) # user nice system idle iowait irq softirq idle = parts[3] + parts[4] total = sum(parts) return idle, total def read_cpu(): a = _cpu_sample() time.sleep(1) b = _cpu_sample() dt = b[1] - a[1] di = b[0] - a[0] return round(100.0 * (1.0 - di / dt), 1) if dt else 0.0 def read_ram(): mem = {} with open(PROC + "/meminfo") as f: for line in f: k, v = line.split(":") mem[k.strip()] = int(v.split()[0]) # kB total_kb = mem["MemTotal"] avail_kb = mem.get("MemAvailable", mem.get("MemFree", 0)) used_kb = total_kb - avail_kb pct = round(100.0 * used_kb / total_kb, 1) if total_kb else 0.0 return pct, round(used_kb / 1048576, 1), round(total_kb / 1048576, 1) def read_disk(): st = os.statvfs(HOST) total = st.f_blocks * st.f_frsize free = st.f_bfree * st.f_frsize used = total - free pct = round(100.0 * used / total, 1) if total else 0.0 gb = 1024 ** 3 return pct, round(used / gb, 1), round(total / gb, 1) def check_services(): results = [] for name, url, expected, link in SERVICES: ok = False try: req = urllib.request.Request(url, headers={"User-Agent": "status-dashboard/1.0"}) with urllib.request.urlopen(req, timeout=5) as r: ok = r.status == expected except urllib.error.HTTPError as e: ok = e.code == expected except Exception: ok = False results.append({"name": name, "ok": ok, "link": link}) return results def collect_loop(): while True: try: cpu = read_cpu() # includes 1 s sleep ram_pct, ram_used, ram_total = read_ram() disk_pct, disk_used, disk_total = read_disk() services = check_services() with _lock: _cache.update({ "cpu": cpu, "ram_pct": ram_pct, "ram_used_gb": ram_used, "ram_total_gb": ram_total, "disk_pct": disk_pct, "disk_used_gb": disk_used, "disk_total_gb": disk_total, "services": services, "updated": time.time(), }) except Exception as e: print("collect error:", e, flush=True) time.sleep(29) # 29 s rest + 1 s CPU sample = ~30 s cycle # ── HTML rendering ─────────────────────────────────────────────────────────── def _bar_color(pct): if pct < 60: return GREEN if pct < 85: return AMBER return RED def _service_dot(svc): c = GREEN if svc["ok"] else RED return ( '
' '
' '' + svc["name"] + '' '
' ) def _stat_bar(label, pct, detail): bc = _bar_color(pct) pct_str = str(pct) detail_html = (' ' + detail + '') if detail else '' return ( '
' '
' '' + label + '' '' + pct_str + '%' + detail_html + '' '
' '
' '
' '
' '
' ) CSS = ( '*{box-sizing:border-box;margin:0;padding:0}' 'body{font-family:system-ui,sans-serif;background:' + BG + ' url(/bg.jpg) center/cover no-repeat fixed;' 'color:' + TEXT + ';min-height:100vh;padding:2rem}' 'h1{font-size:1.1rem;font-weight:600;color:' + TEXT + ';margin-bottom:2rem;letter-spacing:.06em;' 'text-transform:uppercase;text-shadow:0 1px 4px rgba(0,0,0,.6)}' '.grid{display:grid;gap:1.5rem;grid-template-columns:1fr 1fr;max-width:820px}' '@media(max-width:600px){.grid{grid-template-columns:1fr}}' '.card{background:rgba(15,23,42,.75);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);' 'border-radius:.75rem;padding:1.5rem;border:1px solid rgba(255,255,255,.08)}' '.card h2{font-size:.7rem;font-weight:600;color:' + MUTED + ';text-transform:uppercase;letter-spacing:.1em;margin-bottom:1.25rem}' '.footer{margin-top:2rem;font-size:.75rem;color:rgba(148,163,184,.7);max-width:820px;text-align:right;' 'text-shadow:0 1px 2px rgba(0,0,0,.5)}' ) def render(): with _lock: s = dict(_cache) svc_html = "".join(_service_dot(svc) for svc in s["services"]) \ or 'Checking…' stats_html = ( _stat_bar("CPU", s["cpu"], "") + _stat_bar("RAM", s["ram_pct"], str(s["ram_used_gb"]) + " / " + str(s["ram_total_gb"]) + " GB") + _stat_bar("Disk", s["disk_pct"], str(int(s["disk_used_gb"])) + " / " + str(int(s["disk_total_gb"])) + " GB") ) updated = time.strftime("%H:%M:%S", time.localtime(s["updated"])) if s["updated"] else "—" return ( '' '' '' '' '' 'cloud.ladkau.de' '' '' '' '' '

cloud.ladkau.de

' '
' '

Services

' + svc_html + '
' '

System

' + stats_html + '
' '
' '' '' '' ) # ── HTTP server ─────────────────────────────────────────────────────────────── class _Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/bg.jpg": try: with open(BG_IMAGE, "rb") as f: body = f.read() self.send_response(200) self.send_header("Content-Type", "image/jpeg") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "max-age=3600") self.end_headers() self.wfile.write(body) except OSError: self.send_error(404) return body = render().encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *_args): pass # suppress per-request log noise if __name__ == "__main__": threading.Thread(target=collect_loop, daemon=True).start() print("Dashboard listening on :8080", flush=True) HTTPServer(("", 8080), _Handler).serve_forever() {% endraw %}