Commits vergleichen

...

6 Commits

Autor SHA1 Nachricht Datum
42a0647cd7 Promote develop → main (2026-05-09 15:05 UTC) 2026-05-09 17:05:22 +02:00
Claude (cleanup)
38a13c0b64 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.
2026-05-09 14:44:20 +00:00
Claude (cleanup)
3a838809c6 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.
2026-05-09 14:38:36 +00:00
Claude (cleanup)
f1680c9f4f ux(quellen-health): Sub-Tabs Vorschläge / Health-Status / Verlauf, Lucide-Icons statt Emojis
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 <div id=sub-source-health>: 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 (&check; &times; ) ersetzt.
  Inline-SVG statt CDN-Library, damit keine externe Abhaengigkeit.

Cache-Buster fuer source-health.js auf 20260509i gebumpt.
2026-05-09 14:26:10 +00:00
Claude (cleanup)
5191962ce0 ux(quellen-health): Verschlankung - Beschreibung gekürzt, Verlauf eingeklappt, schmalere Health-Tabelle, Icon-Buttons
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 <details>-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.
2026-05-09 14:18:04 +00:00
Claude (cleanup)
b6926df84d 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.
2026-05-09 14:03:51 +00:00
4 geänderte Dateien mit 109 neuen und 60 gelöschten Zeilen

Datei anzeigen

