Phase 4: Admin-Übersicht erweitern (Stats-Bar + Health-Badge inline + Letzter Treffer)

Backend
- routers/sources.py:
  - GET /api/sources/global/stats NEU: aggregierte Counter
    nach Typ, Total-Articles, Health-Bilanz (errors/warnings/ok)
  - GET /api/sources/global liefert pro Quelle health_status
    (worst-case error > warning > ok, NULL wenn nie gecheckt)

Frontend
- dashboard.html sub-global-sources: Stats-Bar Container oben.
  Tabellenkopf bekommt zwei neue Spalten: Letzter Treffer + Health.
- style.css: .sources-stats-bar (analog Monitor-Style),
  .health-badge mit Varianten error/warning/ok/unknown.
- sources.js:
  - loadGlobalSources lädt parallel /global + /global/stats
  - renderGlobalStats: rendert Stats-Bar mit Total-Quellen,
    Counts pro Typ (aus META), Total-Articles, Health-Counters
  - renderGlobalSources: 9 Spalten statt 7, Letzter-Treffer + Health-Badge,
    typeLabel statt TYPE_LABELS-Direktzugriff
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 03:12:30 +00:00
Ursprung 9350e4538a
Commit 2001815e19
4 geänderte Dateien mit 166 neuen und 7 gelöschten Zeilen

Datei anzeigen

@@ -50,16 +50,45 @@ async function loadGlobalSources() {
populateSelect(document.getElementById("globalFilterType"),
(window.META.types || []).filter(t => t.key !== "excluded"), "Alle Typen");
}
globalSourcesCache = await API.get("/api/sources/global");
const [list, stats] = await Promise.all([
API.get("/api/sources/global"),
API.get("/api/sources/global/stats"),
]);
globalSourcesCache = list;
renderGlobalStats(stats);
renderGlobalSources(globalSourcesCache);
} catch (err) {
console.error("Grundquellen laden fehlgeschlagen:", err);
}
}
function renderGlobalStats(stats) {
const bar = document.getElementById("globalStatsBar");
if (!bar) return;
if (!stats || !stats.by_type) { bar.innerHTML = ""; return; }
const types = window.META && window.META.types ? window.META.types : [];
const parts = [];
parts.push(`<span class="sources-stat-item"><span class="sources-stat-value">${stats.total || 0}</span> Quellen gesamt</span>`);
for (const t of types) {
if (t.key === "excluded") continue;
const v = stats.by_type[t.key] || { count: 0, articles: 0 };
parts.push(`<span class="sources-stat-item"><span class="sources-stat-value">${v.count}</span> ${esc(t.label)}</span>`);
}
parts.push(`<span class="sources-stat-item"><span class="sources-stat-value">${stats.total_articles || 0}</span> Artikel</span>`);
const h = stats.health || { errors: 0, warnings: 0, ok: 0 };
if (h.errors) parts.push(`<span class="sources-stat-item health-error"><span class="sources-stat-value">${h.errors}</span> Fehler</span>`);
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>`);
bar.innerHTML = parts.join("");
}
function renderGlobalSources(sources) {
const tbody = document.getElementById("globalSourceTable");
const cols = 7;
const cols = 9;
if (sources.length === 0) {
tbody.innerHTML = `<tr><td colspan="${cols}" class="text-muted">Keine Grundquellen</td></tr>`;
return;
@@ -87,12 +116,18 @@ function renderGlobalSources(sources) {
const notesRow = hasNotes
? `<tr class="src-notes-row" id="notes-${s.id}" style="display:none;"><td colspan="${cols}" class="src-notes-cell">${esc(s.notes)}</td></tr>`
: '';
const lastSeen = s.last_seen_at ? formatDate(s.last_seen_at) : "-";
const hs = s.health_status || "unknown";
const hsLabel = { error: "Fehler", warning: "Warnung", ok: "OK", unknown: "—" }[hs];
const hsClass = "health-badge-" + (hs === "unknown" ? "unknown" : hs);
html += `<tr>
<td>${infoBtn} ${esc(s.name)}</td>
<td class="text-secondary" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.url || "-")}</td>
<td>${esc(s.domain || "-")}</td>
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
<td>${typeLabel(s.source_type)}</td>
<td class="text-right">${s.article_count || 0}</td>
<td class="text-secondary">${lastSeen}</td>
<td><span class="health-badge ${hsClass}">${hsLabel}</span></td>
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
<td>
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>