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()
|
||||
]
|
||||
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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}
|
||||
|
||||
|
||||
@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}")
|
||||
def update_organization_endpoint(
|
||||
organization_id: str,
|
||||
|
||||
Reference in new issue
Block a user