feat(sources): Knopf Jetzt pruefen startet den Health-Check aus dem Portal

- POST /api/sources/health/run (202, bei laufendem Run 409) stoesst den
  Lauf als BackgroundTask an: eigene DB-Connection, run_health_checks +
  generate_suggestions aus den geteilten Diensten, exakt nach dem Vorbild
  des Monitor-Nachtjobs daily_source_health_check. Kein Code-Duplikat.
- GET /api/sources/health/run-status liefert den In-Memory-Status
  (health_run.py, pure Funktionen mit Doppelstart-Sperre, unit-getestet).
  Nach einem Neustart ist der Status idle, ein partieller Stand wird vom
  naechsten Lauf archiviert und ueberschrieben (wie beim Nachtjob).
- Frontend: Knopf Jetzt pruefen + Zuletzt geprueft in der Stats-Leiste
  der Quellenliste, Polling alle 4 s, Abschluss-Toast mit geprueft/
  Probleme/neue Vorschlaege, danach Liste + Aufgaben-Badge neu laden.
  Beim Oeffnen des Reiters wird ein laufender Run aus anderer Session
  erkannt und weiterverfolgt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 18:14:14 +00:00
Ursprung bcfdac158e
Commit 14e766173c
8 geänderte Dateien mit 222 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -114,6 +114,7 @@ async function loadUnifiedSources() {
}
renderUnifiedStats(stats);
filterUnifiedSources();
checkHealthRunOnLoad();
} catch (err) {
console.error("Quellenliste laden fehlgeschlagen:", err);
}
@@ -194,7 +195,75 @@ function renderUnifiedStats(stats) {
if (h.warnings) parts.push(`<span class="sources-stat-item health-warning"><span class="sources-stat-value">${h.warnings}</span> Warnungen</span>`);
if (h.ok) parts.push(`<span class="sources-stat-item health-ok"><span class="sources-stat-value">${h.ok}</span> OK</span>`);
const lastCheck = stats.last_check ? formatDateTime(stats.last_check) : "noch nie";
parts.push(`<span class="sources-stat-item health-run-item"><button class="btn btn-secondary btn-small" id="runHealthCheckBtn" onclick="startHealthRun()">Jetzt prüfen</button><span class="text-secondary" id="healthRunInfo">Zuletzt geprüft ${lastCheck}</span></span>`);
bar.innerHTML = parts.join("");
if (healthRunPollTimer) setHealthRunButton(true);
}
// --- Manueller Health-Run (Knopf in der Stats-Leiste) ---
let healthRunPollTimer = null;
async function startHealthRun() {
try {
await API.post("/api/sources/health/run", {});
setHealthRunButton(true);
beginHealthRunPolling();
showToast("Prüfung gestartet. Das kann einige Minuten dauern.", "info");
} catch (err) {
if (String(err.message || "").includes("bereits")) {
showToast("Es läuft bereits eine Prüfung.", "warning");
setHealthRunButton(true);
beginHealthRunPolling();
} else {
showToast("Start fehlgeschlagen. " + err.message, "error");
}
}
}
function beginHealthRunPolling() {
if (healthRunPollTimer) return;
healthRunPollTimer = setInterval(pollHealthRunStatus, 4000);
}
async function pollHealthRunStatus() {
try {
const st = await API.get("/api/sources/health/run-status");
if (st.running) { setHealthRunButton(true); return; }
// Lauf beendet
if (healthRunPollTimer) { clearInterval(healthRunPollTimer); healthRunPollTimer = null; }
setHealthRunButton(false);
if (st.result) {
const r = st.result;
showToast(`Health-Check abgeschlossen. ${r.checked} geprüft, ${r.issues} Probleme, ${r.suggestions} neue Vorschläge.`, "success");
} else if (st.error) {
showToast("Health-Check fehlgeschlagen. " + st.error, "error");
} else {
showToast("Prüfung beendet, Ergebnis unbekannt (Server wurde neu gestartet).", "info");
}
loadUnifiedSources();
if (typeof refreshTasksBadge === "function") refreshTasksBadge();
} catch (_) { /* nächster Tick versucht es erneut */ }
}
function setHealthRunButton(running) {
const btn = document.getElementById("runHealthCheckBtn");
if (!btn) return;
btn.disabled = running;
btn.textContent = running ? "Prüfung läuft…" : "Jetzt prüfen";
}
// Beim Öffnen des Quellen-Reiters einmal nachsehen, ob (z.B. aus einer anderen
// Session) gerade ein Lauf aktiv ist, und dann das Polling aufnehmen.
async function checkHealthRunOnLoad() {
try {
const st = await API.get("/api/sources/health/run-status");
if (st.running) {
setHealthRunButton(true);
beginHealthRunPolling();
}
} catch (_) { /* unkritisch */ }
}
function renderUnifiedSources(sources) {