Aktivitaetenliste: Installationsereignisse + zusammengefasste Agent-Laeufe

fetch_aktivitaeten(): ein Agent-Lauf meldet je Merkmal ein Ereignis (~56,
gleicher Zeitstempel) - hier zu einer Zeile zusammengefasst, Fehler
benannt. Neuer Endpunkt /organizations/{id}/aktivitaeten, beide
Dashboards liefern zusaetzlich 'aktivitaeten'. GUI-Runde 10/2026 Punkt 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-09 11:30:32 +02:00
1 parent 3381b2b13c
commit b038350e72
1 file changed
+92
+92
View File
@@ -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,