From 465910f70075e6e8c83acc48c7997de4e4453bb1 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Fri, 9 Oct 2026 11:40:40 +0200 Subject: [PATCH] Dashboards: Verteilungen fuer Ringdiagramme (Zustand, Distro, Workspace, Organisation) _dashboard_verteilungen(): Geraetezustand ausschliessend (nie gemeldet, >7 Tage nicht gemeldet, Ressourcen knapp, in Ordnung), Distro und wirksamer Workspace je Geraet ueber geraete_basis(), Organisationen im Admin-Dashboard. Hoechstens 5 Teile + 'Weitere', Sonderfaelle (ohne Installation/Workspace) immer am Ende. GUI-Runde 10/2026 Punkt 6. Co-Authored-By: Claude Opus 5.5 --- app.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/app.py b/app.py index 5a4f4aa..1988cee 100644 --- a/app.py +++ b/app.py @@ -2519,6 +2519,90 @@ def _dashboard_flotten_gesundheit(geraete_rows) -> dict: } +DASHBOARD_VERTEILUNG_MAX = 5 +VERTEILUNG_SONDERFAELLE = {"ohne Installation", "ohne Workspace", "unbekannt"} + + +def _verteilung(zaehler: dict, max_teile: int = DASHBOARD_VERTEILUNG_MAX) -> list[dict]: + """Zähler -> [{name, anzahl}] absteigend; ab max_teile+1 Einträgen werden + die kleinsten zu "Weitere" zusammengefasst (Ringdiagramm: höchstens + sechs Teile, GUI-Runde 09.10.2026).""" + + # Keine echten Kategorien (grau dargestellt) immer ans Ende, damit die + # Kategoriefarben für echte Einträge lückenlos in fester Reihenfolge bleiben. + sonder = {n: a for n, a in zaehler.items() if n in VERTEILUNG_SONDERFAELLE} + teile = sorted(((n, a) for n, a in zaehler.items() if n not in sonder), key=lambda e: (-e[1], e[0])) + if len(teile) + len(sonder) > max_teile + 1: + behalten = max(max_teile - len(sonder), 1) + rest = sum(anzahl for _, anzahl in teile[behalten:]) + teile = teile[:behalten] + ([("Weitere", rest)] if rest else []) + teile += sorted(sonder.items()) + return [{"name": name, "anzahl": anzahl} for name, anzahl in teile] + + +def _dashboard_verteilungen(cur, organization_id: str | None = None) -> dict: + """ + Verteilungen für die Dashboard-Ringdiagramme (GUI-Runde 09.10.2026, + Punkt 6): Gerätezustand, Distro, wirksamer Workspace, bei + organisationsübergreifender Sicht auch Organisation. Zustand ist + ausschließend (jedes Gerät genau einmal): nicht gemeldet hat Vorrang vor + knappen Ressourcen. + """ + + cur.execute( + """ + SELECT d.id, o.name, d.agent_last_checkin, d.health_disk_used_percent, d.health_ram_used_percent + FROM devices d + JOIN organizations o ON o.id = d.organization_id + WHERE d.archived_at IS NULL AND (%(org)s::uuid IS NULL OR d.organization_id = %(org)s::uuid) + """, + {"org": organization_id}, + ) + geraete = cur.fetchall() + + cur.execute("SELECT id, name FROM backends") + backend_namen = {str(i): n for i, n in cur.fetchall()} + cur.execute("SELECT v.id, w.name FROM workspace_versionen v JOIN workspaces w ON w.id = v.workspace_id") + version_workspace = {str(i): n for i, n in cur.fetchall()} + + schwelle = datetime.now(timezone.utc) - timedelta(days=DASHBOARD_KEIN_CHECKIN_TAGE) + zustand = {"in Ordnung": 0, "Ressourcen knapp": 0, f"länger als {DASHBOARD_KEIN_CHECKIN_TAGE} Tage nicht gemeldet": 0, "noch nie gemeldet": 0} + distros, workspaces, organisationen = {}, {}, {} + + for device_id, org_name, checkin, disk, ram in geraete: + if checkin is None: + zustand["noch nie gemeldet"] += 1 + elif checkin < schwelle: + zustand[f"länger als {DASHBOARD_KEIN_CHECKIN_TAGE} Tage nicht gemeldet"] += 1 + elif (disk or 0) >= DASHBOARD_DISK_KRITISCH_PROZENT or (ram or 0) >= DASHBOARD_RAM_KRITISCH_PROZENT: + zustand["Ressourcen knapp"] += 1 + else: + zustand["in Ordnung"] += 1 + + basis = geraete_basis(cur, str(device_id)) + if basis is None: + distro, workspace = "ohne Installation", "ohne Installation" + else: + version_id, backend_id = basis + distro = backend_namen.get(str(backend_id), "unbekannt") + workspace = version_workspace.get(str(version_id), "ohne Workspace") if version_id else "ohne Workspace" + distros[distro] = distros.get(distro, 0) + 1 + workspaces[workspace] = workspaces.get(workspace, 0) + 1 + organisationen[org_name] = organisationen.get(org_name, 0) + 1 + + ergebnis = { + "geraete_gesamt": len(geraete), + # feste Reihenfolge (Statusfarben), leere Zustände bleiben drin - + # die Oberfläche blendet sie in der Legende aus + "zustand": [{"name": n, "anzahl": a} for n, a in zustand.items()], + "distros": _verteilung(distros), + "workspaces": _verteilung(workspaces), + } + if organization_id is None: + ergebnis["organisationen"] = _verteilung(organisationen) + return ergebnis + + def fetch_customer_dashboard(organization_id: str): with get_database_connection() as conn: with conn.cursor() as cur: @@ -2538,6 +2622,7 @@ def fetch_customer_dashboard(organization_id: str): (organization_id,), ) geraete_rows = cur.fetchall() + verteilungen = _dashboard_verteilungen(cur, organization_id) cur.execute( """ @@ -2570,6 +2655,7 @@ def fetch_customer_dashboard(organization_id: str): }, "flotten_gesundheit": _dashboard_flotten_gesundheit(geraete_rows), "aktivitaeten": fetch_aktivitaeten(organization_id, DASHBOARD_LETZTE_AKTIONEN_LIMIT), + "verteilungen": verteilungen, "letzte_aktionen": [ { "occurred_at": occurred_at.isoformat(), @@ -2677,6 +2763,7 @@ def fetch_admin_dashboard(): """ ) geraete_rows = cur.fetchall() + verteilungen = _dashboard_verteilungen(cur, None) cur.execute( """ @@ -2718,6 +2805,7 @@ def fetch_admin_dashboard(): for org_id, name, geraete_anzahl, letzte_aktivitaet in organisationen ], "aktivitaeten": fetch_aktivitaeten(None, DASHBOARD_LETZTE_AKTIONEN_LIMIT), + "verteilungen": verteilungen, "letzte_aktionen": [ { "occurred_at": occurred_at.isoformat(),