Phase 6: Verwendungs-Sicht pro Grundquelle

Backend
- /api/sources/global liefert pro Quelle articles_7d, articles_30d und
  tenant_excluded_count (eine aggregierte Query mit CTEs, kein N+1).
- Match-Logik fuer Articles: LOWER(articles.source) = LOWER(sources.name)
  - articles.source_url ist Artikel-URL, NICHT Feed-URL, daher matcht das
    nicht mit sources.url. source-Name-Match liefert sinnvolle Treffer.
- tenant_excluded_count zaehlt distinct organization_ids aus
  user_excluded_domains (per LOWER(domain)-Match).

Frontend
- dashboard.html: zwei neue sortierbare Spalten Aktivitaet (7d/30d) +
  Sperren in der Grundquellen-Tabelle.
- style.css: .activity-cell + .exclude-badge Styles (mit zero-Variante
  fuer ruhigen Look bei keiner Aktivitaet/Sperre).
- sources.js:
  - cols 9 -> 11
  - Render: 7d-Wert fett, 30d-Wert dezent, Tooltip 7 Tage / 30 Tage
  - Sort-Logik: NUMERIC_FIELDS um articles_7d/articles_30d/tenant_excluded_count
    erweitert (numerischer Compare statt localeCompare)
Dieser Commit ist enthalten in:
claude-dev
2026-05-09 03:23:42 +00:00
Ursprung 6b70a7195e
Commit e9ff2bac02
4 geänderte Dateien mit 77 neuen und 21 gelöschten Zeilen

Datei anzeigen

@@ -72,26 +72,48 @@ async def list_global_sources(
pro Zeile zeigen, ohne separate Health-Tab-Abfrage.
"""
cursor = await db.execute("""
WITH article_stats AS (
-- Match per source-Name (case-insensitive). source_url im articles ist die
-- Artikel-URL, nicht die Feed-URL - daher matcht das nicht mit sources.url.
SELECT LOWER(source) AS s_lower,
SUM(CASE WHEN collected_at > datetime('now', '-7 days') THEN 1 ELSE 0 END) AS a7d,
SUM(CASE WHEN collected_at > datetime('now', '-30 days') THEN 1 ELSE 0 END) AS a30d
FROM articles
WHERE collected_at > datetime('now', '-30 days')
AND source IS NOT NULL
GROUP BY LOWER(source)
),
excluded_counts AS (
SELECT LOWER(ued.domain) AS dom,
COUNT(DISTINCT u.organization_id) AS cnt
FROM user_excluded_domains ued
JOIN users u ON u.id = ued.user_id
WHERE ued.domain IS NOT NULL
GROUP BY LOWER(ued.domain)
),
health_agg AS (
SELECT source_id,
MAX(CASE WHEN status = 'error' THEN 3
WHEN status = 'warning' THEN 2
WHEN status = 'ok' THEN 1
ELSE 0 END) AS rank
FROM source_health_checks
GROUP BY source_id
)
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
CASE ha.rank
WHEN 3 THEN 'error'
WHEN 2 THEN 'warning'
WHEN 1 THEN 'ok'
ELSE NULL
END AS health_status,
COALESCE(ast.a7d, 0) AS articles_7d,
COALESCE(ast.a30d, 0) AS articles_30d,
COALESCE(ec.cnt, 0) AS tenant_excluded_count
FROM sources s
LEFT JOIN article_stats ast ON ast.s_lower = LOWER(s.name)
LEFT JOIN excluded_counts ec ON ec.dom = LOWER(s.domain)
LEFT JOIN health_agg ha ON ha.source_id = s.id
WHERE s.tenant_id IS NULL
ORDER BY s.category, s.source_type, s.name
""")