diff --git a/src/routers/sources.py b/src/routers/sources.py index dd96608..7a3f8d7 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -565,35 +565,82 @@ async def add_discovered_sources( @router.get("/health") async def get_health( + limit: int = 100, + offset: int = 0, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): - """Health-Check-Ergebnisse abrufen.""" + """Health-Check-Ergebnisse abrufen. + + Default-Limit 100, sortiert nach Status (errors first, dann warnings, dann ok). + Counters (errors/warnings/ok/total_checks) beziehen sich auf den GESAMTEN + Datenbestand, nicht nur auf die zurückgegebene Page. Damit kann das Frontend + den vollen Status anzeigen, ohne alle Zeilen rendern zu müssen. + has_more zeigt an, ob es weitere Items zum Nachladen gibt. + all_orgs liefert die Liste aller Tenants mit Health-Checks (für Filter-Dropdown). + """ + limit = max(1, min(int(limit or 100), 5000)) + offset = max(0, int(offset or 0)) + # Prüfen ob Tabelle existiert cursor = await db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='source_health_checks'" ) if not await cursor.fetchone(): - return {"last_check": None, "total_checks": 0, "errors": 0, "warnings": 0, "ok": 0, "checks": []} + return { + "last_check": None, "total_checks": 0, + "errors": 0, "warnings": 0, "ok": 0, + "checks": [], "all_orgs": [], + "limit": limit, "offset": offset, "has_more": False, + } + # Aggregate über GESAMTEN Bestand. Eine GROUP-BY-Query nach (check_type, status) + # liefert sowohl die Top-Counters als auch das feine Breakdown für die UI. + cursor = await db.execute( + "SELECT check_type, status, COUNT(*) AS n FROM source_health_checks GROUP BY check_type, status" + ) + breakdown = {} # {check_type: {status: count}} + error_count = 0 + warning_count = 0 + ok_count = 0 + for row in await cursor.fetchall(): + ct = row["check_type"] + st = row["status"] + breakdown.setdefault(ct, {})[st] = row["n"] + if st == "error": + error_count += row["n"] + elif st == "warning": + warning_count += row["n"] + elif st == "ok": + ok_count += row["n"] + total_checks = error_count + warning_count + ok_count + + # Paginierte Daten cursor = await db.execute(""" SELECT - h.id, h.source_id, s.name, s.domain, s.url, s.source_type, - s.tenant_id, s.category, s.language, s.bias, + h.source_id, s.name, s.domain, s.tenant_id, s.language, o.name AS org_name, - h.check_type, h.status, h.message, h.details, h.checked_at + h.check_type, h.status, h.message FROM source_health_checks h JOIN sources s ON s.id = h.source_id LEFT JOIN organizations o ON o.id = s.tenant_id ORDER BY CASE h.status WHEN 'error' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END, s.name - """) + LIMIT ? OFFSET ? + """, (limit, offset)) checks = [dict(row) for row in await cursor.fetchall()] - error_count = sum(1 for c in checks if c["status"] == "error") - warning_count = sum(1 for c in checks if c["status"] == "warning") - ok_count = sum(1 for c in checks if c["status"] == "ok") + # Org-Liste (alle Tenants mit Health-Checks, für Frontend-Filter-Dropdown) + cursor = await db.execute(""" + SELECT DISTINCT s.tenant_id AS id, o.name AS name + FROM source_health_checks h + JOIN sources s ON s.id = h.source_id + LEFT JOIN organizations o ON o.id = s.tenant_id + WHERE s.tenant_id IS NOT NULL + ORDER BY o.name + """) + all_orgs = [dict(row) for row in await cursor.fetchall()] cursor = await db.execute("SELECT MAX(checked_at) as last_check FROM source_health_checks") row = await cursor.fetchone() @@ -601,11 +648,16 @@ async def get_health( return { "last_check": last_check, - "total_checks": len(checks), + "total_checks": total_checks, "errors": error_count, "warnings": warning_count, "ok": ok_count, + "breakdown": breakdown, "checks": checks, + "all_orgs": all_orgs, + "limit": limit, + "offset": offset, + "has_more": (offset + len(checks)) < total_checks, } diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 61b4d41..421014e 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -393,6 +393,13 @@ + +