29 Stellen im Frontend lokalisiert (Toasts: Lage aktualisiert/geloescht/
archiviert/wiederhergestellt, Recherche abgebrochen, Daten aktualisiert,
Quelle hinzugefuegt/aktualisiert, Bericht heruntergeladen, kein RSS;
Confirms: Lage loeschen, Recherche abbrechen; Button-States: Wird
gestartet/abgebrochen/erstellt/gesendet, Suche Feeds, Quelle speichern;
Lizenz: abgelaufen/keine/Org-deaktiviert -- Nur Lesezugriff;
Notification-Center: Titel, Alle gelesen, Keine Benachrichtigungen;
Empty-States: Kein Vorfall ausgewaehlt; Map: Orte einlesen + Tooltip,
Keine Orte erkannt; Modal-Hint: Nur deutschsprachige Quellen). 30+
neue i18n-Keys. Cache-Buster app.js auf v=20260514c.
app.js:1037-1043 setzte den Text der notify-summary-Checkbox dynamisch
auf Neues Lagebild / Neuer Recherchebericht und damit das data-i18n-
Attribut zurueck. Jetzt ueber T() mit Forschungs-/Lagebild-Varianten.
Neuer Key modal.notify.summary_research.
LayoutManager.applyTypeLabels(layout.js:58-65) und App-Render
(app.js:1063,1081) ueberschreiben die Tab-Texte je nach Lage-Typ.
Beides nutzt jetzt T() mit DE-Fallback. Neue Keys tab.summary_short
und tab.summary_report. Cache-Buster layout.js + app.js gebumpt.
Bei Phase 6 wurde components.js und i18n.js gebumpt, app.js aber nicht.
Browser zogen die alte app.js ohne I18N-Init aus dem Cache, sodass
eng_demo-Nutzer eine deutsche Oberflaeche sahen.
STAGING_MODE deaktivierte bisher den Org-Switcher im Frontend, weil keine
Demo-Besucher zwischen Mandanten hoppen sollten. Mit eng_demo brauchen
wir aber bewussten Zugriff auf alle Sprach-Mandanten via Switcher. Der
Token-Budget-Schutz (license_service._staging_mode) bleibt unveraendert.
Backend:
- UserMeResponse um output_language (de | en) erweitert.
- /auth/me liefert die Org-Sprache aus organization_settings.
Frontend:
- Neu: static/js/i18n.js mit T(key)-Helper, I18N.load(lang) und
applyDom() ueber data-i18n + data-i18n-attr.
- Neu: static/i18n/de.json + en.json (sichtbare Bereiche: Sidebar,
Header, Modal-Titel, Faktencheck-Status, Refresh-Hinweise).
- dashboard.html: i18n.js Script-Tag vor api.js, data-i18n auf den
prominenten Strings (Abmelden, + Neuer Fall, Alle/Eigene, Sidebar-
Sektionen, Bericht exportieren, Faktencheck-Tab, Lage anlegen).
Tutorial.init() entfernt aus DOMContentLoaded.
- components.js: factCheckLabels/Tooltips/ChipLabels als Getter ueber
T() mit DE-Fallbacks.
- app.js: vor Setup wird I18N.load(user.output_language) aufgerufen und
applyDom() ausgefuehrt. Tutorial.init() laeuft nur bei lang === de.
Phase 6 von 8 (eng_demo / Org-Sprache).
- email_utils/templates.magic_link_login_email + incident_notification_email
nehmen jetzt lang Parameter (de | en).
- routers/auth.request_magic_link zieht Sprache aus der Org des Users und
uebergibt sie ans Template.
- agents/orchestrator._send_email_notifications_for_incident lokalisiert
ebenfalls und gibt lang an incident_notification_email durch.
- DB-Notification-Texte (refresh_summary, new_articles) sind in der
Pipeline org-sprach-relativ (englische Variante: "3 new articles", etc.).
Status-Change-Notifications: Codes (confirmed/contradicted) bleiben, FE
uebersetzt sie in Phase 6.
Phase 5 von 8 (eng_demo / Org-Sprache).
- Neue Spalte sources.primary_language (ISO-2-Code) mit Backfill aus dem
Freitext-Feld language (Erste Sprache vor /-Trennung). Edge-Cases wie
Iran Military Magazine (English) [Farsi/Arabisch] landen als fa und
koennen ueber das Verwaltungsportal manuell justiert werden.
- get_source_rules(tenant_id) bestimmt die Org-Sprache und bucketed Feeds
nach primary (=Org-Sprache) / international (=alle anderen) / behoerden
(Kategorie behoerde). Bei tenant_id=None oder Helper-Fehler default de.
- rss_parser.search_feeds unveraendert in Logik (international=False
laesst weiterhin alle ausser dem international-Bucket durch), Kommentare
generischer formuliert.
Phase 3 von 8 (eng_demo / Org-Sprache).
- OUTPUT_LANGUAGE Konstante aus config.py entfernt (jetzt pro Org in
organization_settings).
- Orchestrator laedt output_language einmal pro Refresh aus der Org-Sprache.
- researcher.search(), analyzer.analyze/.analyze_incremental/.generate_latest_developments,
factchecker.check/.check_incremental/.check_incremental_twophase bekommen
output_language als Parameter (Default Deutsch).
- LANG_INTERNATIONAL / LANG_GERMAN_ONLY (+ Deep-Varianten) sind Funktionen,
die je nach output_language die Sprachanweisung erzeugen (Deutsch | English
| Fallback).
- Sprachfilter in researcher.search ist org-relativ: bei nicht-international
werden Artikel mit Sprache != output_language_iso gefiltert.
Phase 2 von 8 (eng_demo / Org-Sprache). Bestandsorgs unveraendert, weil
Default-Setting weiterhin de (siehe Phase-1-Migration).
Neue Tabelle organization_settings (organization_id, key, value) als KV-Store
fuer Org-spezifische Konfiguration. Erster Use-Case: output_language (de|en).
Bestandsorgs werden per Migration auf de gesetzt.
Helper services/org_settings.py mit get_org_setting / set_org_setting /
get_org_language / language_display. In-Memory-Cache TTL 60s.
Phase 1 von 8 (eng_demo / Org-Sprache).
Beim Öffnen des Bearbeiten-Dialogs einer Recherche-Lage (type=research) hat
toggleTypeDefaults() den Aktualisierungs-Select hartcodiert auf manual gesetzt
und damit den tatsächlichen DB-Wert im UI verdeckt. User glaubte, manuell sei
gewählt, in der DB stand aber auto und die Lage lief weiter im Auto-Refresh.
Fix: toggleTypeDefaults erhält einen optionalen Parameter preserveMode.
handleEdit ruft mit preserveMode=true auf, damit der DB-Wert respektiert
wird; bei Typ-Wechsel und Neuanlage bleibt der Default-Reset auf manual
für research erhalten.
Cache-Buster app.js: 20260501h -> 20260512a.
Endpoints unter /api/sources/classification/* weg, Service-Module (source_classifier, external_reputation) gelöscht. Quellen-Modal verliert Tab Klassifikations-Review, Klassifikations-Section in der Edit-Form, alle Bulk-Buttons (Sync, Klassifikation starten, Bulk-Approve). API-Methoden in api.js entfernt, alignment-Helper raus, saveSource entschlackt.
Read-Only bleibt: Filter-Dropdowns über der Quellenliste (Politik, Medientyp, Reliability, Externe Reputation, Alignment) und Inline-Badges (_renderClassificationBadges + Label-Maps in components.js). Kunde sieht nur freigegebene Werte.
GET /api/sources liefert weiter Klassifikations-Felder + alignments für die Anzeige; SourceCreate/SourceUpdate akzeptieren keine Klassifikations-Felder mehr.
Bulk-Klassifikations-Skripte entfernt — Pflege läuft über Verwaltungs-UI.
Live-Test heute zeigte: Strategie-Eskalations-Heuristik hat keine Vorschlaege
erzeugt, obwohl Verfassungsschutz und Rheinische Post beide fetch_strategy=
googlebot UND status=error haben. Grund: die Karteileichen-Heuristik lief
zuerst und fing diese Sources schon ein (article_count=0, weil googlebot-
Workaround blockiert), sodass die Doppel-Vermeidung der Strategie-
Eskalations-Stufe alles uebersprungen hat.
Fix: Reihenfolge in generate_suggestions umgekehrt. Strategie-Eskalation
zuerst (spezifischere Diagnose mit Begruendung "Workaround greift nicht:
HTTP 403"), Karteileichen danach (generische Auffanglogik).
Neue Funktion generate_strategy_escalation_suggestions(db) erkennt aktive
Quellen, deren fetch_strategy bereits auf googlebot oder paywall eskaliert
wurde, beim Reachability-Check aber weiterhin status=error melden.
Beispiel: Rheinische Post hat fetch_strategy=googlebot, kriegt aber HTTP 403.
-> Auch der Googlebot-UA-Workaround greift nicht. Quelle wird automatisch
als deactivate-Vorschlag mit priority=high markiert.
Doppel-Vermeidung wie in der Karteileichen-Heuristik: nur wenn fuer die
source_id noch kein pending deactivate-Vorschlag existiert.
Aufgerufen in generate_suggestions als zweite deterministische Stufe,
zwischen Karteileichen-Heuristik und Haiku-Aufruf. Counter im Log
gibt jetzt alle drei Quellen-Beitraege getrennt aus.
Neue Funktion generate_stale_deactivation_suggestions(db, days_threshold=60)
erzeugt deactivate_source-Vorschlaege fuer aktive Quellen, die entweder
- noch nie einen Artikel geliefert haben (article_count=0), oder
- seit mehr als 60 Tagen stumm sind (last_seen_at < now - 60d).
Reine SQL-Heuristik, kein KI-Aufruf. Wird zu Beginn von generate_suggestions
ausgefuehrt, vor dem bestehenden Haiku-Lauf.
Doppel-Vermeidung: existiert fuer eine source_id schon ein pending
deactivate_source-Vorschlag, wird kein neuer eingefuegt.
Hintergrund: Aktuell sind 106 Quellen mit Warning "Noch nie Artikel
geliefert" und einige weitere mit "Letzter Artikel vor 49 Tagen" o.ae.
Diese fluten den Health-Status-Tab. Mit der neuen Heuristik wandern sie
automatisch in die Vorschlaege-Liste, wo der Admin sie per Klick
deaktivieren kann.
Schwelle 60 Tage als Konstante STALE_DEACTIVATE_THRESHOLD_DAYS oben
in der Datei, falls spaeter noch justiert werden soll.
removepaywall.com liefert HTML (Article-Renderer), nicht XML - der
Feed-Validity-Check schlug daher fehl mit "Kein gueltiger RSS/Atom-Feed".
Korrektur:
- paywall: Feed-URL direkt mit Browser-UA laden (kein URL-Rewrite).
- Bei paywall + 4xx: status=warning (erwartbar), Feed-Validity skippen.
- removepaywall.com bleibt im Researcher-Prompt fuer Article-Inhalte
(das ist der korrekte Use-Case).
removepaywall.com liefert HTML (Article-Renderer), nicht XML - der
Feed-Validity-Check schlug daher fehl mit "Kein gueltiger RSS/Atom-Feed".
Korrektur:
- paywall: Feed-URL direkt mit Browser-UA laden (kein URL-Rewrite).
- Bei paywall + 4xx: status=warning (erwartbar), Feed-Validity skippen.
- removepaywall.com bleibt im Researcher-Prompt fuer Article-Inhalte
(das ist der korrekte Use-Case).
User-Korrektur: die echte Service-Domain heisst removepaywall.com (Singular).
removepaywalls.com (Plural) liefert HTTP 403 - vermutlich nicht der gleiche
Service oder gar nicht mehr existent.
Betrifft:
- services/source_health.py: REMOVEPAYWALLS_PREFIX-Konstante (Phase 18)
- agents/researcher.py: Claude-Prompts fuer Paywall-Hinweise (zwei Stellen)
Verifiziert mit curl: removepaywall.com -> 200, removepaywalls.com -> 403.
User-Korrektur: die echte Service-Domain heisst removepaywall.com (Singular).
removepaywalls.com (Plural) liefert HTTP 403 - vermutlich nicht der gleiche
Service oder gar nicht mehr existent.
Betrifft:
- services/source_health.py: REMOVEPAYWALLS_PREFIX-Konstante (Phase 18)
- agents/researcher.py: Claude-Prompts fuer Paywall-Hinweise (zwei Stellen)
Verifiziert mit curl: removepaywall.com -> 200, removepaywalls.com -> 403.
Pro Quelle ein Feld sources.fetch_strategy (default | googlebot | paywall | skip):
- default: normaler UA, Retry mit Googlebot bei 403/406/429.
- googlebot: direkt mit Googlebot-UA (fuer SEO-freundliche Sites).
- paywall: Anfrage via removepaywalls.com (fuer Spiegel+/SZ+/FT etc.).
- skip: Health-Check ueberspringen (bekannte unerreichbare Quellen wie Login-only).
Pre-Flagging in der Migration: FT/WSJ/NZZ/Handelsblatt/WiWo -> paywall,
Rheinische Post/Verfassungsschutz -> googlebot.
(Test mit den vier prominent fehlerhaften Quellen zeigt: FT/RP/Verfassungsschutz
sind besonders streng, gehen auch nicht ueber Googlebot/removepaywalls durch.
Fuer milder restriktive Quellen wirkt der Retry-Mechanismus.)
Pro Quelle ein Feld sources.fetch_strategy (default | googlebot | paywall | skip):
- default: normaler UA, Retry mit Googlebot bei 403/406/429.
- googlebot: direkt mit Googlebot-UA (fuer SEO-freundliche Sites).
- paywall: Anfrage via removepaywalls.com (fuer Spiegel+/SZ+/FT etc.).
- skip: Health-Check ueberspringen (bekannte unerreichbare Quellen wie Login-only).
Pre-Flagging in der Migration: FT/WSJ/NZZ/Handelsblatt/WiWo -> paywall,
Rheinische Post/Verfassungsschutz -> googlebot.
(Test mit den vier prominent fehlerhaften Quellen zeigt: FT/RP/Verfassungsschutz
sind besonders streng, gehen auch nicht ueber Googlebot/removepaywalls durch.
Fuer milder restriktive Quellen wirkt der Retry-Mechanismus.)
Telegram-Quellen mit url=t.me/kanal (ohne https:// Prefix) liessen httpx
mit "ValueError: unknown url type" crashen. Fix: vor dem Request
https:// vorne anhaengen wenn kein Schema vorhanden ist.
Beobachtet auf Live: 110 Health-Errors, davon einige Telegram-Kanaele
mit "ValueError: unknown url type:" als Fehlermeldung.
Telegram-Quellen mit url=t.me/kanal (ohne https:// Prefix) liessen httpx
mit "ValueError: unknown url type" crashen. Fix: vor dem Request
https:// vorne anhaengen wenn kein Schema vorhanden ist.
Beobachtet auf Live: 110 Health-Errors, davon einige Telegram-Kanaele
mit "ValueError: unknown url type:" als Fehlermeldung.
Phase 2 hatte die Verbesserungen nur in der Verwaltung
(src/shared/services/source_health.py). Der Daily-Health-Check laeuft aber
im Monitor-Backend (Cron 04:00 UTC) und nutzte deshalb weiter den alten
Code - Folge:
- Tenant-Quellen wurden NIE gecheckt (0 Eintraege in source_health_checks
fuer tenant_id IS NOT NULL).
- source_health_history blieb leer.
Diese Aenderung holt die Phase-2-Logik in den Monitor:
- services/source_health.py: Verwaltung-Version 1:1 uebernommen
(tenant_id-Filter weg + History-Save vor DELETE + UA/Timeout aus config).
- config.py: HEALTH_CHECK_USER_AGENT + HEALTH_CHECK_TIMEOUT_S ergaenzt.
Manueller Test auf Staging-Monitor:
283 Quellen geprueft, 253 Issues, 61 davon Tenant-Quellen.
History 0 -> 458 Eintraege.
Damit ist die shared/-LOCKED-FILES-Markierung in der Verwaltung obsolet -
beide Repos haben jetzt den gleichen Code.
Phase 2 hatte die Verbesserungen nur in der Verwaltung
(src/shared/services/source_health.py). Der Daily-Health-Check laeuft aber
im Monitor-Backend (Cron 04:00 UTC) und nutzte deshalb weiter den alten
Code - Folge:
- Tenant-Quellen wurden NIE gecheckt (0 Eintraege in source_health_checks
fuer tenant_id IS NOT NULL).
- source_health_history blieb leer.
Diese Aenderung holt die Phase-2-Logik in den Monitor:
- services/source_health.py: Verwaltung-Version 1:1 uebernommen
(tenant_id-Filter weg + History-Save vor DELETE + UA/Timeout aus config).
- config.py: HEALTH_CHECK_USER_AGENT + HEALTH_CHECK_TIMEOUT_S ergaenzt.
Manueller Test auf Staging-Monitor:
283 Quellen geprueft, 253 Issues, 61 davon Tenant-Quellen.
History 0 -> 458 Eintraege.
Damit ist die shared/-LOCKED-FILES-Markierung in der Verwaltung obsolet -
beide Repos haben jetzt den gleichen Code.
Beim Anlegen einer neuen Lage ist der Schalter "Internationale Quellen einbeziehen"
ab jetzt standardmaessig DEAKTIVIERT.
Hintergrund: Bei lokalen DACH-Ereignissen (Tier-/Personenstoryen wie
"Buckelwal timmy") hat der "international=True"-Default zu schlechteren
Treffern gefuehrt, weil Claude in Deutsch UND Englisch suchte und die
englische Berichterstattung haeufig fehlt. Excluded-Sources- und
Boulevard-Filter haben das Problem zusaetzlich verschaerft.
Aenderungen:
- src/models.py IncidentCreate.international_sources: bool=True -> False
(nur das Pydantic-Default beim Create-Endpoint - IncidentResponse/Incident
bleiben True, weil das die DB-Werte bestehender Lagen reflektiert)
- src/static/dashboard.html: <input id="inc-international" checked> -> ohne checked
(UI-Default ist jetzt unchecked, User muss bewusst aktivieren fuer
internationale Lagen)
- Tooltip-Text ergaenzt: "Deaktiviert (Standard): ... empfohlen fuer DACH-Lagen."
Bestandslagen sind nicht betroffen - DB-Schema-Default INTEGER DEFAULT 1
bleibt unveraendert, fuer alle existierenden Lagen behaelt international
seinen aktuellen Wert.
Damit ist die Buckelwal-Diagnose komplett geloest:
- Bug 1 (rss_parser min_matches adaptiv) seit a08df3d auf main
- Bug 2 (Eigennamen-Pflicht-Keywords) seit e83f80d auf main
- Bug 3 (international-Default) jetzt auf develop, gleich Cherry-pick auf main
Beim Anlegen einer neuen Lage ist der Schalter "Internationale Quellen einbeziehen"
ab jetzt standardmaessig DEAKTIVIERT.
Hintergrund: Bei lokalen DACH-Ereignissen (Tier-/Personenstoryen wie
"Buckelwal timmy") hat der "international=True"-Default zu schlechteren
Treffern gefuehrt, weil Claude in Deutsch UND Englisch suchte und die
englische Berichterstattung haeufig fehlt. Excluded-Sources- und
Boulevard-Filter haben das Problem zusaetzlich verschaerft.
Aenderungen:
- src/models.py IncidentCreate.international_sources: bool=True -> False
(nur das Pydantic-Default beim Create-Endpoint - IncidentResponse/Incident
bleiben True, weil das die DB-Werte bestehender Lagen reflektiert)
- src/static/dashboard.html: <input id="inc-international" checked> -> ohne checked
(UI-Default ist jetzt unchecked, User muss bewusst aktivieren fuer
internationale Lagen)
- Tooltip-Text ergaenzt: "Deaktiviert (Standard): ... empfohlen fuer DACH-Lagen."
Bestandslagen sind nicht betroffen - DB-Schema-Default INTEGER DEFAULT 1
bleibt unveraendert, fuer alle existierenden Lagen behaelt international
seinen aktuellen Wert.
Damit ist die Buckelwal-Diagnose komplett geloest:
- Bug 1 (rss_parser min_matches adaptiv) seit a08df3d auf main
- Bug 2 (Eigennamen-Pflicht-Keywords) seit e83f80d auf main
- Bug 3 (international-Default) jetzt auf develop, gleich Cherry-pick auf main
KEYWORD_EXTRACTION_PROMPT explizit erweitert:
- Eigennamen/Tiernamen/Personennamen aus dem THEMA als ZWINGEND markiert.
- Hinweis dass DE und EN identisch sein duerfen (Eigennamen).
- Klar gesagt: bei spezifischen Begriffen (>=7 Zeichen) reicht 1 Treffer in
RSS-Headlines (passt zu rss_parser.py adaptive Schwelle aus a08df3d).
Code-Post-Processing (researcher.py _extract_keywords):
- Nach dem Parser werden Lagentitel-Woerter (>=4 Zeichen, nicht in Stopwords)
ggf. in die Keyword-Liste injiziert, falls Haiku sie weggelassen hat.
- Verhindert konkret den "Buckelwal timmy"-Bug: "timmy" fehlte in Haikus
Liste, damit fielen Headlines mit nur "Buckelwal" durch das min_matches.
Hintergrund: Memory-Eintrag rss_match_und_keyword_bug.md, Bug 2 von 3.
Bug 1 (rss_parser min_matches adaptiv) ist seit Commit a08df3d auf Live.
Bug 3 (international=True default) bleibt offen, ist primaer UX-Frage.
KEYWORD_EXTRACTION_PROMPT explizit erweitert:
- Eigennamen/Tiernamen/Personennamen aus dem THEMA als ZWINGEND markiert.
- Hinweis dass DE und EN identisch sein duerfen (Eigennamen).
- Klar gesagt: bei spezifischen Begriffen (>=7 Zeichen) reicht 1 Treffer in
RSS-Headlines (passt zu rss_parser.py adaptive Schwelle aus a08df3d).
Code-Post-Processing (researcher.py _extract_keywords):
- Nach dem Parser werden Lagentitel-Woerter (>=4 Zeichen, nicht in Stopwords)
ggf. in die Keyword-Liste injiziert, falls Haiku sie weggelassen hat.
- Verhindert konkret den "Buckelwal timmy"-Bug: "timmy" fehlte in Haikus
Liste, damit fielen Headlines mit nur "Buckelwal" durch das min_matches.
Hintergrund: Memory-Eintrag rss_match_und_keyword_bug.md, Bug 2 von 3.
Bug 1 (rss_parser min_matches adaptiv) ist seit Commit a08df3d auf Live.
Bug 3 (international=True default) bleibt offen, ist primaer UX-Frage.
Beide Files hatten Doppel-Encoded UTF-8 in Docstrings, Kommentaren und
Prompt-Strings (z.B. "prüft" statt "prüft", "Vorschläge" statt
"Vorschläge"). ftfy hat das automatisch repariert.
Hauptauswirkungen:
- Logs sind jetzt mit echten Umlauten lesbar
- Claude/Haiku-Prompts in source_suggester.py (Quellen-Vorschlaege via KI)
bekommen jetzt korrekte deutsche Umlaute - sollte bessere Antworten geben
Daneben hat ftfy line-endings normalisiert, daher der grosse Diff in
source_health.py - inhaltlich nur Mojibake-Reparatur.
Verifiziert mit:
grep -cE "ä|ö|ü|ß|Ä|Ö|Ü" src/services/*.py
-> 0 Treffer
Beide Files hatten Doppel-Encoded UTF-8 in Docstrings, Kommentaren und
Prompt-Strings (z.B. "prüft" statt "prüft", "Vorschläge" statt
"Vorschläge"). ftfy hat das automatisch repariert.
Hauptauswirkungen:
- Logs sind jetzt mit echten Umlauten lesbar
- Claude/Haiku-Prompts in source_suggester.py (Quellen-Vorschlaege via KI)
bekommen jetzt korrekte deutsche Umlaute - sollte bessere Antworten geben
Daneben hat ftfy line-endings normalisiert, daher der grosse Diff in
source_health.py - inhaltlich nur Mojibake-Reparatur.
Verifiziert mit:
grep -cE "ä|ö|ü|ß|Ä|Ö|Ü" src/services/*.py
-> 0 Treffer
False positive bei sync_eu_disinfo: t.me wurde als Quelle markiert, weil
EUvsDisinfo anonyme Telegram-Posts unter der Plattform-Domain aggregiert.
Eine Allowlist von Plattform-Domains schliesst diese Falle aus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/services/source_classifier.py: classify_source(db, id) ruft Haiku mit
strukturiertem Prompt (4 Achsen + state_affiliated + country + Konfidenz)
und schreibt Vorschlaege in proposed_*-Spalten. bulk_classify(db, limit)
iteriert sequenziell ueber unklassifizierte Quellen.
- API-Endpoints (alle hinter Auth, globale Quellen nur fuer org_admin):
- GET /api/sources/classification/stats
- GET /api/sources/classification/queue
- POST /api/sources/{id}/classification/approve (proposed_* -> echte Felder)
- POST /api/sources/{id}/classification/reject (proposed_* loeschen)
- POST /api/sources/{id}/classification/reclassify (sofort, ~3-5s)
- POST /api/sources/classification/bulk-classify (BackgroundTask)
- scripts/migrate_sources_classification.py: CLI-Wrapper fuer Bulk-Migration
zur einmaligen Erstbestueckung aller Bestandsquellen.
Sample-Test auf Staging steht aus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>