From 5e08d0678418b362b64196fc393e969561b98ec0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 15:26:24 +0000 Subject: [PATCH] feat(quellen-health): Strategie-Eskalation, Loesung-suchen bei Warnings, Trend-Delta Drei zusammenhaengende Verbesserungen am Quellen-Health-Bereich: 1. shared/services/source_suggester.py: - sync mit Monitor commit 49c5572. - Neue Funktion generate_strategy_escalation_suggestions: erzeugt deactivate-Vorschlaege fuer Quellen mit fetch_strategy=googlebot| paywall, deren Reachability-Check trotzdem error meldet. 2. source-health.js: Loesung-suchen-Button erweitert. Bisher nur bei status=error AND check_type=reachability. Jetzt auch bei status=warning AND check_type=feed_validity (z.B. "Feed erreichbar aber leer"). Backend-Endpoint /api/sources/health/ search-fix wird in beiden Faellen aufgerufen, Claude sucht eine bessere URL fuer die Quelle. 3. source-health.js: Trend-Delta im Counter. Liest healthHistoryCache[1] (vorletzter Run) und vergleicht mit aktuellen errors/warnings/ok. Zeigt z.B. "3 Fehler (+2)" rot oder "143 Warnungen (-15)" gruen. Bei steigenden ok-Counts ist Plus gruen, bei steigenden Fehlern ist Plus rot. Wenn der vorletzte Run nicht verfuegbar (Initial-Lauf): kein Delta. Cache-Buster source-health.js auf 20260509l gebumpt. --- src/shared/services/source_suggester.py | 90 +++++++++++++++++++++++-- src/static/dashboard.html | 2 +- src/static/js/source-health.js | 28 ++++++-- 3 files changed, 109 insertions(+), 11 deletions(-) diff --git a/src/shared/services/source_suggester.py b/src/shared/services/source_suggester.py index 555ee52..d84c30a 100644 --- a/src/shared/services/source_suggester.py +++ b/src/shared/services/source_suggester.py @@ -102,18 +102,96 @@ async def generate_stale_deactivation_suggestions( return created +async def generate_strategy_escalation_suggestions(db: aiosqlite.Connection) -> int: + """Erzeugt deactivate_source-Vorschläge für Quellen, bei denen die fetch_strategy + bereits eskaliert wurde (googlebot oder paywall) und der Reachability-Check + trotzdem error meldet. + + Beispiel: Rheinische Post hat fetch_strategy=googlebot, kriegt aber HTTP 403. + -> Strategie greift nicht, Quelle ist faktisch nicht abrufbar. Vorschlag: deaktivieren. + + Doppel-Vermeidung wie in der Karteileichen-Heuristik: nur wenn noch kein pending + deactivate-Vorschlag für die source_id existiert. + + Returns: Anzahl neu erstellter Vorschläge. + """ + cursor = await db.execute( + """ + SELECT s.id, s.name, s.url, s.domain, s.fetch_strategy, h.message + FROM sources s + JOIN source_health_checks h ON h.source_id = s.id + WHERE s.status = 'active' + AND s.fetch_strategy IN ('googlebot', 'paywall') + AND h.check_type = 'reachability' + AND h.status = 'error' + """ + ) + candidates = [dict(row) for row in await cursor.fetchall()] + if not candidates: + return 0 + + cursor = await db.execute( + "SELECT DISTINCT source_id FROM source_suggestions " + "WHERE status = 'pending' AND suggestion_type = 'deactivate_source' " + "AND source_id IS NOT NULL" + ) + already_pending = {row["source_id"] for row in await cursor.fetchall()} + + created = 0 + for c in candidates: + sid = c["id"] + if sid in already_pending: + continue + title = f"{c['name']} (ID {sid}) - Strategie greift nicht" + description = ( + f"Quelle: {c['name']} | URL: {c['url']} | Domain: {c['domain'] or '-'}\n" + f"fetch_strategy='{c['fetch_strategy']}' wurde bereits zur Eskalation gesetzt, " + f"liefert beim Health-Check aber weiter einen Fehler:\n" + f" {c['message']}\n" + "Vorschlag: deaktivieren oder fetch_strategy='skip' setzen, damit die Quelle " + "den Health-Check nicht weiter verfälscht.\n" + "Hinweis: Quelle wurde automatisch erkannt. Bitte vor Annahme prüfen." + ) + suggested_data = json.dumps( + {"action": "deactivate", "source_id": sid, + "reason": "fetch_strategy_failed", "current_strategy": c["fetch_strategy"]}, + ensure_ascii=False, + ) + await db.execute( + "INSERT INTO source_suggestions " + "(suggestion_type, title, description, source_id, suggested_data, " + " priority, status) VALUES " + "('deactivate_source', ?, ?, ?, ?, 'high', 'pending')", + (title, description, sid, suggested_data), + ) + created += 1 + + if created > 0: + await db.commit() + logger.info( + "Strategie-Eskalations-Heuristik: %d neue deactivate-Vorschläge " + "(%d Kandidaten, %d bereits pending)", + created, len(candidates), len(already_pending), + ) + return created + + async def generate_suggestions(db: aiosqlite.Connection) -> int: """Generiert Quellen-Vorschläge basierend auf Health-Checks und Lückenanalyse. - Zwei Stufen: + Drei Stufen: 1. Deterministisch: Karteileichen-Heuristik (article_count=0 oder >60d stumm) erzeugt sofort deactivate_source-Vorschläge ohne KI-Aufruf. - 2. KI-basiert: Haiku schaut sich Quellensammlung + Health-Probleme an + 2. Deterministisch: Strategie-Eskalations-Heuristik (fetch_strategy=googlebot + oder paywall, aber Reachability weiter error) erzeugt deactivate_source- + Vorschläge mit Priorität 'high'. + 3. KI-basiert: Haiku schaut sich Quellensammlung + Health-Probleme an und schlägt weitere Verbesserungen vor (add_source, deactivate_source, fix_url, ...). - Rückgabe ist die Gesamtzahl neu erzeugter Vorschläge beider Stufen. + Rückgabe ist die Gesamtzahl neu erzeugter Vorschläge aller Stufen. """ stale_count = await generate_stale_deactivation_suggestions(db) + strategy_count = await generate_strategy_escalation_suggestions(db) logger.info("Starte Quellen-Vorschläge via Haiku...") @@ -267,15 +345,15 @@ Nur das JSON-Array, kein anderer Text.""" await db.commit() logger.info( f"Quellen-Vorschläge: {count} neue Vorschläge generiert via Haiku " - f"(+{stale_count} aus Karteileichen-Heuristik) " + f"(+{stale_count} Karteileichen, +{strategy_count} Strategie-Eskalation) " f"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / " f"${usage.cost_usd:.4f})" ) - return count + stale_count + return count + stale_count + strategy_count except Exception as e: logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True) - return stale_count + return stale_count + strategy_count async def apply_suggestion( diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 4c69e88..4bd8138 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -723,7 +723,7 @@ - +
diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index b8622be..92c001c 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -260,6 +260,23 @@ function renderHealthDashboard() { return `${total} ${label} (${esc(detail)})`; } + // Trend-Delta zum vorletzten Run (healthHistoryCache[1]). Index 0 ist + // typischerweise der aktuelle Stand, Index 1 der davor archivierte Run. + // Wenn weniger als 2 Runs in der History: kein Delta anzeigen. + const prevRun = (healthHistoryCache && healthHistoryCache.length > 1) ? healthHistoryCache[1] : null; + function deltaBadge(currentValue, prevValue, badIsUp) { + if (prevValue == null) return ""; + const d = currentValue - prevValue; + if (d === 0) return ` (±0)`; + const sign = d > 0 ? "+" : ""; + // badIsUp=true: Anstieg = schlecht (rot), Abnahme = gut (grün). Umgekehrt für OK. + const cls = (badIsUp ? (d > 0) : (d < 0)) ? "text-danger" : "text-success"; + return ` (${sign}${d})`; + } + const dErr = prevRun ? deltaBadge(healthData.errors, prevRun.errors, true) : ""; + const dWarn = prevRun ? deltaBadge(healthData.warnings, prevRun.warnings, true) : ""; + const dOk = prevRun ? deltaBadge(okCount, prevRun.ok, false) : ""; + healthHtml = `
@@ -267,9 +284,9 @@ function renderHealthDashboard() { Letzter Check: ${healthData.last_check ? formatDateTime(healthData.last_check) : "Noch nie"}  |  - ${breakdownLine("error", "text-danger") || `0 Fehler`}   - ${breakdownLine("warning", "text-warning") || `0 Warnungen`}   - ${okCount} OK + ${breakdownLine("error", "text-danger") || `0 Fehler`}${dErr}   + ${breakdownLine("warning", "text-warning") || `0 Warnungen`}${dWarn}   + ${okCount} OK${dOk}
@@ -319,7 +336,10 @@ function renderHealthDashboard() { ${c.tenant_id == null ? 'global' : esc(c.org_name || ("Org " + c.tenant_id))} ${c.status === "error" ? "Fehler" : (c.status === "warning" ? "Warnung" : "OK")} ${esc(c.message || "")} - ${c.status === "error" && c.check_type === "reachability" ? `` : ""} + ${( + (c.status === "error" && c.check_type === "reachability") || + (c.status === "warning" && c.check_type === "feed_validity") + ) ? `` : ""} `; } ) -- 2.49.1