Promote develop -> main
This commit was merged in pull request #4.
Dieser Commit ist enthalten in:
@@ -102,18 +102,96 @@ async def generate_stale_deactivation_suggestions(
|
|||||||
return created
|
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:
|
async def generate_suggestions(db: aiosqlite.Connection) -> int:
|
||||||
"""Generiert Quellen-Vorschläge basierend auf Health-Checks und Lückenanalyse.
|
"""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)
|
1. Deterministisch: Karteileichen-Heuristik (article_count=0 oder >60d stumm)
|
||||||
erzeugt sofort deactivate_source-Vorschläge ohne KI-Aufruf.
|
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,
|
und schlägt weitere Verbesserungen vor (add_source, deactivate_source,
|
||||||
fix_url, ...).
|
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)
|
stale_count = await generate_stale_deactivation_suggestions(db)
|
||||||
|
strategy_count = await generate_strategy_escalation_suggestions(db)
|
||||||
|
|
||||||
logger.info("Starte Quellen-Vorschläge via Haiku...")
|
logger.info("Starte Quellen-Vorschläge via Haiku...")
|
||||||
|
|
||||||
@@ -267,15 +345,15 @@ Nur das JSON-Array, kein anderer Text."""
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Quellen-Vorschläge: {count} neue Vorschläge generiert via Haiku "
|
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"(Haiku: {usage.input_tokens} in / {usage.output_tokens} out / "
|
||||||
f"${usage.cost_usd:.4f})"
|
f"${usage.cost_usd:.4f})"
|
||||||
)
|
)
|
||||||
return count + stale_count
|
return count + stale_count + strategy_count
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True)
|
logger.error(f"Fehler bei Quellen-Vorschlägen: {e}", exc_info=True)
|
||||||
return stale_count
|
return stale_count + strategy_count
|
||||||
|
|
||||||
|
|
||||||
async def apply_suggestion(
|
async def apply_suggestion(
|
||||||
|
|||||||
@@ -723,7 +723,7 @@
|
|||||||
|
|
||||||
<script src="/static/js/app.js?v=20260509j"></script>
|
<script src="/static/js/app.js?v=20260509j"></script>
|
||||||
<script src="/static/js/sources.js?v=20260509d"></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>
|
<script src="/static/js/audit.js?v=20260509d"></script>
|
||||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||||
</body>
|
</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>`;
|
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 = `
|
healthHtml = `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -267,9 +284,9 @@ function renderHealthDashboard() {
|
|||||||
<span class="text-secondary" style="font-size:13px;">
|
<span class="text-secondary" style="font-size:13px;">
|
||||||
Letzter Check: ${healthData.last_check ? formatDateTime(healthData.last_check) : "Noch nie"}
|
Letzter Check: ${healthData.last_check ? formatDateTime(healthData.last_check) : "Noch nie"}
|
||||||
|
|
|
|
||||||
${breakdownLine("error", "text-danger") || `<span class="text-danger">0 Fehler</span>`}
|
${breakdownLine("error", "text-danger") || `<span class="text-danger">0 Fehler</span>`}${dErr}
|
||||||
${breakdownLine("warning", "text-warning") || `<span class="text-warning">0 Warnungen</span>`}
|
${breakdownLine("warning", "text-warning") || `<span class="text-warning">0 Warnungen</span>`}${dWarn}
|
||||||
<span class="text-success">${okCount} OK</span>
|
<span class="text-success">${okCount} OK</span>${dOk}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-bar" style="border-bottom:1px solid var(--border, rgba(255,255,255,0.08));">
|
<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 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><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 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>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren