diff --git a/src/routers/sources.py b/src/routers/sources.py index 2ac3e3d..fe7e73e 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -516,3 +516,134 @@ async def run_health_check_now( "issues": result["issues"], "suggestions": suggestion_count, } + + + +@router.post("/health/search-fix/{source_id}") +async def search_fix_for_source( + source_id: int, + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Sonnet mit WebSearch nach Lösung für eine kaputte Quelle suchen lassen.""" + import json as _json + + cursor = await db.execute( + "SELECT id, name, url, domain, source_type, category FROM sources WHERE id = ?", + (source_id,), + ) + source = await cursor.fetchone() + if not source: + raise HTTPException(status_code=404, detail="Quelle nicht gefunden") + + source = dict(source) + + # Health-Check-Probleme für diese Quelle laden + cursor = await db.execute( + "SELECT check_type, status, message FROM source_health_checks WHERE source_id = ?", + (source_id,), + ) + issues = [dict(row) for row in await cursor.fetchall()] + issues_text = "\n".join(f"- {i['check_type']}: {i['status']} - {i['message']}" for i in issues) + + prompt = f"""Du bist ein OSINT-Analyst. Folgende Quelle ist nicht mehr erreichbar: + +Name: {source['name']} +URL: {source['url'] or 'keine'} +Domain: {source['domain'] or 'unbekannt'} +Typ: {source['source_type']} +Kategorie: {source['category']} + +Probleme: +{issues_text} + +Aufgabe: Suche im Internet nach funktionierenden Alternativen für diese Quelle. +- Finde konkrete RSS-Feed-URLs die tatsächlich funktionieren +- Prüfe ob es alternative Zugangswege gibt (andere Subdomains, Feed-Aggregatoren, alternative URLs) +- Gibt es eine Lösung oder ist die Quelle nur noch per WebSearch erreichbar? + +Antworte NUR mit einem JSON-Objekt: +{{ + "fixable": true/false, + "solutions": [ + {{ + "type": "replace_url|add_feed|deactivate", + "name": "Anzeigename", + "url": "https://...", + "description": "Kurze Begründung" + }} + ], + "summary": "Zusammenfassung in 1-2 Sätzen" +}} + +Nur das JSON, kein anderer Text.""" + + sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor/src") + from agents.claude_client import call_claude + + try: + response, usage = await call_claude(prompt, tools="WebSearch,WebFetch") + + import re + json_match = re.search(r'\{.*\}', response, re.DOTALL) + if json_match: + result = _json.loads(json_match.group(0)) + else: + result = {"fixable": False, "solutions": [], "summary": response[:500]} + + # Lösungen als Vorschläge speichern + await db.executescript(""" + CREATE TABLE IF NOT EXISTS source_suggestions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + suggestion_type TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + source_id INTEGER REFERENCES sources(id) ON DELETE SET NULL, + suggested_data TEXT, + priority TEXT DEFAULT 'medium', + status TEXT DEFAULT 'pending', + reviewed_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """) + + for sol in result.get("solutions", []): + sol_type = sol.get("type", "add_feed") + suggestion_type = { + "replace_url": "fix_url", + "add_feed": "add_source", + "deactivate": "deactivate_source", + }.get(sol_type, "add_source") + + title = f"{source['name']}: {sol.get('description', sol_type)[:80]}" + + # Duplikat-Check + cursor = await db.execute( + "SELECT id FROM source_suggestions WHERE title = ? AND status = 'pending'", + (title,), + ) + if await cursor.fetchone(): + continue + + data = _json.dumps({ + "name": sol.get("name", source["name"]), + "url": sol.get("url", ""), + "domain": source["domain"] or "", + "category": source["category"], + }, ensure_ascii=False) + + await db.execute( + "INSERT INTO source_suggestions " + "(suggestion_type, title, description, source_id, suggested_data, priority, status) " + "VALUES (?, ?, ?, ?, ?, 'high', 'pending')", + (suggestion_type, title, sol.get("description", ""), source_id, data), + ) + + await db.commit() + + result["cost_usd"] = usage.cost_usd + result["tokens"] = {"input": usage.input_tokens, "output": usage.output_tokens} + return result + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Recherche fehlgeschlagen: {e}") diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index 64e3323..7527f2e 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -1,259 +1,289 @@ -/* Quellen-Health & Vorschläge */ -"use strict"; - -let healthData = null; -let suggestionsCache = []; - -const CHECK_TYPE_LABELS = { - reachability: "Erreichbarkeit", - feed_validity: "Feed-Validität", - stale: "Aktualität", - duplicate: "Duplikat", -}; - -const SUGGESTION_TYPE_LABELS = { - add_source: "Neue Quelle", - deactivate_source: "Deaktivieren", - remove_source: "Entfernen", - fix_url: "URL korrigieren", -}; - -const PRIORITY_LABELS = { - high: "Hoch", - medium: "Mittel", - low: "Niedrig", -}; - -// --- Init --- -function setupHealthTab() { - const tab = document.querySelector('#sourceSubTabs .nav-tab[data-subtab="source-health"]'); - if (tab) { - tab.addEventListener("click", () => loadHealthData()); - } -} - -document.addEventListener("DOMContentLoaded", setupHealthTab); - -// --- Health-Daten laden --- -async function loadHealthData() { - try { - const [health, suggestions] = await Promise.all([ - API.get("/api/sources/health"), - API.get("/api/sources/suggestions"), - ]); - healthData = health; - suggestionsCache = suggestions; - renderHealthDashboard(); - } catch (err) { - console.error("Health-Daten laden fehlgeschlagen:", err); - document.getElementById("healthContent").innerHTML = - '
Fehler beim Laden der Health-Daten.
'; - } -} - -function renderHealthDashboard() { - const container = document.getElementById("healthContent"); - if (!container) return; - - // Vorschläge rendern - const pendingSuggestions = suggestionsCache.filter((s) => s.status === "pending"); - const recentSuggestions = suggestionsCache.filter((s) => s.status !== "pending"); - - let suggestionsHtml = ""; - if (pendingSuggestions.length > 0) { - suggestionsHtml = ` -
-
-

Vorschläge (${pendingSuggestions.length} offen)

-
-
- - - - - - - - - - - - - ${pendingSuggestions - .map( - (s) => ` - - - - - - - - `, - ) - .join("")} - -
TypTitelBeschreibungPrioritätErstelltAktionen
${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}${esc(s.title)}${esc(s.description || "")}${PRIORITY_LABELS[s.priority] || s.priority}${formatDate(s.created_at)} - - -
-
-
`; - } else { - suggestionsHtml = ` -
-

Vorschläge

-
Keine offenen Vorschläge vorhanden.
-
`; - } - - // Vergangene Vorschläge - let historyHtml = ""; - if (recentSuggestions.length > 0) { - historyHtml = ` -
-

Verlauf

-
- - - - - - ${recentSuggestions - .slice(0, 20) - .map( - (s) => ` - - - - - - `, - ) - .join("")} - -
TypTitelStatusBearbeitet
${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}${esc(s.title)}${s.status === "accepted" ? "Angenommen" : "Abgelehnt"}${formatDate(s.reviewed_at)}
-
-
`; - } - - // Health-Check Ergebnisse - let healthHtml = ""; - if (healthData && healthData.checks && healthData.checks.length > 0) { - const issues = healthData.checks.filter((c) => c.status !== "ok"); - const okCount = healthData.checks.filter((c) => c.status === "ok").length; - - healthHtml = ` -
-
-

Health-Check Ergebnisse

- - Letzter Check: ${healthData.last_check ? formatDate(healthData.last_check) : "Noch nie"} -  |  - ${healthData.errors} Fehler   - ${healthData.warnings} Warnungen   - ${okCount} OK - -
`; - - if (issues.length > 0) { - healthHtml += ` -
- - - - - - ${issues - .map( - (c) => ` - - - - - - - `, - ) - .join("")} - -
QuelleDomainTypStatusDetails
${esc(c.name)}${esc(c.domain || "")}${CHECK_TYPE_LABELS[c.check_type] || c.check_type}${c.status === "error" ? "Fehler" : "Warnung"}${esc(c.message)}
-
`; - } else { - healthHtml += '
Alle Quellen sind gesund.
'; - } - healthHtml += "
"; - } else { - healthHtml = ` -
-

Health-Check Ergebnisse

-
Noch kein Health-Check durchgeführt.
-
`; - } - - container.innerHTML = suggestionsHtml + historyHtml + healthHtml; -} - -// --- Vorschlag annehmen/ablehnen --- -async function handleSuggestion(id, accept) { - const action = accept ? "annehmen" : "ablehnen"; - const suggestion = suggestionsCache.find((s) => s.id === id); - if (!suggestion) return; - - if (!confirm(`Vorschlag "${suggestion.title}" ${action}?`)) return; - - try { - const result = await API.put("/api/sources/suggestions/" + id, { accept }); - if (result.action) { - alert(`Ergebnis: ${result.action}`); - } - loadHealthData(); - // Grundquellen-Liste auch aktualisieren - if (typeof loadGlobalSources === "function") loadGlobalSources(); - } catch (err) { - alert("Fehler: " + err.message); - } -} - -// --- Health-Check manuell starten --- -async function runHealthCheck() { - const btn = document.getElementById("runHealthCheckBtn"); - if (btn) { - btn.disabled = true; - btn.textContent = "Läuft..."; - } - - try { - const result = await API.post("/api/sources/health/run"); - alert( - `Health-Check abgeschlossen: ${result.checked} Quellen geprüft, ` + - `${result.issues} Probleme gefunden. ` + - `${result.suggestions} neue Vorschläge generiert.`, - ); - loadHealthData(); - } catch (err) { - alert("Fehler: " + err.message); - } finally { - if (btn) { - btn.disabled = false; - btn.textContent = "Jetzt prüfen"; - } - } -} - -// --- Hilfsfunktionen --- -function formatDate(dateStr) { - if (!dateStr) return "-"; - try { - const d = new Date(dateStr); - return d.toLocaleDateString("de-DE", { - day: "2-digit", - month: "2-digit", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - } catch (_) { - return dateStr; - } -} +/* Quellen-Health & Vorschläge */ +"use strict"; + +let healthData = null; +let suggestionsCache = []; + +const CHECK_TYPE_LABELS = { + reachability: "Erreichbarkeit", + feed_validity: "Feed-Validität", + stale: "Aktualität", + duplicate: "Duplikat", +}; + +const SUGGESTION_TYPE_LABELS = { + add_source: "Neue Quelle", + deactivate_source: "Deaktivieren", + remove_source: "Entfernen", + fix_url: "URL korrigieren", +}; + +const PRIORITY_LABELS = { + high: "Hoch", + medium: "Mittel", + low: "Niedrig", +}; + +// --- Init --- +function setupHealthTab() { + const tab = document.querySelector('#sourceSubTabs .nav-tab[data-subtab="source-health"]'); + if (tab) { + tab.addEventListener("click", () => loadHealthData()); + } +} + +document.addEventListener("DOMContentLoaded", setupHealthTab); + +// --- Health-Daten laden --- +async function loadHealthData() { + try { + const [health, suggestions] = await Promise.all([ + API.get("/api/sources/health"), + API.get("/api/sources/suggestions"), + ]); + healthData = health; + suggestionsCache = suggestions; + renderHealthDashboard(); + } catch (err) { + console.error("Health-Daten laden fehlgeschlagen:", err); + document.getElementById("healthContent").innerHTML = + '
Fehler beim Laden der Health-Daten.
'; + } +} + +function renderHealthDashboard() { + const container = document.getElementById("healthContent"); + if (!container) return; + + // Vorschläge rendern + const pendingSuggestions = suggestionsCache.filter((s) => s.status === "pending"); + const recentSuggestions = suggestionsCache.filter((s) => s.status !== "pending"); + + let suggestionsHtml = ""; + if (pendingSuggestions.length > 0) { + suggestionsHtml = ` +
+
+

Vorschläge (${pendingSuggestions.length} offen)

+
+
+ + + + + + + + + + + + + ${pendingSuggestions + .map( + (s) => ` + + + + + + + + `, + ) + .join("")} + +
TypTitelBeschreibungPrioritätErstelltAktionen
${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}${esc(s.title)}${esc(s.description || "")}${PRIORITY_LABELS[s.priority] || s.priority}${formatDate(s.created_at)} + + +
+
+
`; + } else { + suggestionsHtml = ` +
+

Vorschläge

+
Keine offenen Vorschläge vorhanden.
+
`; + } + + // Vergangene Vorschläge + let historyHtml = ""; + if (recentSuggestions.length > 0) { + historyHtml = ` +
+

Verlauf

+
+ + + + + + ${recentSuggestions + .slice(0, 20) + .map( + (s) => ` + + + + + + `, + ) + .join("")} + +
TypTitelStatusBearbeitet
${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}${esc(s.title)}${s.status === "accepted" ? "Angenommen" : "Abgelehnt"}${formatDate(s.reviewed_at)}
+
+
`; + } + + // Health-Check Ergebnisse + let healthHtml = ""; + if (healthData && healthData.checks && healthData.checks.length > 0) { + const issues = healthData.checks.filter((c) => c.status !== "ok"); + const okCount = healthData.checks.filter((c) => c.status === "ok").length; + + healthHtml = ` +
+
+

Health-Check Ergebnisse

+ + Letzter Check: ${healthData.last_check ? formatDate(healthData.last_check) : "Noch nie"} +  |  + ${healthData.errors} Fehler   + ${healthData.warnings} Warnungen   + ${okCount} OK + +
`; + + if (issues.length > 0) { + healthHtml += ` +
+ + + + + + ${issues + .map( + (c) => ` + + + + + + + + `, + ) + .join("")} + +
QuelleDomainTypStatusDetailsAktionen
${esc(c.name)}${esc(c.domain || "")}${CHECK_TYPE_LABELS[c.check_type] || c.check_type}${c.status === "error" ? "Fehler" : "Warnung"}${esc(c.message)}${c.status === "error" && c.check_type === "reachability" ? \`\` : ""}
+
`; + } else { + healthHtml += '
Alle Quellen sind gesund.
'; + } + healthHtml += "
"; + } else { + healthHtml = ` +
+

Health-Check Ergebnisse

+
Noch kein Health-Check durchgeführt.
+
`; + } + + container.innerHTML = suggestionsHtml + historyHtml + healthHtml; +} + +// --- Vorschlag annehmen/ablehnen --- +async function handleSuggestion(id, accept) { + const action = accept ? "annehmen" : "ablehnen"; + const suggestion = suggestionsCache.find((s) => s.id === id); + if (!suggestion) return; + + if (!confirm(`Vorschlag "${suggestion.title}" ${action}?`)) return; + + try { + const result = await API.put("/api/sources/suggestions/" + id, { accept }); + if (result.action) { + alert(`Ergebnis: ${result.action}`); + } + loadHealthData(); + // Grundquellen-Liste auch aktualisieren + if (typeof loadGlobalSources === "function") loadGlobalSources(); + } catch (err) { + alert("Fehler: " + err.message); + } +} + +// --- Health-Check manuell starten --- +async function runHealthCheck() { + const btn = document.getElementById("runHealthCheckBtn"); + if (btn) { + btn.disabled = true; + btn.textContent = "Läuft..."; + } + + try { + const result = await API.post("/api/sources/health/run"); + alert( + `Health-Check abgeschlossen: ${result.checked} Quellen geprüft, ` + + `${result.issues} Probleme gefunden. ` + + `${result.suggestions} neue Vorschläge generiert.`, + ); + loadHealthData(); + } catch (err) { + alert("Fehler: " + err.message); + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = "Jetzt prüfen"; + } + } +} + +// --- Hilfsfunktionen --- +function formatDate(dateStr) { + if (!dateStr) return "-"; + try { + const d = new Date(dateStr); + return d.toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + } catch (_) { + return dateStr; + } +} + + +// --- Sonnet-Recherche für kaputte Quelle --- +async function searchFix(sourceId, sourceName) { + if (!confirm(`Sonnet mit WebSearch nach einer Lösung für "${sourceName}" suchen lassen?\n\nDas nutzt Kontingent vom Max-Abo (~$3-4).`)) return; + + const btn = event.target; + btn.disabled = true; + btn.textContent = "Sucht..."; + + try { + const result = await API.post("/api/sources/health/search-fix/" + sourceId); + + let msg = result.summary || "Keine Zusammenfassung"; + if (result.solutions && result.solutions.length > 0) { + msg += "\n\nGefundene Lösungen als Vorschläge gespeichert."; + } + if (result.cost_usd) { + msg += `\n\nKosten: $${result.cost_usd.toFixed(2)}`; + } + alert(msg); + loadHealthData(); + } catch (err) { + alert("Fehler: " + err.message); + } finally { + btn.disabled = false; + btn.textContent = "Lösung suchen"; + } +}