Phase 17: Health-Tab Filter + Org-Spalte + History-View + URL-Schema-Fix
Backend: - shared/services/source_health.py: URL ohne https://-Prefix wird normalisiert bevor httpx.get() aufgerufen wird (Bug-Fix: t.me/kanal liess httpx mit ValueError crashen, Synchron mit Monitor-Fix 1ee6c4d). - routers/sources.py /health: Query erweitert um tenant_id, category, language, bias, org_name (LEFT JOIN organizations) - Frontend kann jetzt pro Issue Tenant-Info anzeigen. - routers/sources.py /health/history NEU: letzte N Runs aus source_health_history aggregiert (run_id, archived_at, errors/warnings/ok). Frontend (source-health.js): - healthFilters State: status / check_type / org. - applyHealthFilter() reduziert die Anzeige. - Filter-Bar mit 3 Dropdowns + Counter "X / Y Ergebnisse". - Tabelle erweitert: Org-Spalte ("global" oder Org-Name), Sprache-Spalte. - History-View neu: letzte 10 Runs als Tabelle (Zeitpunkt, Run-ID, Counts). Cache-Buster auf 20260509c gebumpt.
Dieser Commit ist enthalten in:
@@ -578,9 +578,12 @@ async def get_health(
|
||||
cursor = await db.execute("""
|
||||
SELECT
|
||||
h.id, h.source_id, s.name, s.domain, s.url, s.source_type,
|
||||
s.tenant_id, s.category, s.language, s.bias,
|
||||
o.name AS org_name,
|
||||
h.check_type, h.status, h.message, h.details, h.checked_at
|
||||
FROM source_health_checks h
|
||||
JOIN sources s ON s.id = h.source_id
|
||||
LEFT JOIN organizations o ON o.id = s.tenant_id
|
||||
ORDER BY
|
||||
CASE h.status WHEN 'error' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
||||
s.name
|
||||
@@ -605,6 +608,40 @@ async def get_health(
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get("/health/history")
|
||||
async def get_health_history(
|
||||
limit: int = 20,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Liefert die letzten N Health-Check-Runs aus source_health_history.
|
||||
|
||||
Pro Run: run_id, archived_at (Run-Zeitpunkt), Counts pro Status.
|
||||
"""
|
||||
cursor = await db.execute("""
|
||||
SELECT name FROM sqlite_master WHERE type='table' AND name='source_health_history'
|
||||
""")
|
||||
if not await cursor.fetchone():
|
||||
return []
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT run_id,
|
||||
MIN(archived_at) AS archived_at,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errors,
|
||||
SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) AS warnings,
|
||||
SUM(CASE WHEN status = 'ok' THEN 1 ELSE 0 END) AS ok
|
||||
FROM source_health_history
|
||||
GROUP BY run_id
|
||||
ORDER BY archived_at DESC
|
||||
LIMIT ?
|
||||
""", (max(1, min(limit, 100)),))
|
||||
return [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
@router.get("/suggestions")
|
||||
async def get_suggestions(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren