"""Eine Live-Zahl je Gerät für die Kacheln des Homelab-Cockpits und der Übersicht (Ausbauplan Welle 1, seit 25.09.2026). Proxmox-Host wie viele Gäste laufen (aus dem Bericht) PBS wann zuletzt gesichert wurde (jüngste Sicherung auf den Zielen der Aufträge) NPMplus wie es um seine Zertifikate steht (Zertifikats-Wache, zertifikate.py) Arcane laufende von allen Containern (Arcane-API mit dem Schlüssel aus den Einstellungen) AdGuard Home Anteil geblockter Anfragen (nur mit hinterlegtem Zugang, Einstellungen; adguard.py) Je Ziel-ID {"text", "zeit"?, "stufe"?}: „zeit“ setzt die Oberfläche als „vor …“ dahinter, „stufe“ (gelb/rot) färbt die Zeile. Was sich nicht ermitteln lässt, fehlt — die Kachel zeigt dann nur ihren Stand. Arcane und AdGuard werden höchstens alle zwei Minuten gefragt. """ import logging from services.homelab import adguard, apps, arcane, zertifikate log = logging.getLogger(__name__) NPM = {"npmplus", "nginxproxymanager"} def _komma(zahl: float) -> str: return f"{zahl:.1f}".replace(".", ",") if zahl < 10 else str(round(zahl)) def _npm(vmid: int) -> dict | None: eigene = [z for z in zertifikate.lage()["zertifikate"] if z["quelle"] == "npm" and z["gast"] == vmid] if not eigene: return None probleme = [z for z in eigene if z["stufe"] != "ok"] if not probleme: return {"text": f"{len(eigene)} Zertifikat{'e' if len(eigene) != 1 else ''} gültig"} stufe = "rot" if any(z["stufe"] == "rot" for z in probleme) else "gelb" return {"text": f"{len(probleme)} von {len(eigene)} Zertifikaten mit Problem", "stufe": stufe} def fuer_bericht(bericht: dict) -> dict[str, dict]: """Ziel-ID → Kennzahl. Ein Fehler bei einem Gerät kostet nur dessen Zahl.""" zahlen: dict[str, dict] = {} gaeste = [g for g in bericht.get("gaeste") or [] if isinstance(g, dict) and "vmid" in g] if gaeste: laufend = sum(1 for g in gaeste if g.get("status") == "running") zahlen["pve"] = {"text": f"{laufend} von {len(gaeste)} Gästen an"} ziele = (bericht.get("host") or {}).get("sicherungsziele") or {} for g in gaeste: app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name")) if not app or g.get("status") != "running": continue zid = f"{'ct' if g.get('art') == 'lxc' else 'vm'}-{g['vmid']}" try: if app.kennung == "proxmox-backup-server": zuletzt = max((t for z in ziele.values() if isinstance(z, dict) for t in (z.get("neueste") or {}).values() if isinstance(t, int)), default=None) if zuletzt: zahlen[zid] = {"text": "letzte Sicherung", "zeit": zuletzt} elif app.kennung in NPM: if zahl := _npm(g["vmid"]): zahlen[zid] = zahl elif app.kennung == "arcane" and (url := apps.adresse(app, g.get("ip"))): container = arcane.container_zahlen(url.rstrip("/")) if container and container["gesamt"]: zahlen[zid] = {"text": f"{container['laufend']} von {container['gesamt']} Containern an"} elif app.kennung == "adguard": statistik = adguard.geblockt() if statistik and statistik.get("anteil") is not None: zahlen[zid] = {"text": f"{_komma(statistik['anteil'])} % geblockt"} except Exception: log.warning("kennzahlen: %s ließ sich nicht ermitteln", zid, exc_info=True) return zahlen