@@ -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(

Datei anzeigen

@@ -393,9 +393,17 @@
</div>
</div>
<!-- Quellen-Health (Sub-Tab; Inhalt wird von source-health.js dynamisch in #healthContent gerendert) -->
<!-- Quellen-Health (Sub-Tab) - drei Bereiche als Sub-Sub-Tabs;
source-health.js rendert pro Bereich in den jeweiligen Container. -->
<div class="section" id="sub-source-health">
<div id="healthContent"></div>
<div class="nav-tabs" id="healthSubTabs" style="margin-top:0;">
<button class="nav-tab active" data-healthtab="suggestions">Vorschläge</button>
<button class="nav-tab" data-healthtab="checks">Health-Status</button>
<button class="nav-tab" data-healthtab="verlauf">Verlauf</button>
</div>
<div id="ht-suggestions" class="health-pane active"></div>
<div id="ht-checks" class="health-pane" style="display:none;"></div>
<div id="ht-verlauf" class="health-pane" style="display:none;"></div>
</div>
</div> <!-- /sec-sources -->
@@ -713,9 +721,9 @@
</div>
</div>
<script src="/static/js/app.js?v=20260509d"></script>
<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=20260509g"></script>
<script src="/static/js/source-health.js?v=20260509k"></script>
<script src="/static/js/audit.js?v=20260509d"></script>
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
</body>

Datei anzeigen

@@ -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");

Datei anzeigen

@@ -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: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><polyline points="20 6 9 17 4 12"/></svg>',
x: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
search:'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
refresh:'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
};
// --- 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() {
<tr>
<td><span class="badge badge-suggestion-${s.suggestion_type}">${SUGGESTION_TYPE_LABELS[s.suggestion_type] || s.suggestion_type}</span></td>
<td>${esc(s.title)}</td>
<td class="text-secondary" style="max-width:300px;">${esc(s.description || "")}</td>
<td class="text-secondary" style="max-width:300px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" title="${esc(s.description || "")}">${esc(s.description || "")}</td>
<td><span class="badge badge-priority-${s.priority}">${PRIORITY_LABELS[s.priority] || s.priority}</span></td>
<td class="text-secondary">${formatDateTime(s.created_at)}</td>
<td style="white-space:nowrap;">
${s.suggestion_type === "deactivate_source" && s.source_id ? `<button class="btn btn-secondary btn-small" data-source-id="${s.source_id}" data-source-name="${esc(s.title.split(':')[0] || s.title)}" onclick="searchFix(this)">Lösung suchen</button> ` : ""}
<button class="btn btn-success btn-small" onclick="handleSuggestion(${s.id}, true)">Annehmen</button>
<button class="btn btn-danger btn-small" onclick="handleSuggestion(${s.id}, false)">Ablehnen</button>
${s.suggestion_type === "deactivate_source" && s.source_id ? `<button class="btn btn-secondary btn-small" data-source-id="${s.source_id}" data-source-name="${esc(s.title.split(':')[0] || s.title)}" onclick="searchFix(this)" title="Lösung suchen">${LUCIDE_ICONS.search}</button> ` : ""}
<button class="btn btn-success btn-small" onclick="handleSuggestion(${s.id}, true)" title="Annehmen">${LUCIDE_ICONS.check}</button>
<button class="btn btn-danger btn-small" onclick="handleSuggestion(${s.id}, false)" title="Ablehnen">${LUCIDE_ICONS.x}</button>
</td>
</tr>`,
)
@@ -166,20 +196,23 @@ function renderHealthDashboard() {
</div>`;
}
// 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 = `
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Verlauf</h2></div>
<div class="table-wrap">
<details class="card" style="margin-bottom:16px;">
<summary style="cursor:pointer; padding:14px 18px; list-style:none;">
<span style="color:var(--accent, #C8A851); font-weight:600; font-size:1.02rem;">Verlauf</span>
<span class="text-secondary" style="font-size:13px; margin-left:8px;">(${recentSuggestions.length} erledigte Vorschläge - klick zum Aufklappen)</span>
</summary>
<div class="table-wrap" style="border-top:1px solid var(--border, rgba(255,255,255,0.08));">
<table>
<thead>
<tr><th>Typ</th><th>Titel</th><th>Status</th><th>Bearbeitet</th></tr>
</thead>
<tbody>
${recentSuggestions
.slice(0, 20)
${shown
.map(
(s) => `
<tr>
@@ -193,7 +226,7 @@ function renderHealthDashboard() {
</tbody>
</table>
</div>
</div>`;
</details>`;
}
// Health-Check Ergebnisse
@@ -268,22 +301,27 @@ function renderHealthDashboard() {
<div class="table-wrap">
<table>
<thead>
<tr><th>Quelle</th><th>Domain</th><th>Typ</th><th>Org</th><th>Sprache</th><th>Status</th><th>Details</th><th>Aktionen</th></tr>
<tr><th>Quelle</th><th>Typ</th><th>Org</th><th>Status</th><th>Details</th><th>Aktion</th></tr>
</thead>
<tbody>
${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 `
<tr>
<td>${esc(c.name)}</td>
<td class="text-secondary">${esc(c.domain || "-")}</td>
<td${nameTip}>${esc(c.name)}</td>
<td>${CHECK_TYPE_LABELS[c.check_type] || c.check_type}</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 class="text-secondary">${esc(c.language || "-")}</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:250px;" 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)">Lösung suchen</button>` : ""}</td>
</tr>`,
<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>
</tr>`;
}
)
.join("")}
</tbody>
@@ -323,24 +361,39 @@ function renderHealthDashboard() {
</div>`;
}
// 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 = `
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Verlauf der Health-Check-Runs</h2></div>
<div class="table-wrap">
<table>
<thead><tr><th>Zeitpunkt (Run-Ende)</th><th>Run-ID</th><th>Total</th><th>Fehler</th><th>Warnungen</th><th>OK</th></tr></thead>
<table style="table-layout:fixed; width:100%;">
<colgroup>
<col style="width:200px;">
<col style="width:160px;">
<col style="width:110px;">
<col style="width:130px;">
<col style="width:110px;">
</colgroup>
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Run-ID</th>
<th style="text-align:center;">Fehler</th>
<th style="text-align:center;">Warnungen</th>
<th style="text-align:center;">OK</th>
</tr>
</thead>
<tbody>
${healthHistoryCache.map(r => `
<tr>
<td>${formatDateTime(r.archived_at)}</td>
<td class="text-secondary"><code>${esc(r.run_id)}</code></td>
<td class="text-right">${r.total}</td>
<td class="text-right text-danger">${r.errors || 0}</td>
<td class="text-right text-warning">${r.warnings || 0}</td>
<td class="text-right text-success">${r.ok || 0}</td>
<td class="text-secondary" style="font-size:12px;" title="${esc(r.run_id)}"><code>${esc(String(r.run_id || "").slice(0, 12))}</code></td>
<td class="text-danger" style="text-align:center;">${r.errors || 0}</td>
<td class="text-warning" style="text-align:center;">${r.warnings || 0}</td>
<td class="text-success" style="text-align:center;">${r.ok || 0}</td>
</tr>`).join("")}
</tbody>
</table>
@@ -348,7 +401,17 @@ function renderHealthDashboard() {
</div>`;
}
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 ---