From e8bb2495eeafb3083212471ebb3d473e7710061e Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 9 May 2026 13:24:44 +0000 Subject: [PATCH] ux(quellen-health): Default "Nur Probleme", Counter feiner gegliedert, Filter-Hint bei Pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schritt 1 der Quellen-Health-Aufraeumung. Drei UX-Verbesserungen, kein Daten-Eingriff: 1. Default-Filter "Nur Probleme" (errors + warnings, ohne OK). - Neuer Status-Filter-Wert "issues" als virtuelles Frontend-Konstrukt. - applyHealthFilter behandelt "issues" als status != ok. - Default in healthFilters ist jetzt "issues". User sieht beim Tab-Klick sofort die kritischen 146 Eintraege statt der 281 gruenen OK-Zeilen. 2. Counter aufgegliedert nach check_type. - Backend (/api/sources/health): zusaetzliches Feld "breakdown" mit der GROUP-BY (check_type, status) Aggregation. - Frontend rendert pro Status-Zeile die feine Aufschluesselung, z.B. "143 Warnungen (112 Aktualität, 27 Feed-Validität, 3 Duplikat, 1 Erreichbarkeit)". - Hilft dem Admin, sofort zu sehen wo das Problem liegt. 3. Filter-Hint bei Pagination + leeren Treffern. - Wenn der aktuelle Filter ueber die geladenen 100 Items keinen Treffer findet UND has_more=true, zeigt das Frontend einen Hinweis-Link "Alle X Health-Checks laden und Filter erneut anwenden". - Loest das Edge-Problem, dass z.B. Filter "Nur OK" auf den Default-100 (errors first) leer schien. Cache-Buster fuer source-health.js auf 20260509g gebumpt. --- src/routers/sources.py | 24 +++++++++++++++------ src/static/dashboard.html | 2 +- src/static/js/source-health.js | 38 ++++++++++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/routers/sources.py b/src/routers/sources.py index ef3522c..7a3f8d7 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -594,14 +594,25 @@ async def get_health( "limit": limit, "offset": offset, "has_more": False, } - # Aggregate über GESAMTEN Bestand (eine GROUP-BY-Query, billig) + # 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 status, COUNT(*) AS n FROM source_health_checks GROUP BY status" + "SELECT check_type, status, COUNT(*) AS n FROM source_health_checks GROUP BY check_type, 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) + 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 @@ -641,6 +652,7 @@ async def get_health( "errors": error_count, "warnings": warning_count, "ok": ok_count, + "breakdown": breakdown, "checks": checks, "all_orgs": all_orgs, "limit": limit, diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 02fc726..421014e 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -715,7 +715,7 @@ - +
diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index 36c225a..2927c94 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -3,7 +3,9 @@ let healthData = null; let suggestionsCache = []; -let healthFilters = { status: "", check_type: "", org: "all" }; +// Default-Filter zeigt nur Probleme (errors + warnings); OK ist meistens Rauschen. +// "issues" ist ein virtueller Status-Wert, den nur das Frontend versteht (siehe applyHealthFilter). +let healthFilters = { status: "issues", check_type: "", org: "all" }; let healthHistoryCache = []; // 60-Sekunden-Cache, damit Tab-Wechsel nicht jedes Mal die volle Antwort neu lädt. @@ -91,7 +93,9 @@ function loadAllHealth() { function applyHealthFilter(checks) { return checks.filter(c => { - if (healthFilters.status && c.status !== healthFilters.status) return false; + // "issues" = Sammelfilter für errors + warnings (Default) + if (healthFilters.status === "issues" && c.status === "ok") return false; + if (healthFilters.status && healthFilters.status !== "issues" && c.status !== healthFilters.status) return false; if (healthFilters.check_type && c.check_type !== healthFilters.check_type) return false; if (healthFilters.org === "global" && c.tenant_id !== null) return false; if (healthFilters.org !== "all" && healthFilters.org !== "global" @@ -208,6 +212,21 @@ function renderHealthDashboard() { const checkTypes = Array.from(new Set(allChecks.map(c => c.check_type))); + // Counter-Aufgliederung aus Backend-Breakdown (pro check_type x status). + // Beispiel: { reachability: {ok: 281, error: 3, warning: 1}, feed_validity: {...}, stale: {...}, duplicate: {...} } + const breakdown = healthData.breakdown || {}; + function breakdownLine(statusKey, cssClass) { + const entries = Object.entries(breakdown) + .map(([ct, byStatus]) => [ct, byStatus[statusKey] || 0]) + .filter(([_, n]) => n > 0) + .sort((a, b) => b[1] - a[1]); + if (entries.length === 0) return ""; + const total = entries.reduce((s, [, n]) => s + n, 0); + const detail = entries.map(([ct, n]) => `${n} ${CHECK_TYPE_LABELS[ct] || ct}`).join(", "); + const label = statusKey === "error" ? "Fehler" : (statusKey === "warning" ? "Warnungen" : "OK"); + return `${total} ${label} (${esc(detail)})`; + } + healthHtml = `
@@ -215,14 +234,15 @@ function renderHealthDashboard() { Letzter Check: ${healthData.last_check ? formatDateTime(healthData.last_check) : "Noch nie"}  |  - ${healthData.errors} Fehler   - ${healthData.warnings} Warnungen   + ${breakdownLine("error", "text-danger") || `0 Fehler`}   + ${breakdownLine("warning", "text-warning") || `0 Warnungen`}   ${okCount} OK