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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-09 11:40:40 +02:00
1 parent 0561691f20
commit 465910f700
1 file changed
+88
+88
View File
@@ -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(),