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:
@@ -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}")
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|
|
|
||||||
<span class="text-danger">${healthData.errors} Fehler</span>
|
<span class="text-danger">${healthData.errors} Fehler</span>
|
||||||
<span class="text-warning">${healthData.warnings} Warnungen</span>
|
<span class="text-warning">${healthData.warnings} Warnungen</span>
|
||||||
<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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren