From b6926df84d4adbc0730044ff31edd3f70044793b Mon Sep 17 00:00:00 2001 From: "Claude (cleanup)" Date: Sat, 9 May 2026 14:03:51 +0000 Subject: [PATCH 1/5] cleanup(sources): redundanten /health/run Endpoint entfernen Frontend ruft ausschliesslich /health/run-stream auf. Der Legacy-Endpoint /health/run war ein simples synchrones Pendant ohne Fortschrittsanzeige und wurde nirgends mehr aufgerufen (verifiziert via grep -r im Repo). Schritt 2 der Quellen-Health-Aufraeumung. Reine Code-Saeuberung, keine UX- oder Backend-Verhaltensaenderung. --- src/routers/sources.py | 22 ---------------------- 1 file changed, 22 deletions(-) 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( From 5191962ce0f6fa786aaa5e4ab2c000b232bd880e Mon Sep 17 00:00:00 2001 From: "Claude (cleanup)" Date: Sat, 9 May 2026 14:18:04 +0000 Subject: [PATCH 2/5] =?UTF-8?q?ux(quellen-health):=20Verschlankung=20-=20B?= =?UTF-8?q?eschreibung=20gek=C3=BCrzt,=20Verlauf=20eingeklappt,=20schmaler?= =?UTF-8?q?e=20Health-Tabelle,=20Icon-Buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vier UX-Hebel zusammengelegt, alle reines Frontend: 1. Vorschlaege-Tabelle: Beschreibung als Einzeiler mit Ellipsis; voller Text im title-Tooltip. Spart bei 24 offenen Vorschlaegen ~25 Bildschirmhoehen. 2. Verlauf-Card: standardmaessig eingeklappt via
-Element. Header zeigt nur "Verlauf (N erledigte Vorschlaege - klick zum Aufklappen)". Klick expandiert die Tabelle. 3. Health-Tabelle: Spalten Domain und Sprache aus der Tabelle raus, beide als Tooltip auf dem Quellen-Namen. Tabelle hat statt 8 Spalten nur noch 6, ist schmaler und besser lesbar. 4. Aktionen-Spalten: Text-Buttons ("Annehmen", "Ablehnen", "Lösung suchen") durch kompakte Icon-Buttons ersetzt (✓ ✗ 🔍). Funktion identisch, Tooltip via title-Attribut. Cache-Buster fuer source-health.js auf 20260509h gebumpt. --- src/static/dashboard.html | 2 +- src/static/js/source-health.js | 46 ++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 421014e..eda15ca 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -715,7 +715,7 @@ - +
diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index 2927c94..4c2c219 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -143,13 +143,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 +166,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 +196,7 @@ function renderHealthDashboard() {
TypTitelStatusBearbeitet
-
`; +
`; } // Health-Check Ergebnisse @@ -268,22 +271,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("")} From f1680c9f4fd00f5420e11643d9d793b12d4ad9e8 Mon Sep 17 00:00:00 2001 From: "Claude (cleanup)" Date: Sat, 9 May 2026 14:26:10 +0000 Subject: [PATCH 3/5] =?UTF-8?q?ux(quellen-health):=20Sub-Tabs=20Vorschl?= =?UTF-8?q?=C3=A4ge=20/=20Health-Status=20/=20Verlauf,=20Lucide-Icons=20st?= =?UTF-8?q?att=20Emojis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splittet die Quellen-Health-Section in drei eigene Sub-Tabs auf, damit der User je nach Aufgabe nur den relevanten Bereich sieht und nicht durch die ganze Seite scrollen muss. dashboard.html: - Innerhalb von
: neue nav-tabs healthSubTabs mit drei Buttons (Vorschläge / Health-Status / Verlauf). - Drei Pane-Container ht-suggestions / ht-checks / ht-verlauf, jeweils per inline-style display kontrolliert. source-health.js: - setupHealthSubTabs(): Click-Handler fuer den Tab-Wechsel (toggle .active auf den Buttons + display none/block auf den Panes). - renderHealthDashboard splittet jetzt in drei innerHTML-Calls, einen pro Pane: paneSuggestions <- Vorschlaege offen paneChecks <- Counter + Filter + Tabelle + Mehr-laden paneVerlauf <- erledigte Vorschlaege + Run-Verlauf - Tab-Label "Vorschlaege" wird mit Counter angereichert (z.B. "Vorschlaege (24 offen)"), wenn welche offen sind. - LUCIDE_ICONS-Konstante mit Inline-SVG fuer check, x, search, refresh. Emojis und HTML-Entities (✓ × ) ersetzt. Inline-SVG statt CDN-Library, damit keine externe Abhaengigkeit. Cache-Buster fuer source-health.js auf 20260509i gebumpt. --- src/static/dashboard.html | 14 +++++++-- src/static/js/source-health.js | 56 +++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index eda15ca..0d9c672 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -393,9 +393,17 @@
- +
-
+ +
+ +
@@ -715,7 +723,7 @@ - +
diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index 4c2c219..f851804 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"); @@ -147,9 +177,9 @@ function renderHealthDashboard() {
`, ) @@ -289,7 +319,7 @@ function renderHealthDashboard() { - + `; } ) @@ -356,7 +386,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 --- From 3a838809c6dc735fb2e998f6955aee903b64958e Mon Sep 17 00:00:00 2001 From: "Claude (cleanup)" Date: Sat, 9 May 2026 14:38:36 +0000 Subject: [PATCH 4/5] fix(navigation): #healthSubTabs aus globalem Top-Tab-Handler ausnehmen Der globale setupNavTabs in app.js fing nav-tab-Clicks aus ALLEN nav-tabs ab, ausser #orgDetailTabs und #sourceSubTabs. Das neue #healthSubTabs (aus dem letzten Commit) war nicht in der :not()- Liste und triggerte daher den Top-Level-Handler, der getElementById("sec-suggestions") suchte und null bekam -> Crash beim classList.add("active"). Fix: :not(#healthSubTabs) ergaenzt an allen drei Stellen (setupNavTabs, setupNavTabs Click-Handler, openSection-Helfer in Z. 408). Cache-Buster fuer app.js gebumpt 20260509d -> 20260509j. --- src/static/dashboard.html | 2 +- src/static/js/app.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 0d9c672..190f5fc 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -721,7 +721,7 @@ - + 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"); From 38a13c0b6422948da93cf3b6be32206c0ebc36e4 Mon Sep 17 00:00:00 2001 From: "Claude (cleanup)" Date: Sat, 9 May 2026 14:44:20 +0000 Subject: [PATCH 5/5] ux(quellen-health): Run-Verlauf-Tabelle kompakter - Total-Spalte raus (= errors+warnings+ok, redundant). - Spalten-Widths explizit per colgroup gesetzt: 200/160/110/130/110px, damit die Werte nicht in einer leeren Flaeche rechts kleben. - Header-Bezeichnungen + Werte fuer Counter-Spalten zentriert (statt rechtsbuendig auf gleichmaessig verteilten Spalten). - Run-ID gekuerzt auf 12 Zeichen, kleinerer font-size, voller Wert im title-Tooltip. - Spaltenbeschriftung von "Zeitpunkt (Run-Ende)" -> "Zeitpunkt" (Klammer-Erklaerung war Footnote-Material). Cache-Buster source-health.js auf 20260509k gebumpt. --- src/static/dashboard.html | 2 +- src/static/js/source-health.js | 31 +++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 190f5fc..4c69e88 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -723,7 +723,7 @@ - +
diff --git a/src/static/js/source-health.js b/src/static/js/source-health.js index f851804..b8622be 100644 --- a/src/static/js/source-health.js +++ b/src/static/js/source-health.js @@ -361,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" ? `` : ""}
${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 ? ` ` : ""} + +
${c.tenant_id == null ? 'global' : esc(c.org_name || ("Org " + c.tenant_id))} ${c.status === "error" ? "Fehler" : (c.status === "warning" ? "Warnung" : "OK")} ${esc(c.message || "")}${c.status === "error" && c.check_type === "reachability" ? `` : ""}${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}