Promote develop -> main (Strategie-Eskalation, Search-Fix bei Warnings, Trend-Delta) #4
@@ -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(
|
||||
|
||||
@@ -723,7 +723,7 @@
|
||||
|
||||
<script src="/static/js/app.js?v=20260509j"></script>
|
||||
<script src="/static/js/sources.js?v=20260509d"></script>
|
||||
<script src="/static/js/source-health.js?v=20260509k"></script>
|
||||
<script src="/static/js/source-health.js?v=20260509l"></script>
|
||||
<script src="/static/js/audit.js?v=20260509d"></script>
|
||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||
</body>
|
||||
|
||||
@@ -260,6 +260,23 @@ function renderHealthDashboard() {
|
||||
return `<span class="${cssClass}" title="${esc(detail)}">${total} ${label}</span> <span class="text-secondary" style="font-size:11px;">(${esc(detail)})</span>`;
|
||||
}
|
||||
|
||||
// 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 ` <span class="text-secondary" style="font-size:11px;" title="unverändert seit letztem Run">(±0)</span>`;
|
||||
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 ` <span class="${cls}" style="font-size:11px;" title="seit letztem Run">(${sign}${d})</span>`;
|
||||
}
|
||||
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 = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -267,9 +284,9 @@ function renderHealthDashboard() {
|
||||
<span class="text-secondary" style="font-size:13px;">
|
||||
Letzter Check: ${healthData.last_check ? formatDateTime(healthData.last_check) : "Noch nie"}
|
||||
|
|
||||
${breakdownLine("error", "text-danger") || `<span class="text-danger">0 Fehler</span>`}
|
||||
${breakdownLine("warning", "text-warning") || `<span class="text-warning">0 Warnungen</span>`}
|
||||
<span class="text-success">${okCount} OK</span>
|
||||
${breakdownLine("error", "text-danger") || `<span class="text-danger">0 Fehler</span>`}${dErr}
|
||||
${breakdownLine("warning", "text-warning") || `<span class="text-warning">0 Warnungen</span>`}${dWarn}
|
||||
<span class="text-success">${okCount} OK</span>${dOk}
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-bar" style="border-bottom:1px solid var(--border, rgba(255,255,255,0.08));">
|
||||
@@ -319,7 +336,10 @@ function renderHealthDashboard() {
|
||||
<td class="text-secondary">${c.tenant_id == null ? '<span style="color:#94a3b8;">global</span>' : esc(c.org_name || ("Org " + c.tenant_id))}</td>
|
||||
<td><span class="badge badge-health-${c.status}">${c.status === "error" ? "Fehler" : (c.status === "warning" ? "Warnung" : "OK")}</span></td>
|
||||
<td class="text-secondary" style="max-width:300px;" title="${esc(c.message || "")}">${esc(c.message || "")}</td>
|
||||
<td>${c.status === "error" && c.check_type === "reachability" ? `<button class="btn btn-secondary btn-small" data-source-id="${c.source_id}" data-source-name="${esc(c.name)}" onclick="searchFix(this)" title="Lösung suchen">${LUCIDE_ICONS.search}</button>` : ""}</td>
|
||||
<td>${(
|
||||
(c.status === "error" && c.check_type === "reachability") ||
|
||||
(c.status === "warning" && c.check_type === "feed_validity")
|
||||
) ? `<button class="btn btn-secondary btn-small" data-source-id="${c.source_id}" data-source-name="${esc(c.name)}" onclick="searchFix(this)" title="Lösung suchen">${LUCIDE_ICONS.search}</button>` : ""}</td>
|
||||
</tr>`;
|
||||
}
|
||||
)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren