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:
@@ -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
|
||||
""")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren