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 = `
-
-
-
-
-
-
- | Typ |
- Titel |
- Beschreibung |
- Priorität |
- Erstellt |
- Aktionen |
-
-
-
- ${pendingSuggestions
- .map(
- (s) => `
-
- | ${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)} |
-
-
-
- |
-
`,
- )
- .join("")}
-
-
-
-
`;
- } else {
- suggestionsHtml = `
-
-
-
Keine offenen Vorschläge vorhanden.
-
`;
- }
-
- // Vergangene Vorschläge
- let historyHtml = "";
- if (recentSuggestions.length > 0) {
- historyHtml = `
-
-
-
-
-
- | Typ | Titel | Status | Bearbeitet |
-
-
- ${recentSuggestions
- .slice(0, 20)
- .map(
- (s) => `
-
- | ${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type} |
- ${esc(s.title)} |
- ${s.status === "accepted" ? "Angenommen" : "Abgelehnt"} |
- ${formatDate(s.reviewed_at)} |
-
`,
- )
- .join("")}
-
-
-
-
`;
- }
-
- // 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 = `
-
- `;
-
- if (issues.length > 0) {
- healthHtml += `
-
-
-
- | Quelle | Domain | Typ | Status | Details |
-
-
- ${issues
- .map(
- (c) => `
-
- | ${esc(c.name)} |
- ${esc(c.domain || "")} |
- ${CHECK_TYPE_LABELS[c.check_type] || c.check_type} |
- ${c.status === "error" ? "Fehler" : "Warnung"} |
- ${esc(c.message)} |
-
`,
- )
- .join("")}
-
-
-
`;
- } else {
- healthHtml += '
Alle Quellen sind gesund.
';
- }
- healthHtml += "
";
- } else {
- healthHtml = `
-
-
-
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 = `
+
+
+
+
+
+
+ | Typ |
+ Titel |
+ Beschreibung |
+ Priorität |
+ Erstellt |
+ Aktionen |
+
+
+
+ ${pendingSuggestions
+ .map(
+ (s) => `
+
+ | ${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)} |
+
+
+
+ |
+
`,
+ )
+ .join("")}
+
+
+
+
`;
+ } else {
+ suggestionsHtml = `
+
+
+
Keine offenen Vorschläge vorhanden.
+
`;
+ }
+
+ // Vergangene Vorschläge
+ let historyHtml = "";
+ if (recentSuggestions.length > 0) {
+ historyHtml = `
+
+
+
+
+
+ | Typ | Titel | Status | Bearbeitet |
+
+
+ ${recentSuggestions
+ .slice(0, 20)
+ .map(
+ (s) => `
+
+ | ${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type} |
+ ${esc(s.title)} |
+ ${s.status === "accepted" ? "Angenommen" : "Abgelehnt"} |
+ ${formatDate(s.reviewed_at)} |
+
`,
+ )
+ .join("")}
+
+
+
+
`;
+ }
+
+ // 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 = `
+
+ `;
+
+ if (issues.length > 0) {
+ healthHtml += `
+
+
+
+ | Quelle | Domain | Typ | Status | Details | Aktionen |
+
+
+ ${issues
+ .map(
+ (c) => `
+
+ | ${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" ? \`\` : ""} |
+
`,
+ )
+ .join("")}
+
+
+
`;
+ } else {
+ healthHtml += '
Alle Quellen sind gesund.
';
+ }
+ healthHtml += "
";
+ } else {
+ healthHtml = `
+
+
+
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";
+ }
+}