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:
@@ -65,10 +65,36 @@ async def list_global_sources(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Alle Grundquellen auflisten (tenant_id IS NULL)."""
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM sources WHERE tenant_id IS NULL ORDER BY category, source_type, name"
|
||||
)
|
||||
"""Alle Grundquellen auflisten (tenant_id IS NULL).
|
||||
|
||||
Liefert pro Quelle den worst-case Health-Status aus source_health_checks
|
||||
(error > warning > ok > unknown). Damit kann das Frontend ein Inline-Badge
|
||||
pro Zeile zeigen, ohne separate Health-Tab-Abfrage.
|
||||
"""
|
||||
cursor = await db.execute("""
|
||||
SELECT s.*,
|
||||
COALESCE((
|
||||
SELECT CASE
|
||||
WHEN MAX(CASE WHEN h.status = 'error' THEN 3
|
||||
WHEN h.status = 'warning' THEN 2
|
||||
WHEN h.status = 'ok' THEN 1
|
||||
ELSE 0 END) = 3 THEN 'error'
|
||||
WHEN MAX(CASE WHEN h.status = 'error' THEN 3
|
||||
WHEN h.status = 'warning' THEN 2
|
||||
WHEN h.status = 'ok' THEN 1
|
||||
ELSE 0 END) = 2 THEN 'warning'
|
||||
WHEN MAX(CASE WHEN h.status = 'error' THEN 3
|
||||
WHEN h.status = 'warning' THEN 2
|
||||
WHEN h.status = 'ok' THEN 1
|
||||
ELSE 0 END) = 1 THEN 'ok'
|
||||
ELSE NULL
|
||||
END
|
||||
FROM source_health_checks h WHERE h.source_id = s.id
|
||||
), NULL) AS health_status
|
||||
FROM sources s
|
||||
WHERE s.tenant_id IS NULL
|
||||
ORDER BY s.category, s.source_type, s.name
|
||||
""")
|
||||
return [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
@@ -174,6 +200,60 @@ async def delete_global_source(
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/global/stats")
|
||||
async def get_global_stats(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Aggregierte Stats für die Grundquellen-Stats-Bar oben im Tab."""
|
||||
cur = await db.execute("""
|
||||
SELECT source_type, COUNT(*) AS count, COALESCE(SUM(article_count), 0) AS articles
|
||||
FROM sources
|
||||
WHERE tenant_id IS NULL AND status = 'active'
|
||||
GROUP BY source_type
|
||||
""")
|
||||
by_type = {}
|
||||
total = 0
|
||||
total_articles = 0
|
||||
for r in await cur.fetchall():
|
||||
d = dict(r)
|
||||
by_type[d["source_type"]] = {"count": d["count"], "articles": d["articles"]}
|
||||
total += d["count"]
|
||||
total_articles += d["articles"]
|
||||
|
||||
# Health-Counter
|
||||
health = {"errors": 0, "warnings": 0, "ok": 0}
|
||||
cur = await db.execute("""
|
||||
SELECT name FROM sqlite_master WHERE type='table' AND name='source_health_checks'
|
||||
""")
|
||||
if await cur.fetchone():
|
||||
cur = await db.execute("""
|
||||
SELECT h.status AS hs, COUNT(DISTINCT h.source_id) AS cnt
|
||||
FROM source_health_checks h
|
||||
JOIN sources s ON s.id = h.source_id
|
||||
WHERE s.tenant_id IS NULL AND s.status = 'active'
|
||||
GROUP BY h.status
|
||||
""")
|
||||
for r in await cur.fetchall():
|
||||
d = dict(r)
|
||||
if d["hs"] == "error":
|
||||
health["errors"] = d["cnt"]
|
||||
elif d["hs"] == "warning":
|
||||
health["warnings"] = d["cnt"]
|
||||
elif d["hs"] == "ok":
|
||||
health["ok"] = d["cnt"]
|
||||
|
||||
return {
|
||||
"by_type": by_type,
|
||||
"total": total,
|
||||
"total_articles": total_articles,
|
||||
"health": health,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tenant")
|
||||
async def list_tenant_sources(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren