diff --git a/src/routers/sources.py b/src/routers/sources.py index 20d3f9b..8c70966 100644 --- a/src/routers/sources.py +++ b/src/routers/sources.py @@ -11,6 +11,7 @@ import aiosqlite from auth import get_current_admin from database import db_dependency from audit import log_action, get_client_ip, row_to_dict +from source_meta import get_meta from config import HEALTH_CHECK_USER_AGENT, HEALTH_CHECK_TIMEOUT_S from shared.source_rules import ( discover_source, @@ -28,6 +29,17 @@ router = APIRouter(prefix="/api/sources", tags=["sources"]) SOURCE_UPDATE_COLUMNS = {"name", "url", "domain", "source_type", "category", "status", "notes"} +@router.get("/meta") +async def get_sources_meta(admin: dict = Depends(get_current_admin)): + """Liefert Kategorien und Typen als Single Source of Truth. + + Frontend lädt das beim Init und befüllt damit Filter-Dropdowns + Label-Lookups. + Damit gibt es keine hardcoded Listen mehr im JS/HTML. + """ + return get_meta() + + + class GlobalSourceCreate(BaseModel): name: str = Field(min_length=1, max_length=200) url: Optional[str] = None diff --git a/src/source_meta.py b/src/source_meta.py new file mode 100644 index 0000000..d351f6c --- /dev/null +++ b/src/source_meta.py @@ -0,0 +1,73 @@ +"""Single Source of Truth für Quellen-Kategorien und -Typen. + +Wird vom Backend über GET /api/sources/meta exportiert. +Frontend (sources.js, source-health.js, dashboard.html) lädt diese +beim Init und befüllt damit Filter-Dropdowns und Label-Lookups. +""" +from typing import TypedDict + + +class CategoryEntry(TypedDict): + key: str + label: str + + +class TypeEntry(TypedDict): + key: str + label: str + + +SOURCE_CATEGORIES: list[CategoryEntry] = [ + {"key": "nachrichtenagentur", "label": "Nachrichtenagentur"}, + {"key": "oeffentlich-rechtlich", "label": "Öffentlich-Rechtlich"}, + {"key": "qualitaetszeitung", "label": "Qualitätszeitung"}, + {"key": "behoerde", "label": "Behörde"}, + {"key": "fachmedien", "label": "Fachmedien"}, + {"key": "think-tank", "label": "Think-Tank"}, + {"key": "international", "label": "International"}, + {"key": "regional", "label": "Regional"}, + {"key": "boulevard", "label": "Boulevard"}, + {"key": "sonstige", "label": "Sonstige"}, + {"key": "cybercrime", "label": "Cybercrime / Hacktivismus"}, + {"key": "cybercrime-leaks", "label": "Cybercrime / Leaks"}, + {"key": "ukraine-russland-krieg", "label": "Ukraine-Russland-Krieg"}, + {"key": "irankonflikt", "label": "Irankonflikt"}, + {"key": "osint-international", "label": "OSINT International"}, + {"key": "extremismus-deutschland", "label": "Extremismus Deutschland"}, + {"key": "russische-staatspropaganda", "label": "Russische Staatspropaganda"}, + {"key": "russische-opposition", "label": "Russische Opposition / Exilmedien"}, + {"key": "syrien-nahost", "label": "Syrien / Nahost"}, +] + + +SOURCE_TYPES: list[TypeEntry] = [ + {"key": "rss_feed", "label": "RSS-Feed"}, + {"key": "web_source", "label": "Webquelle"}, + {"key": "telegram_channel", "label": "Telegram-Kanal"}, + {"key": "podcast_feed", "label": "Podcast-Feed"}, + {"key": "excluded", "label": "Ausgeschlossen"}, +] + + +def get_meta() -> dict: + """Vollständige Meta-Information für Frontend-Konsumenten.""" + return { + "categories": SOURCE_CATEGORIES, + "types": SOURCE_TYPES, + } + + +def category_label(key: str) -> str: + """Lookup: Kategorie-Key -> Label. Fallback: Key selbst.""" + for c in SOURCE_CATEGORIES: + if c["key"] == key: + return c["label"] + return key + + +def type_label(key: str) -> str: + """Lookup: Typ-Key -> Label. Fallback: Key selbst.""" + for t in SOURCE_TYPES: + if t["key"] == key: + return t["label"] + return key diff --git a/src/static/dashboard.html b/src/static/dashboard.html index c0ab006..705858a 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -303,32 +303,9 @@