diff --git a/app.py b/app.py index 39564a8..30de69b 100644 --- a/app.py +++ b/app.py @@ -2560,6 +2560,7 @@ def fetch_customer_dashboard(organization_id: str): "hostname_konvention_aktiv": bool(hostname_praefix), }, "flotten_gesundheit": _dashboard_flotten_gesundheit(geraete_rows), + "aktivitaeten": fetch_aktivitaeten(organization_id, DASHBOARD_LETZTE_AKTIONEN_LIMIT), "letzte_aktionen": [ { "occurred_at": occurred_at.isoformat(), @@ -2572,6 +2573,88 @@ def fetch_customer_dashboard(organization_id: str): } +AKTIVITAETEN_LIMIT = 50 + +INSTALLATIONSEREIGNIS_TEXTE = { + "activation_confirmed": "Aktivierung bestätigt", + "installation_started": "Installation gestartet", + "installation_completed": "Installation abgeschlossen", + "installation_failed": "Installation fehlgeschlagen", +} + + +def fetch_aktivitaeten(organization_id: str | None = None, limit: int = AKTIVITAETEN_LIMIT): + """ + Aktivitätenliste (GUI-Runde 09.10.2026, Punkt 4): Installationsereignisse + einzeln, Agent-Läufe zusammengefasst. Ein Lauf meldet je Merkmal ein + Ereignis (rund 56) mit demselben Zeitstempel - die Liste bliebe sonst + unlesbar. Ohne organization_id über alle Organisationen (Admin-Dashboard). + """ + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + """ + WITH laeufe AS ( + SELECT e.device_id, e.occurred_at, count(*) AS anzahl, + array_remove(array_agg(m.name ORDER BY m.name) + FILTER (WHERE e.event_type LIKE '%%failed'), NULL) AS fehler + FROM device_merkmal_events e + JOIN merkmale m ON m.id = e.merkmal_id + JOIN devices d ON d.id = e.device_id + WHERE (%(org)s::uuid IS NULL OR d.organization_id = %(org)s::uuid) + GROUP BY e.device_id, e.occurred_at + ORDER BY e.occurred_at DESC + LIMIT %(limit)s + ), + installationen AS ( + SELECT ie.device_id, ie.occurred_at, ie.event_type + FROM device_installation_events ie + JOIN devices d ON d.id = ie.device_id + WHERE (%(org)s::uuid IS NULL OR d.organization_id = %(org)s::uuid) + ORDER BY ie.occurred_at DESC + LIMIT %(limit)s + ) + SELECT x.occurred_at, x.art, x.event_type, x.anzahl, x.fehler, + d.id, d.hostname, o.name + FROM ( + SELECT occurred_at, device_id, 'agent' AS art, NULL AS event_type, anzahl, fehler FROM laeufe + UNION ALL + SELECT occurred_at, device_id, 'installation', event_type, NULL, NULL FROM installationen + ) x + JOIN devices d ON d.id = x.device_id + JOIN organizations o ON o.id = d.organization_id + ORDER BY x.occurred_at DESC + LIMIT %(limit)s + """, + {"org": organization_id, "limit": limit}, + ) + rows = cur.fetchall() + + ergebnis = [] + for occurred_at, art, event_type, anzahl, fehler, device_id, hostname, org_name in rows: + fehler = fehler or [] + if art == "agent": + text = f"Agent-Lauf: {anzahl} Merkmale geprüft" + ( + f", {len(fehler)} fehlgeschlagen ({', '.join(fehler[:5])}{' …' if len(fehler) > 5 else ''})" + if fehler else ", ohne Fehler" + ) + problem = bool(fehler) + else: + text = INSTALLATIONSEREIGNIS_TEXTE.get(event_type, event_type) + problem = event_type == "installation_failed" + ergebnis.append({ + "zeit": occurred_at.isoformat(), + "art": art, + "text": text, + "problem": problem, + "device_id": str(device_id), + "hostname": hostname or "unbenannt", + "organization_name": org_name, + }) + return ergebnis + + def fetch_admin_dashboard(): with get_database_connection() as conn: with conn.cursor() as cur: @@ -2625,6 +2708,7 @@ def fetch_admin_dashboard(): } for org_id, name, geraete_anzahl, letzte_aktivitaet in organisationen ], + "aktivitaeten": fetch_aktivitaeten(None, DASHBOARD_LETZTE_AKTIONEN_LIMIT), "letzte_aktionen": [ { "occurred_at": occurred_at.isoformat(), @@ -5689,6 +5773,14 @@ def set_hostname_konvention_endpoint( return {"success": True} +@app.get("/api/v1/organizations/{organization_id}/aktivitaeten") +def list_aktivitaeten(organization_id: str, limit: int = AKTIVITAETEN_LIMIT, authorization: str | None = Header(default=None)): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + + return {"success": True, "aktivitaeten": fetch_aktivitaeten(organization_id, max(1, min(limit, 500)))} + + @app.get("/api/v1/organizations/{organization_id}/activation-codes") def list_activation_codes( organization_id: str,