perf(sources): Quellen-Health Pagination (default 100, plus mehr/alle laden)
Echter Bottleneck war der DOM-Render von 519 Tabellen-Zeilen, nicht
das Backend (45ms). Backend-Slim und Cache aus dem letzten Commit
haben Bandbreite und wiederholte Klicks beschleunigt, aber der erste
Klick blieb langsam, weil weiterhin alle 519 Items in einem
innerHTML-Schub gerendert wurden.
Lösung: Server-Side-Pagination.
Backend (/api/sources/health):
- Neue Query-Param: limit (default 100, max 5000), offset (default 0)
- Counters errors/warnings/ok/total_checks aus separater GROUP-BY-
Aggregat-Query über den GESAMTEN Bestand, nicht über die Page.
- Neues Feld all_orgs in der Antwort: alle Tenants mit Health-Checks,
damit das Filter-Dropdown auch im Pagination-Modus die volle
Org-Liste hat.
- Neue Felder limit, offset, has_more.
Frontend (source-health.js):
- healthLoadLimit (default 100), wird durch loadMoreHealth() um 200
hochgesetzt oder durch loadAllHealth() auf alles gesetzt.
- Cache-Key beinhaltet jetzt auch das aktuelle Limit, damit beim
Mehr-laden nicht aus altem Cache bedient wird.
- Org-Liste kommt aus healthData.all_orgs statt aus den geladenen
Page-Items, sonst wäre sie nach Pagination unvollständig.
- Footer mit zwei Buttons ("+200 laden", "Alle N weiteren laden")
unter der Tabelle, nur sichtbar bei has_more=true.
- Counter-Anzeige: "X / Y angezeigt (von Z insgesamt)".
Cache-Buster für source-health.js auf 20260509f gebumpt.
Dieser Commit ist enthalten in:
@@ -565,17 +565,46 @@ 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, billig)
|
||||
cursor = await db.execute(
|
||||
"SELECT status, COUNT(*) AS n FROM source_health_checks GROUP BY status"
|
||||
)
|
||||
counts = {row["status"]: row["n"] for row in await cursor.fetchall()}
|
||||
error_count = counts.get("error", 0)
|
||||
warning_count = counts.get("warning", 0)
|
||||
ok_count = counts.get("ok", 0)
|
||||
total_checks = error_count + warning_count + ok_count
|
||||
|
||||
# Paginierte Daten
|
||||
cursor = await db.execute("""
|
||||
SELECT
|
||||
h.source_id, s.name, s.domain, s.tenant_id, s.language,
|
||||
@@ -587,12 +616,20 @@ async def get_health(
|
||||
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()
|
||||
@@ -600,11 +637,15 @@ 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,
|
||||
"checks": checks,
|
||||
"all_orgs": all_orgs,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": (offset + len(checks)) < total_checks,
|
||||
}
|
||||
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren