Phase 5: Dashboard-Datengrundlage (Kunde + Admin)
Neue fetch_customer_dashboard()/fetch_admin_dashboard() + Endpunkte
GET /api/v1/organizations/{id}/dashboard und GET /api/v1/dashboard/admin.
V1 bewusst nur aus bereits vorhandenen Daten (siehe Plan-Klärungsrunde
11.09.2026) - der Agent meldet Platten-/RAM-Auslastung schon länger bei
jedem Check-in (health_disk_used_percent/health_ram_used_percent), das
wird hier erstmals aggregiert statt nur pro Gerät angezeigt:
- "Nächste Schritte": eigener Workspace vorhanden, Installationsmedium
erstellt (das eigentliche iso_build_id lebt in kundenplattforms eigener
DB, dort ergänzt), erstes Gerät aktiv, Hostnamenkonvention eingerichtet.
- Flotten-Gesundheit: Geräte ohne Check-in seit 7 Tagen, Platten-/RAM-
Auslastung ab 85% - gemeinsame Auswertung _dashboard_flotten_gesundheit()
für Kunde (org-gefiltert) und Admin (alle Organisationen, mit Namens-
Präfix je Zeile).
- Letzte Aktionen: jüngste device_merkmal_events (applied/apply_failed).
- Admin zusätzlich: Organisationstabelle mit Geräteanzahl/letzter Aktivität.
Festplattenfehler-/SMART-Daten bleiben bewusst außen vor - der Agent
erfasst das aktuell nicht, wäre ein eigener Erfassungsmechanismus (siehe
Rückfrage-Runde im Plan).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
1 parent
4706870774
commit
2aae6348a6
1 file changed
+194
@@ -2121,6 +2121,184 @@ def fetch_all_devices():
|
|||||||
) in cur.fetchall()
|
) in cur.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Phase 5 des GUI-Ausbaus (11.09.2026, siehe
|
||||||
|
# project_kundenplattform_gui_umbau_2026-09-11_dashboard_workspace-Memory):
|
||||||
|
# Dashboard v1, ausschliesslich aus bereits vorhandenen Daten (kein neuer
|
||||||
|
# Agent-Datenerfassungsmechanismus - der Agent meldet schon länger Platten-/
|
||||||
|
# RAM-Auslastung + Load-Average bei jedem Check-in, siehe
|
||||||
|
# GERAETE_HEALTH_SPALTEN_SQL/agent_checkin() weiter oben). Fehlende
|
||||||
|
# Festplattenfehler-/SMART-Daten bleiben eine bekannte, bewusst hier NICHT
|
||||||
|
# geschlossene Lücke (Nutzer-Rückfrage-Runde, siehe Plan).
|
||||||
|
DASHBOARD_KEIN_CHECKIN_TAGE = 7
|
||||||
|
DASHBOARD_DISK_KRITISCH_PROZENT = 85
|
||||||
|
DASHBOARD_RAM_KRITISCH_PROZENT = 85
|
||||||
|
DASHBOARD_LETZTE_AKTIONEN_LIMIT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def _dashboard_flotten_gesundheit(geraete_rows) -> dict:
|
||||||
|
"""
|
||||||
|
Gemeinsame Auswertung fuer Kunden- und Admin-Dashboard: geraete_rows ist
|
||||||
|
eine Liste von (hostname, agent_last_checkin, disk_prozent, ram_prozent)
|
||||||
|
-Tupeln, optional mit vorangestelltem organisationsnamen (siehe Aufrufer).
|
||||||
|
"""
|
||||||
|
|
||||||
|
schwelle = datetime.now(timezone.utc) - timedelta(days=DASHBOARD_KEIN_CHECKIN_TAGE)
|
||||||
|
|
||||||
|
ohne_checkin = []
|
||||||
|
disk_kritisch = []
|
||||||
|
ram_kritisch = []
|
||||||
|
|
||||||
|
for row in geraete_rows:
|
||||||
|
*praefix, hostname, last_checkin, disk, ram = row
|
||||||
|
anzeige_name = praefix[0] + " – " + (hostname or "unbenannt") if praefix else (hostname or "unbenannt")
|
||||||
|
|
||||||
|
if last_checkin is None or last_checkin < schwelle:
|
||||||
|
ohne_checkin.append({
|
||||||
|
"name": anzeige_name,
|
||||||
|
"agent_last_checkin": last_checkin.isoformat() if last_checkin else None,
|
||||||
|
})
|
||||||
|
if disk is not None and disk >= DASHBOARD_DISK_KRITISCH_PROZENT:
|
||||||
|
disk_kritisch.append({"name": anzeige_name, "wert": round(disk)})
|
||||||
|
if ram is not None and ram >= DASHBOARD_RAM_KRITISCH_PROZENT:
|
||||||
|
ram_kritisch.append({"name": anzeige_name, "wert": round(ram)})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"geraete_gesamt": len(geraete_rows),
|
||||||
|
"geraete_ohne_checkin": sorted(ohne_checkin, key=lambda e: e["agent_last_checkin"] or ""),
|
||||||
|
"geraete_disk_kritisch": sorted(disk_kritisch, key=lambda e: -e["wert"]),
|
||||||
|
"geraete_ram_kritisch": sorted(ram_kritisch, key=lambda e: -e["wert"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_customer_dashboard(organization_id: str):
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SELECT hostname_praefix FROM organizations WHERE id = %s", (organization_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
hostname_praefix = row[0] if row else None
|
||||||
|
|
||||||
|
cur.execute("SELECT COUNT(*) FROM workspaces WHERE organization_id = %s", (organization_id,))
|
||||||
|
(anzahl_eigene_workspaces,) = cur.fetchone()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT hostname, agent_last_checkin, health_disk_used_percent, health_ram_used_percent
|
||||||
|
FROM devices
|
||||||
|
WHERE organization_id = %s AND archived_at IS NULL
|
||||||
|
""",
|
||||||
|
(organization_id,),
|
||||||
|
)
|
||||||
|
geraete_rows = cur.fetchall()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM devices
|
||||||
|
WHERE organization_id = %s AND archived_at IS NULL AND agent_last_checkin IS NOT NULL
|
||||||
|
""",
|
||||||
|
(organization_id,),
|
||||||
|
)
|
||||||
|
(anzahl_aktive_geraete,) = cur.fetchone()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT e.occurred_at, d.hostname, m.name, e.event_type
|
||||||
|
FROM device_merkmal_events e
|
||||||
|
JOIN devices d ON d.id = e.device_id
|
||||||
|
JOIN merkmale m ON m.id = e.merkmal_id
|
||||||
|
WHERE d.organization_id = %s AND e.event_type IN ('applied', 'apply_failed')
|
||||||
|
ORDER BY e.occurred_at DESC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(organization_id, DASHBOARD_LETZTE_AKTIONEN_LIMIT),
|
||||||
|
)
|
||||||
|
aktionen = cur.fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"naechste_schritte": {
|
||||||
|
"hat_workspace": anzahl_eigene_workspaces > 0,
|
||||||
|
"hat_aktives_geraet": anzahl_aktive_geraete > 0,
|
||||||
|
"hostname_konvention_aktiv": bool(hostname_praefix),
|
||||||
|
},
|
||||||
|
"flotten_gesundheit": _dashboard_flotten_gesundheit(geraete_rows),
|
||||||
|
"letzte_aktionen": [
|
||||||
|
{
|
||||||
|
"occurred_at": occurred_at.isoformat(),
|
||||||
|
"hostname": hostname or "unbenannt",
|
||||||
|
"merkmal_name": merkmal_name,
|
||||||
|
"event_type": event_type,
|
||||||
|
}
|
||||||
|
for occurred_at, hostname, merkmal_name, event_type in aktionen
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_admin_dashboard():
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT o.name, d.hostname, 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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
geraete_rows = cur.fetchall()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT o.id, o.name,
|
||||||
|
COUNT(d.id) FILTER (WHERE d.archived_at IS NULL) AS geraete,
|
||||||
|
MAX(d.agent_last_checkin) AS letzte_aktivitaet
|
||||||
|
FROM organizations o
|
||||||
|
LEFT JOIN devices d ON d.organization_id = o.id
|
||||||
|
GROUP BY o.id, o.name
|
||||||
|
ORDER BY o.name
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
organisationen = cur.fetchall()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT e.occurred_at, o.name, d.hostname, m.name, e.event_type
|
||||||
|
FROM device_merkmal_events e
|
||||||
|
JOIN devices d ON d.id = e.device_id
|
||||||
|
JOIN organizations o ON o.id = d.organization_id
|
||||||
|
JOIN merkmale m ON m.id = e.merkmal_id
|
||||||
|
WHERE e.event_type IN ('applied', 'apply_failed')
|
||||||
|
ORDER BY e.occurred_at DESC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(DASHBOARD_LETZTE_AKTIONEN_LIMIT,),
|
||||||
|
)
|
||||||
|
aktionen = cur.fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"flotten_gesundheit": _dashboard_flotten_gesundheit(geraete_rows),
|
||||||
|
"organisationen": [
|
||||||
|
{
|
||||||
|
"id": str(org_id),
|
||||||
|
"name": name,
|
||||||
|
"geraete_anzahl": geraete_anzahl,
|
||||||
|
"letzte_aktivitaet": letzte_aktivitaet.isoformat() if letzte_aktivitaet else None,
|
||||||
|
}
|
||||||
|
for org_id, name, geraete_anzahl, letzte_aktivitaet in organisationen
|
||||||
|
],
|
||||||
|
"letzte_aktionen": [
|
||||||
|
{
|
||||||
|
"occurred_at": occurred_at.isoformat(),
|
||||||
|
"organization_name": organization_name,
|
||||||
|
"hostname": hostname or "unbenannt",
|
||||||
|
"merkmal_name": merkmal_name,
|
||||||
|
"event_type": event_type,
|
||||||
|
}
|
||||||
|
for occurred_at, organization_name, hostname, merkmal_name, event_type in aktionen
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def fetch_latest_hardware_snapshot(device_id: str):
|
def fetch_latest_hardware_snapshot(device_id: str):
|
||||||
"""
|
"""
|
||||||
Für die Kundenportal-Aktion "Hardware-Informationen": neuester
|
Für die Kundenportal-Aktion "Hardware-Informationen": neuester
|
||||||
@@ -4142,6 +4320,22 @@ def get_organization_endpoint(organization_id: str, authorization: str | None =
|
|||||||
return {"success": True, "organization": organization}
|
return {"success": True, "organization": organization}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/organizations/{organization_id}/dashboard")
|
||||||
|
def get_customer_dashboard_endpoint(organization_id: str, 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, "dashboard": fetch_customer_dashboard(organization_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/dashboard/admin")
|
||||||
|
def get_admin_dashboard_endpoint(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, "dashboard": fetch_admin_dashboard()}
|
||||||
|
|
||||||
|
|
||||||
@app.patch("/api/v1/organizations/{organization_id}")
|
@app.patch("/api/v1/organizations/{organization_id}")
|
||||||
def update_organization_endpoint(
|
def update_organization_endpoint(
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
|
|||||||
Reference in new issue
Block a user