diff --git a/src/routers/sources.py b/src/routers/sources.py index 7a3f8d7..01c447e 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -821,28 +821,6 @@ async def update_suggestion( return {"status": new_status, "action": result_action} -@router.post("/health/run") -async def run_health_check_now( - admin: dict = Depends(get_current_admin), - db: aiosqlite.Connection = Depends(db_dependency), -): - """Health-Check manuell starten.""" - # source_health und source_suggester importieren - from shared.services.source_health import run_health_checks - from shared.services.source_suggester import generate_suggestions - - result = await run_health_checks(db) - suggestion_count = await generate_suggestions(db) - - return { - "checked": result["checked"], - "issues": result["issues"], - "suggestions": suggestion_count, - } - - - - @router.post("/health/run-stream") async def run_health_check_stream( diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 421014e..4c69e88 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -393,9 +393,17 @@ - +
-
+ +
+ +
@@ -713,9 +721,9 @@ - + - +
diff --git a/src/static/js/app.js b/src/static/js/app.js index 95dac4b..a597f3a 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -55,10 +55,10 @@ function logout() { // --- Navigation --- function setupNavTabs() { - document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(tab => { + document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs):not(#healthSubTabs) .nav-tab").forEach(tab => { tab.addEventListener("click", () => { const section = tab.dataset.section; - document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(t => t.classList.remove("active")); + document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs):not(#healthSubTabs) .nav-tab").forEach(t => t.classList.remove("active")); tab.classList.add("active"); document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active")); document.getElementById(`sec-${section}`).classList.add("active"); @@ -405,7 +405,7 @@ document.addEventListener("DOMContentLoaded", () => { function switchToOrg(orgId) { // Switch to orgs tab and open detail - document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs) .nav-tab").forEach(t => t.classList.remove("active")); + document.querySelectorAll(".nav-tabs:not(#orgDetailTabs):not(#sourceSubTabs):not(#healthSubTabs) .nav-tab").forEach(t => t.classList.remove("active")); document.querySelector('.nav-tab[data-section="orgs"]').classList.add("active"); document.querySelectorAll(".app-content > .section").forEach(s => s.classList.remove("active")); document.getElementById("sec-orgs").classList.add("active"); diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index 2927c94..b8622be 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -39,6 +39,15 @@ const PRIORITY_LABELS = { low: "Niedrig", }; +// Lucide-Icons als Inline-SVG-Konstanten (statt CDN-Abhängigkeit oder Emojis). +// 14x14, currentColor erbt vom Button-Style. +const LUCIDE_ICONS = { + check: '', + x: '', + search:'', + refresh:'', +}; + // --- Init --- function setupHealthTab() { const tab = document.querySelector('#sourceSubTabs .nav-tab[data-subtab="source-health"]'); @@ -47,7 +56,25 @@ function setupHealthTab() { } } -document.addEventListener("DOMContentLoaded", setupHealthTab); +// Sub-Sub-Tabs innerhalb von Quellen-Health: Vorschläge / Health-Status / Verlauf. +function setupHealthSubTabs() { + document.querySelectorAll("#healthSubTabs .nav-tab").forEach((tab) => { + tab.addEventListener("click", () => { + const which = tab.dataset.healthtab; + document.querySelectorAll("#healthSubTabs .nav-tab").forEach(t => t.classList.remove("active")); + tab.classList.add("active"); + ["suggestions", "checks", "verlauf"].forEach(name => { + const pane = document.getElementById("ht-" + name); + if (pane) pane.style.display = name === which ? "block" : "none"; + }); + }); + }); +} + +document.addEventListener("DOMContentLoaded", () => { + setupHealthTab(); + setupHealthSubTabs(); +}); // --- Health-Daten laden --- async function loadHealthData(force = false) { @@ -110,8 +137,11 @@ function setHealthFilter(field, value) { } function renderHealthDashboard() { - const container = document.getElementById("healthContent"); - if (!container) return; + // Drei Sub-Panes (statt einer monolithischen Health-Section). + const paneSuggestions = document.getElementById("ht-suggestions"); + const paneChecks = document.getElementById("ht-checks"); + const paneVerlauf = document.getElementById("ht-verlauf"); + if (!paneSuggestions || !paneChecks || !paneVerlauf) return; // Vorschläge rendern const pendingSuggestions = suggestionsCache.filter((s) => s.status === "pending"); @@ -143,13 +173,13 @@ function renderHealthDashboard() { ${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type} ${esc(s.title)} - ${esc(s.description || "")} + ${esc(s.description || "")} ${PRIORITY_LABELS[s.priority] || s.priority} ${formatDateTime(s.created_at)} - ${s.suggestion_type === "deactivate_source" && s.source_id ? ` ` : ""} - - + ${s.suggestion_type === "deactivate_source" && s.source_id ? ` ` : ""} + + `, ) @@ -166,20 +196,23 @@ function renderHealthDashboard() { `; } - // Vergangene Vorschläge + // Vergangene Vorschläge - eingeklappt by default, weil rein historisch. let historyHtml = ""; if (recentSuggestions.length > 0) { + const shown = recentSuggestions.slice(0, 20); historyHtml = ` -
-

Verlauf

-
+
+ + Verlauf + (${recentSuggestions.length} erledigte Vorschläge - klick zum Aufklappen) + +
- ${recentSuggestions - .slice(0, 20) + ${shown .map( (s) => ` @@ -193,7 +226,7 @@ function renderHealthDashboard() {
TypTitelStatusBearbeitet
-
`; + `; } // Health-Check Ergebnisse @@ -268,22 +301,27 @@ function renderHealthDashboard() {
- + ${filtered .map( - (c) => ` + (c) => { + // Domain + Sprache in Tooltip vom Quellnamen, statt eigene Spalten. + const tipParts = []; + if (c.domain) tipParts.push(c.domain); + if (c.language) tipParts.push(c.language); + const nameTip = tipParts.length ? ` title="${esc(tipParts.join(" · "))}"` : ""; + return ` - - + ${esc(c.name)} - - - - `, + + + `; + } ) .join("")} @@ -323,24 +361,39 @@ function renderHealthDashboard() { `; } - // History-View: letzte Runs + // History-View: letzte Runs. Kompakt: Total raus (= errors+warnings+ok), + // Spalten-Widths explizit, Zahlen zentriert, Run-ID gekürzt + leiser. let runsHtml = ""; if (healthHistoryCache.length > 0) { runsHtml = `

Verlauf der Health-Check-Runs

-
QuelleDomainTypOrgSpracheStatusDetailsAktionen
QuelleTypOrgStatusDetailsAktion
${esc(c.name)}${esc(c.domain || "-")}${CHECK_TYPE_LABELS[c.check_type] || c.check_type} ${c.tenant_id == null ? 'global' : esc(c.org_name || ("Org " + c.tenant_id))}${esc(c.language || "-")} ${c.status === "error" ? "Fehler" : (c.status === "warning" ? "Warnung" : "OK")}${esc(c.message || "")}${c.status === "error" && c.check_type === "reachability" ? `` : ""}
${esc(c.message || "")}${c.status === "error" && c.check_type === "reachability" ? `` : ""}
- +
Zeitpunkt (Run-Ende)Run-IDTotalFehlerWarnungenOK
+ + + + + + + + + + + + + + + + ${healthHistoryCache.map(r => ` - - - - - + + + + `).join("")}
ZeitpunktRun-IDFehlerWarnungenOK
${formatDateTime(r.archived_at)}${esc(r.run_id)}${r.total}${r.errors || 0}${r.warnings || 0}${r.ok || 0}${esc(String(r.run_id || "").slice(0, 12))}${r.errors || 0}${r.warnings || 0}${r.ok || 0}
@@ -348,7 +401,17 @@ function renderHealthDashboard() {
`; } - container.innerHTML = suggestionsHtml + historyHtml + healthHtml + runsHtml; + // Statt einer monolithischen Render: drei Sub-Panes, einer pro Sub-Tab. + paneSuggestions.innerHTML = suggestionsHtml; + paneChecks.innerHTML = healthHtml; + paneVerlauf.innerHTML = historyHtml + runsHtml; + + // Tab-Label "Vorschläge" mit Counter der offenen Vorschläge anreichern. + const tabBtnSugg = document.querySelector('#healthSubTabs .nav-tab[data-healthtab="suggestions"]'); + if (tabBtnSugg) { + const open = pendingSuggestions.length; + tabBtnSugg.textContent = open > 0 ? `Vorschläge (${open} offen)` : "Vorschläge"; + } } // --- Vorschlag annehmen/ablehnen ---