Phase 3b: Kategorien/Typen aus Backend (/api/sources/meta)
- src/source_meta.py NEU: SOURCE_CATEGORIES + SOURCE_TYPES als
Single Source of Truth (Liste mit {key, label}). category_label/type_label
Lookup-Funktionen, get_meta() liefert das gesamte Set.
- src/routers/sources.py: GET /api/sources/meta ergänzt (admin-auth,
liefert Kategorien + Typen)
- src/static/js/app.js: window.META + loadMeta() + categoryLabel/typeLabel +
populateSelect Helper. Beim DOMContentLoaded wird Meta geladen, befüllt
globale CATEGORY_LABELS und TYPE_LABELS.
- src/static/js/sources.js: hardcoded const CATEGORY_LABELS und TYPE_LABELS
entfernt - werden jetzt aus app.js loadMeta() global gesetzt.
loadGlobalSources() ruft populateSelect() für die Filter-Dropdowns auf.
- src/static/js/source-health.js: gleiche hardcoded Listen entfernt.
- src/static/dashboard.html: <option>-Listen für globalFilterCategory und
globalFilterType entfernt (nur noch default Alle). JS befüllt sie dynamisch.
Ergebnis: Bei einer neuen Kategorie nur source_meta.py anpassen,
keine 3-fach-Pflege mehr in HTML+sources.js+source-health.js.
Dieser Commit ist enthalten in:
73
src/source_meta.py
Normale Datei
73
src/source_meta.py
Normale Datei
@@ -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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren