Sonnet-WebSearch "Lösung suchen" für kaputte Quellen

- POST /health/search-fix/{source_id}: Sonnet recherchiert Alternativen
- Button "Lösung suchen" bei Erreichbarkeits-Fehlern im Health-Tab
- Gefundene Lösungen werden automatisch als Vorschläge gespeichert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-08 16:33:11 +01:00
Ursprung 7045a5c657
Commit be0f31344a
2 geänderte Dateien mit 420 neuen und 259 gelöschten Zeilen

Datei anzeigen

@@ -516,3 +516,134 @@ async def run_health_check_now(
"issues": result["issues"], "issues": result["issues"],
"suggestions": suggestion_count, "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}")

Datei anzeigen

@@ -1,259 +1,289 @@
/* Quellen-Health & Vorschläge */ /* Quellen-Health & Vorschläge */
"use strict"; "use strict";
let healthData = null; let healthData = null;
let suggestionsCache = []; let suggestionsCache = [];
const CHECK_TYPE_LABELS = { const CHECK_TYPE_LABELS = {
reachability: "Erreichbarkeit", reachability: "Erreichbarkeit",
feed_validity: "Feed-Validität", feed_validity: "Feed-Validität",
stale: "Aktualität", stale: "Aktualität",
duplicate: "Duplikat", duplicate: "Duplikat",
}; };
const SUGGESTION_TYPE_LABELS = { const SUGGESTION_TYPE_LABELS = {
add_source: "Neue Quelle", add_source: "Neue Quelle",
deactivate_source: "Deaktivieren", deactivate_source: "Deaktivieren",
remove_source: "Entfernen", remove_source: "Entfernen",
fix_url: "URL korrigieren", fix_url: "URL korrigieren",
}; };
const PRIORITY_LABELS = { const PRIORITY_LABELS = {
high: "Hoch", high: "Hoch",
medium: "Mittel", medium: "Mittel",
low: "Niedrig", low: "Niedrig",
}; };
// --- Init --- // --- Init ---
function setupHealthTab() { function setupHealthTab() {
const tab = document.querySelector('#sourceSubTabs .nav-tab[data-subtab="source-health"]'); const tab = document.querySelector('#sourceSubTabs .nav-tab[data-subtab="source-health"]');
if (tab) { if (tab) {
tab.addEventListener("click", () => loadHealthData()); tab.addEventListener("click", () => loadHealthData());
} }
} }
document.addEventListener("DOMContentLoaded", setupHealthTab); document.addEventListener("DOMContentLoaded", setupHealthTab);
// --- Health-Daten laden --- // --- Health-Daten laden ---
async function loadHealthData() { async function loadHealthData() {
try { try {
const [health, suggestions] = await Promise.all([ const [health, suggestions] = await Promise.all([
API.get("/api/sources/health"), API.get("/api/sources/health"),
API.get("/api/sources/suggestions"), API.get("/api/sources/suggestions"),
]); ]);
healthData = health; healthData = health;
suggestionsCache = suggestions; suggestionsCache = suggestions;
renderHealthDashboard(); renderHealthDashboard();
} catch (err) { } catch (err) {
console.error("Health-Daten laden fehlgeschlagen:", err); console.error("Health-Daten laden fehlgeschlagen:", err);
document.getElementById("healthContent").innerHTML = document.getElementById("healthContent").innerHTML =
'<div class="text-muted" style="padding:20px;">Fehler beim Laden der Health-Daten.</div>'; '<div class="text-muted" style="padding:20px;">Fehler beim Laden der Health-Daten.</div>';
} }
} }
function renderHealthDashboard() { function renderHealthDashboard() {
const container = document.getElementById("healthContent"); const container = document.getElementById("healthContent");
if (!container) return; if (!container) return;
// Vorschläge rendern // Vorschläge rendern
const pendingSuggestions = suggestionsCache.filter((s) => s.status === "pending"); const pendingSuggestions = suggestionsCache.filter((s) => s.status === "pending");
const recentSuggestions = suggestionsCache.filter((s) => s.status !== "pending"); const recentSuggestions = suggestionsCache.filter((s) => s.status !== "pending");
let suggestionsHtml = ""; let suggestionsHtml = "";
if (pendingSuggestions.length > 0) { if (pendingSuggestions.length > 0) {
suggestionsHtml = ` suggestionsHtml = `
<div class="card" style="margin-bottom:16px;"> <div class="card" style="margin-bottom:16px;">
<div class="card-header"> <div class="card-header">
<h2>Vorschläge (${pendingSuggestions.length} offen)</h2> <h2>Vorschläge (${pendingSuggestions.length} offen)</h2>
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Typ</th> <th>Typ</th>
<th>Titel</th> <th>Titel</th>
<th>Beschreibung</th> <th>Beschreibung</th>
<th>Priorität</th> <th>Priorität</th>
<th>Erstellt</th> <th>Erstellt</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
${pendingSuggestions ${pendingSuggestions
.map( .map(
(s) => ` (s) => `
<tr> <tr>
<td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td> <td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td>
<td>${esc(s.title)}</td> <td>${esc(s.title)}</td>
<td class="text-secondary" style="max-width:300px;">${esc(s.description || "")}</td> <td class="text-secondary" style="max-width:300px;">${esc(s.description || "")}</td>
<td><span class="badge badge-priority-${s.priority}">${PRIORITY_LABELS[s.priority] || s.priority}</span></td> <td><span class="badge badge-priority-${s.priority}">${PRIORITY_LABELS[s.priority] || s.priority}</span></td>
<td class="text-secondary">${formatDate(s.created_at)}</td> <td class="text-secondary">${formatDate(s.created_at)}</td>
<td style="white-space:nowrap;"> <td style="white-space:nowrap;">
<button class="btn btn-success btn-small" onclick="handleSuggestion(${s.id}, true)">Annehmen</button> <button class="btn btn-success btn-small" onclick="handleSuggestion(${s.id}, true)">Annehmen</button>
<button class="btn btn-danger btn-small" onclick="handleSuggestion(${s.id}, false)">Ablehnen</button> <button class="btn btn-danger btn-small" onclick="handleSuggestion(${s.id}, false)">Ablehnen</button>
</td> </td>
</tr>`, </tr>`,
) )
.join("")} .join("")}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>`; </div>`;
} else { } else {
suggestionsHtml = ` suggestionsHtml = `
<div class="card" style="margin-bottom:16px;"> <div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Vorschläge</h2></div> <div class="card-header"><h2>Vorschläge</h2></div>
<div class="card-body text-muted">Keine offenen Vorschläge vorhanden.</div> <div class="card-body text-muted">Keine offenen Vorschläge vorhanden.</div>
</div>`; </div>`;
} }
// Vergangene Vorschläge // Vergangene Vorschläge
let historyHtml = ""; let historyHtml = "";
if (recentSuggestions.length > 0) { if (recentSuggestions.length > 0) {
historyHtml = ` historyHtml = `
<div class="card" style="margin-bottom:16px;"> <div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Verlauf</h2></div> <div class="card-header"><h2>Verlauf</h2></div>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr><th>Typ</th><th>Titel</th><th>Status</th><th>Bearbeitet</th></tr> <tr><th>Typ</th><th>Titel</th><th>Status</th><th>Bearbeitet</th></tr>
</thead> </thead>
<tbody> <tbody>
${recentSuggestions ${recentSuggestions
.slice(0, 20) .slice(0, 20)
.map( .map(
(s) => ` (s) => `
<tr> <tr>
<td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td> <td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td>
<td>${esc(s.title)}</td> <td>${esc(s.title)}</td>
<td><span class="badge badge-${s.status === "accepted" ? "active" : "inactive"}">${s.status === "accepted" ? "Angenommen" : "Abgelehnt"}</span></td> <td><span class="badge badge-${s.status === "accepted" ? "active" : "inactive"}">${s.status === "accepted" ? "Angenommen" : "Abgelehnt"}</span></td>
<td class="text-secondary">${formatDate(s.reviewed_at)}</td> <td class="text-secondary">${formatDate(s.reviewed_at)}</td>
</tr>`, </tr>`,
) )
.join("")} .join("")}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>`; </div>`;
} }
// Health-Check Ergebnisse // Health-Check Ergebnisse
let healthHtml = ""; let healthHtml = "";
if (healthData && healthData.checks && healthData.checks.length > 0) { if (healthData && healthData.checks && healthData.checks.length > 0) {
const issues = healthData.checks.filter((c) => c.status !== "ok"); const issues = healthData.checks.filter((c) => c.status !== "ok");
const okCount = healthData.checks.filter((c) => c.status === "ok").length; const okCount = healthData.checks.filter((c) => c.status === "ok").length;
healthHtml = ` healthHtml = `
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h2>Health-Check Ergebnisse</h2> <h2>Health-Check Ergebnisse</h2>
<span class="text-secondary" style="font-size:13px;"> <span class="text-secondary" style="font-size:13px;">
Letzter Check: ${healthData.last_check ? formatDate(healthData.last_check) : "Noch nie"} Letzter Check: ${healthData.last_check ? formatDate(healthData.last_check) : "Noch nie"}
&nbsp;|&nbsp; &nbsp;|&nbsp;
<span class="text-danger">${healthData.errors} Fehler</span> &nbsp; <span class="text-danger">${healthData.errors} Fehler</span> &nbsp;
<span class="text-warning">${healthData.warnings} Warnungen</span> &nbsp; <span class="text-warning">${healthData.warnings} Warnungen</span> &nbsp;
<span class="text-success">${okCount} OK</span> <span class="text-success">${okCount} OK</span>
</span> </span>
</div>`; </div>`;
if (issues.length > 0) { if (issues.length > 0) {
healthHtml += ` healthHtml += `
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr><th>Quelle</th><th>Domain</th><th>Typ</th><th>Status</th><th>Details</th></tr> <tr><th>Quelle</th><th>Domain</th><th>Typ</th><th>Status</th><th>Details</th><th>Aktionen</th></tr>
</thead> </thead>
<tbody> <tbody>
${issues ${issues
.map( .map(
(c) => ` (c) => `
<tr> <tr>
<td>${esc(c.name)}</td> <td>${esc(c.name)}</td>
<td class="text-secondary">${esc(c.domain || "")}</td> <td class="text-secondary">${esc(c.domain || "")}</td>
<td>${CHECK_TYPE_LABELS[c.check_type] || c.check_type}</td> <td>${CHECK_TYPE_LABELS[c.check_type] || c.check_type}</td>
<td><span class="badge badge-health-${c.status}">${c.status === "error" ? "Fehler" : "Warnung"}</span></td> <td><span class="badge badge-health-${c.status}">${c.status === "error" ? "Fehler" : "Warnung"}</span></td>
<td class="text-secondary" style="max-width:300px;">${esc(c.message)}</td> <td class="text-secondary" style="max-width:250px;">${esc(c.message)}</td>
</tr>`, <td>${c.status === "error" && c.check_type === "reachability" ? \`<button class="btn btn-secondary btn-small" onclick="searchFix(\${c.source_id}, '\${esc(c.name)}')">Lösung suchen</button>\` : ""}</td>
) </tr>`,
.join("")} )
</tbody> .join("")}
</table> </tbody>
</div>`; </table>
} else { </div>`;
healthHtml += '<div class="card-body text-success">Alle Quellen sind gesund.</div>'; } else {
} healthHtml += '<div class="card-body text-success">Alle Quellen sind gesund.</div>';
healthHtml += "</div>"; }
} else { healthHtml += "</div>";
healthHtml = ` } else {
<div class="card"> healthHtml = `
<div class="card-header"><h2>Health-Check Ergebnisse</h2></div> <div class="card">
<div class="card-body text-muted">Noch kein Health-Check durchgeführt.</div> <div class="card-header"><h2>Health-Check Ergebnisse</h2></div>
</div>`; <div class="card-body text-muted">Noch kein Health-Check durchgeführt.</div>
} </div>`;
}
container.innerHTML = suggestionsHtml + historyHtml + healthHtml;
} container.innerHTML = suggestionsHtml + historyHtml + healthHtml;
}
// --- Vorschlag annehmen/ablehnen ---
async function handleSuggestion(id, accept) { // --- Vorschlag annehmen/ablehnen ---
const action = accept ? "annehmen" : "ablehnen"; async function handleSuggestion(id, accept) {
const suggestion = suggestionsCache.find((s) => s.id === id); const action = accept ? "annehmen" : "ablehnen";
if (!suggestion) return; const suggestion = suggestionsCache.find((s) => s.id === id);
if (!suggestion) return;
if (!confirm(`Vorschlag "${suggestion.title}" ${action}?`)) return;
if (!confirm(`Vorschlag "${suggestion.title}" ${action}?`)) return;
try {
const result = await API.put("/api/sources/suggestions/" + id, { accept }); try {
if (result.action) { const result = await API.put("/api/sources/suggestions/" + id, { accept });
alert(`Ergebnis: ${result.action}`); if (result.action) {
} alert(`Ergebnis: ${result.action}`);
loadHealthData(); }
// Grundquellen-Liste auch aktualisieren loadHealthData();
if (typeof loadGlobalSources === "function") loadGlobalSources(); // Grundquellen-Liste auch aktualisieren
} catch (err) { if (typeof loadGlobalSources === "function") loadGlobalSources();
alert("Fehler: " + err.message); } catch (err) {
} alert("Fehler: " + err.message);
} }
}
// --- Health-Check manuell starten ---
async function runHealthCheck() { // --- Health-Check manuell starten ---
const btn = document.getElementById("runHealthCheckBtn"); async function runHealthCheck() {
if (btn) { const btn = document.getElementById("runHealthCheckBtn");
btn.disabled = true; if (btn) {
btn.textContent = "Läuft..."; btn.disabled = true;
} btn.textContent = "Läuft...";
}
try {
const result = await API.post("/api/sources/health/run"); try {
alert( const result = await API.post("/api/sources/health/run");
`Health-Check abgeschlossen: ${result.checked} Quellen geprüft, ` + alert(
`${result.issues} Probleme gefunden. ` + `Health-Check abgeschlossen: ${result.checked} Quellen geprüft, ` +
`${result.suggestions} neue Vorschläge generiert.`, `${result.issues} Probleme gefunden. ` +
); `${result.suggestions} neue Vorschläge generiert.`,
loadHealthData(); );
} catch (err) { loadHealthData();
alert("Fehler: " + err.message); } catch (err) {
} finally { alert("Fehler: " + err.message);
if (btn) { } finally {
btn.disabled = false; if (btn) {
btn.textContent = "Jetzt prüfen"; btn.disabled = false;
} btn.textContent = "Jetzt prüfen";
} }
} }
}
// --- Hilfsfunktionen ---
function formatDate(dateStr) { // --- Hilfsfunktionen ---
if (!dateStr) return "-"; function formatDate(dateStr) {
try { if (!dateStr) return "-";
const d = new Date(dateStr); try {
return d.toLocaleDateString("de-DE", { const d = new Date(dateStr);
day: "2-digit", return d.toLocaleDateString("de-DE", {
month: "2-digit", day: "2-digit",
year: "numeric", month: "2-digit",
hour: "2-digit", year: "numeric",
minute: "2-digit", hour: "2-digit",
}); minute: "2-digit",
} catch (_) { });
return dateStr; } 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";
}
}