Folgefix zu 952df87. Der Translator-Block laeuft post-summary bei jp_demo
40+ Min und war bisher fuer das Frontend unsichtbar und fuer den Watchdog
ein blinder Fleck (kein Pipeline-Step-Eintrag).
Aenderungen:
- pipeline_tracker.py: neuer Step 'translate' zwischen 'summary' und 'qc'
(DE+EN Label/Tooltip). Bewusst conditional sichtbar: erscheint nur, wenn
fremdsprachige Artikel ohne DE-Uebersetzung vorliegen UND
translator_enabled fuer die Org an ist.
- orchestrator.py: Translator-Block umrandet mit _pipe_start('translate')
und _pipe_done('translate', count_value=uebersetzt, count_secondary=
pending). Translator-Fehler schliesst Step trotzdem sauber ab.
Bedingung 'pending_translations and translator_enabled' ersetzt das
alte 'pending_translations' - skipped den Block sauber wenn Org-Override
deaktiviert (war vorher redundant in translate_articles selbst).
- main.py: ORPHAN_IDLE_LIMIT 30->60 Min, ORPHAN_HARD_LIMIT 90->120 Min.
Deckt jp_demo Translator-Phase (beobachtet bis 41 Min) mit Puffer ab,
ohne echte Haenger durchzulassen.
Resultierend: Frontend zeigt den Uebersetzungs-Schritt mit Fortschritt
(uebersetzt/gesamt). Watchdog killt nicht mehr vorzeitig.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der bisherige Watchdog markierte jeden running-Refresh nach 15 Min als
verwaist. Bei jp_demo-Lagen laeuft nach summary aber noch der Translator
(synchron, ~20 Min bei 200+ Artikeln), der den Refresh legitim ueber das
Limit traegt - er wurde dann faelschlich abgebrochen und der Orchestrator
hing in-memory weiter mit incident in _current_task.
Neuer Watchdog:
- ORPHAN_IDLE_LIMIT (30 Min): wird der Refresh nur als verwaist markiert,
wenn seit dieser Zeit kein refresh_pipeline_steps-Eintrag Fortschritt
zeigte (started_at oder completed_at)
- ORPHAN_HARD_LIMIT (90 Min): absolute Obergrenze gegen echte Haenger
- Wenn ueberhaupt keine Pipeline-Steps existieren -> als verwaist markieren
Folge: Long-Running-Refreshes (Translator-Block) laufen sauber durch,
nur echte Haenger werden bereinigt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Export-Dialog hat ein neues optionales Feld "Ersteller". Ist es
gefuellt, wird dieser Name im Bericht als Ersteller verwendet; bleibt es
leer, gilt wie bisher die E-Mail des Lage-Erstellers.
- export_incident: optionaler Query-Parameter creator, hat Vorrang vor
der E-Mail-Ableitung
- exportReport (api.js) haengt creator an die Export-URL
- submitExport (app.js) liest das neue Feld aus
- Eingabefeld im Export-Modal (dashboard.html)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die Branding-Auswahl im Export blieb wirkungslos, weil der
Browser die alten gecachten app.js/api.js weiterverwendete.
Versions-Query der beiden Skripte angehoben.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim Bericht-Export lässt sich im Modal nun zwischen "Mit
AegisSight-Branding" und "Ohne Firmen-Branding" wählen. Im
neutralen Modus entfallen Logo, AegisSight-Zeile auf dem
Deckblatt und Branding-Footer; die Datei-Metadaten werden
neutralisiert. Das Deckblatt mit Titel, Stand und Ersteller
bleibt erhalten. Betrifft PDF und DOCX.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SOURCE_TYPE_PATTERN kannte kein x_account und SOURCE_CATEGORY_PATTERN
kein x. Dadurch schlug das Speichern einer X-Quelle ueber die Monitor-
Oberflaeche mit HTTP 422 fehl: bei neuen X-Quellen am source_type, beim
Bearbeiten bestehender X-Quellen an der Kategorie x. Beide Patterns
ergaenzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die Filter-Chips wurden nur eingeblendet, wenn ein Fall Telegram- oder
X-Quellen hatte. Bei reinen Web-Faellen (z.B. in der Org jp_demo) fehlte
die Filterleiste damit komplett. Sie wird jetzt immer angezeigt, sobald
Quellen vorhanden sind, und zeigt zugleich, welche Quellentypen der Fall
enthaelt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Die Quellenuebersicht innerhalb einer Lage zeigt jetzt Filter-Chips
(Alle / Web / Telegram / X) und blendet die Quellen-Boxen nach
Quellentyp ein und aus. Die Chips erscheinen nur, wenn neben Web auch
Telegram- oder X-Quellen vorkommen.
- sources-summary-Endpoint liefert pro Quelle einen source_type,
abgeleitet aus dem source-Praefix (X: / Telegram: / sonst Web)
- Filter-Chips und data-type in renderSourceOverviewFromSummary
- App.filterSourceOverview blendet die Boxen nach Typ
- Chip-Styles in style.css
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Typ-Filter im Quellen-Modal kennt jetzt auch podcast_feed, damit
alle Quellentypen (RSS, Web, Telegram, X, Podcast) filterbar sind.
Zusaetzlich zeigt jede Quelle ein korrektes Typ-Badge -- vorher zeigten
Telegram, X und Podcast faelschlich "Web".
- podcast_feed im sources-filter-type-Dropdown
- _sourceTypeLabel-Helfer, korrekte Typ-Badges im Gruppen-Header und in
den Feed-Zeilen, x_account im Info-Tooltip-typeMap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
X-Accounts werden analog zu Telegram als Quelle (source_type=x_account)
konfiguriert und pro Lage ueber include_x zugeschaltet. Der Scraper
(feeds/x_parser.py, twscrape) liest Account-Timelines, optional ueber
einen HTTP-Proxy mit Fallback auf direkten Abruf ueber die Server-IP.
- DB-Migration include_x, Pydantic-Modelle, incidents-Router
- Orchestrator-X-Pipeline plus Haiku-Account-Vorselektion
- sources-Router /x/validate, x_account-Typ in Stats und Frontend
- Lage-Einstellungen: X-Toggle neben international und Telegram
- twscrape als Abhaengigkeit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Damit die Pipeline das aktuelle Bild einfaengt, nicht nur das relevanteste
(oft Monate alt). Bei der Test-Lage Qilin war der neueste Artikel 7 Wochen
alt, die Masse 6-7 Monate — weil Google-News-Volltextsuche nach Relevanz
rankt, nicht nach Datum.
- build_news_search_feeds: neuer Parameter recency_days. Wenn gesetzt, wird
der Google-News-Operator "when:Nd" an die Query gehaengt — der Feed liefert
nur Artikel der letzten N Tage. Eigene Domain-Gruppe '...-recent'.
- orchestrator._rss_pipeline: baut jetzt ZWEI Suchfeed-Saetze — einen
Kontext-Feed (alle Zeiten) und einen Frische-Feed (when:14d). Beide laufen
durch dieselbe Pipeline, Dedup entfernt Ueberschneidungen.
- rss_parser._fetch_feed: relevance_score bekommt einen Aktualitaets-Bonus
(<=3d +0.35, <=14d +0.20, <=60d +0.05) bzw. -Malus (>180d -0.15, >365d
-0.30). Damit ueberleben frische Artikel den Domain-Cap statt von alten
verdraengt zu werden.
Nur adhoc-Pfad betroffen — research-Lagen ueberspringen die RSS-Pipeline
ohnehin und behalten ihre volle historische Tiefe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei Fixes aus der jp_demo-Verifikation:
1. Geoparsing — Länder mit Centroid statt Hauptstadt
Bisher bekam ein Land die Koordinaten seiner Hauptstadt. Damit landeten
alle "Japan"-Marker exakt auf Tokyo (35.69, 139.69) und die Karte
suggerierte faelschlich ein Ereignis in der Hauptstadt. Neue Tabelle
_COUNTRY_CENTROIDS (37 Laender) verortet ein Land in seiner geografischen
Mitte (Japan: 36.20, 138.25). Laender ohne Centroid-Eintrag fallen auf die
Hauptstadt zurueck.
2. Recall — Eigennamen in den Google-News-Suchfeed erzwingen
Beim ersten Refresh fehlt die Headlines-Historie, daher kamen die GNews-
Such-Keywords aus der Feed-Selektion. Haiku legt Eigennamen (z.B. "Qilin")
in die en-Liste, die ja-Liste hatte nur Allgemeinbegriffe — die ja-Query
suchte ohne "Qilin". build_news_search_feeds stellt nicht-englischen
Sprach-Queries jetzt die 2 wichtigsten en-Keywords voran (Eigennamen
kommen auch in fremdsprachigen Artikeln lateinisch vor). Damit ist schon
der erste Refresh spezifisch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Google-News-Feeds (Site-Search wie auch der neue Volltext-Suchfeed) buendeln
Artikel vieler echter Publisher unter einer Feed-URL. Bisher bekamen alle
Artikel den generischen Feed-Namen als 'source' — der Faktencheck zaehlte
damit 25 Artikel verschiedener Zeitungen als EINE Quelle, und die
Quellenuebersicht war unbrauchbar.
Fix: Bei news.google.com-Feeds wird der echte Publisher aus dem <source>-Tag
des Feed-Items uebernommen (feedparser: entry.source.title). Fallback: der
Publisher-Teil hinter dem letzten ' - ' im Google-News-Titel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recall-Problem: Die Pipeline durchsuchte nur ~28 feste site:-RSS-Feeds plus
Claude-WebSearch. Japanische Security-Vendor-Blogs, Fachportale und
Regionalmedien (Cybertrust, ITmedia, INTERNET Watch, Reuters Japan ...)
tauchten in keinem festen Feed auf. Bei der Test-Lage "Qilin Ransomware
Japan" fand die Pipeline 20 Kandidaten — eine generische Google-News-JP-
Suche zum selben Thema liefert 49.
Fix: researcher.build_news_search_feeds baut pro Refresh einen Google-News-
Volltext-Suchfeed je Sprache (news.google.com/rss/search?q=keywords&hl=..&gl=..).
Query = Top-4-Keywords der jeweiligen Sprache aus der Keyword-Extraktion.
Der Orchestrator haengt diese Feeds an die selektierten site:-Feeds an; sie
laufen durch dieselbe Pipeline (Keyword-Match, Pre-Topic-Translate,
Topic-Filter). Precision bleibt, Recall steigt.
- researcher.py: build_news_search_feeds + _GNEWS_LOCALE-Tabelle.
- orchestrator._rss_pipeline: Suchfeeds aus source_language_whitelist
(jp_demo: ['ja']) bzw. output+research_language (normale Orgs) gebaut
und an selected_feeds angehaengt.
- rss_parser._apply_domain_cap: Suchfeeds (domain 'google-news-search-<lang>')
bekommen Cap 25 statt 10 — sie sind der Recall-Treiber, Topic-Filter
uebernimmt die Precision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vor der Stimmungs-Zusammenfassung laeuft ein separater Haiku-Call, der pro
Forum-Beitrag entscheidet:
- publishable: unveraendert uebernehmen
- redact: thematisch wertvoll, aber PII/Beleidigungen — Haiku liefert eine
bereinigte Kurzfassung
- discard: Hassrede gegen Gruppen, NSFW, glaubhafte Drohungen, reines
Trolling — entfernen
Damit liefert die jp_demo-Org keine ungefilterten 5ch/Hatena/Note-Posts
in die Lagen-Anzeige. Fail-open: Bei API-/Parse-Fehler wird die Original-
liste durchgereicht (Pipeline bricht nicht ab).
- analyzer.moderate_forum_articles: Batch (max 25/Call), JSON-Output, Logging
pro Entscheidungs-Klasse.
- orchestrator: Moderation laeuft vor generate_public_mood, gefilterte Liste
geht in die Stimmungs-Zusammenfassung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eigene Pipeline-Stufe nach factcheck, vor summary, die Foren-Artikel
(media_type='forum') zu einer Themen-Zusammenfassung verarbeitet. Wird als
separate Dashboard-Kachel "Öffentliche Stimmung" angezeigt — getrennt von
Lagebild und Faktencheck, damit anonyme Forenposts nicht mit belegter
Faktenlage verwechselt werden.
- DB-Migration: incidents.public_mood (TEXT) + public_mood_updated_at (TS).
- pipeline_tracker: neuer Pipeline-Step "public_mood" (DE/EN-Labels).
- analyzer.generate_public_mood: Haiku-Call der Foren-Beitraege pro Quelle
gruppiert und 3-6 thematische Bullets erzeugt, mit expliziter Quellen-
Herkunft pro Bullet. Bei zu duennem Material gibt's keinen Output.
- orchestrator: neuer Schritt zwischen Factcheck und Summary. Laedt alle
Foren-Artikel der Lage (via JOIN auf sources), uebergibt sie an den
Stimmungs-Agent, speichert den Markdown-Text in incidents.public_mood.
- Topic-Filter (analyzer.filter_relevant_articles) markiert Foren-Quellen
mit [FORUM]-Tag und bekommt im Prompt die Regel, Foren-Artikel weicher
zu bewerten (Lage-Keyword im Titel reicht). Sie sollen in der Stimmungs-
Kachel landen, nicht voreilig verworfen werden.
- IncidentResponse-Modell: public_mood/public_mood_updated_at ergaenzt.
- Frontend: neuer Tab "Öffentliche Stimmung" (nur sichtbar wenn Inhalt da),
eigene Kachel mit Warn-Hinweis "keine Faktenlage". UI.renderPublicMood
als einfacher Bullet-Renderer.
- dashboard.html Cache-Buster fuer components.js + app.js gebumpt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorbereitung fuer jp_demo-Organisation: drei separate Sprach-Settings statt
einer einzigen output_language.
org_settings.py:
- get_source_language_whitelist: Liste erlaubter Quellsprachen als JSON-Array
(z.B. ["ja"] beschraenkt RSS/Telegram auf japanische Quellen).
- get_research_language: Sprache fuer WebSearch-Prompts (Default: output_language).
- get_translator_enabled: Pro-Org-Override des globalen TRANSLATOR_ENABLED-Flags.
- LANGUAGE_DISPLAY_NAMES um ja/zh/ko/ru/ar/fa/he/fr/es erweitert.
source_rules.py:
- get_feeds_with_metadata filtert nach source_language_whitelist, wenn gesetzt.
- Feeds ohne primary_language fallen bei aktiver Whitelist raus (gewollt).
- SELECT um media_type erweitert, damit es im Feed-Dict ankommt.
orchestrator.py:
- Laedt research_language, source_language_whitelist, translator_enabled aus
den Org-Settings.
- Wenn Whitelist gesetzt: international_sources-Flag wird ignoriert.
- research_language_iso wird an researcher.search() weitergegeben.
- translate_articles bekommt enabled-Parameter aus Org-Setting.
- Geoparsing ueberspringt media_type='forum' Artikel.
- SELECT * FROM articles wird zu JOIN sources, damit media_type beim Reload
am Article-Dict haengt.
researcher.py:
- search() akzeptiert research_language_iso. Asymmetrische Sprach-Auswahl
(Recherche != Output) erzeugt eigene Prompt-Anweisung "primaer in Quell-
sprache, englische Region-Outlets erlaubt".
translator.py:
- translate_articles akzeptiert enabled-Parameter. Ueberschreibt die globale
TRANSLATOR_ENABLED-Konstante pro Aufruf.
factchecker.py:
- _format_articles_text filtert Artikel mit media_type='forum' aus. Anonyme
Foren-Posts gelten nicht als Faktenbeleg.
rss_parser.py:
- _fetch_feed traegt media_type aus feed_config ins Article-Dict ein,
damit downstream Pipeline-Schritte Foren-Quellen erkennen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim Aktualisieren von Lage 96 (Verfassungsänderung Japan) ist der Topic-Filter
in den letzten Refreshes auf 2/15, 4/26 bzw. 7/23 zurückgefallen. Die jp-RSS-
Treffer aus Asahi-Politik, NHK-Politik und Mainichi werden offenbar verworfen,
aber ohne Detail-Log lässt sich nicht beurteilen, ob das gerechtfertigt ist.
- analyzer.filter_relevant_articles: pro verworfenem Artikel eine INFO-Zeile
mit laufendem Index, Quelle, Original-Headline und (falls vorhanden) der
englischen Pre-Topic-Übersetzung. Ohne zusätzlichen Claude-Call, nur Logging
des bereits vorhandenen Materials.
- translator._TOPIC_TRANSLATE_CONTENT_MAX von 240 auf 500 erhöht. Bei dichten
Kanji- oder kyrillischen Headlines reichten 240 Zeichen oft nicht aus, um
dem nachgelagerten Topic-Filter den thematischen Kontext zu vermitteln.
Mehrkosten pro Refresh: vernachlässigbar (Haiku, einmal pro Refresh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zwei Lücken beim Befund Lage 96 (Verfassungsänderung Japan): die japanische
Asahi-Shimbun-Quelle wurde durch das Sprach-aware Keyword-Matching (#27) und
Pre-Topic-Translate (#28) erstmals durchgereicht, landete aber mit
language='en' und ohne englische Headline in der DB. Damit ist sie im
Frontend nur als Kanji-Headline zu lesen und das Summary-LLM kann den
Treffer nicht aussagekräftig referenzieren.
1. INSERT INTO articles erweitert um headline_en und content_en. Werte
stammen primär vom Translator (headline_en, falls TRANSLATOR_ENABLED den
Pfad einmal in Englisch befüllt), Fallback auf die für den Topic-Filter
angefertigte Mini-Übersetzung (headline_en_for_topic /
content_en_for_topic). So liegt die englische Variante dauerhaft in der
DB statt nur während des Refresh-Laufs im Speicher.
2. RSS- und Telegram-Parser setzen 'language' nun primär aus der Quell-/
Kanal-Konfiguration (primary_language). Vorher war es hart 'de' wenn die
Headline deutsch wirkte, sonst 'en' - mit dem Resultat, dass ein
Kanji-Titel als language='en' landete. Mit dem Fix bekommen Asahi & Co.
korrekt language='ja', russische Telegram-Kanäle 'ru' etc.
- src/agents/orchestrator.py: INSERT erweitert, Kommentar zur Fallback-Logik
- src/feeds/rss_parser.py: language aus feed_config.primary_language
- src/feeds/telegram_parser.py: channel_lang durch _fetch_channel reichen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bisher gruppierte der Domain-Cap nach der URL-Domain. Bei den 14 japanischen
Quellen, die wir über Google-News-Site-Search-RSS einspielen (MOFA, METI, MOD,
PSIA, Kyodo, Nikkei, Sankei, Tokyo-Shimbun, Chunichi, Ryukyu-Shimpo, Yahoo
Japan, NISC und der Hilfs-Bridge-Endpoint), zeigen alle Artikel-Links auf
news.google.com/articles/... — der Cap warf sie alle in einen Topf und
schnitt 10 davon weg.
Lösung: _fetch_feed gibt jetzt feed_config["domain"] (aus sources.domain,
also "mod.go.jp", "kyodo.com", ...) als source_domain mit ins Artikel-Dict.
_apply_domain_cap nutzt diese bevorzugt vor der URL-Domain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der Topic-Filter (Haiku) hat bisher fremdsprachige Headlines (CJK, Arabisch,
Hebräisch, Kyrillisch) konservativ verworfen, weil er die Sicherheitsregel
"im Zweifel NICHT relevant" auf jeden Text anwandte, den er nicht klar lesen
konnte. Bei Lage 96 (Verfassungsänderung Japan) landeten so 79 von 87
Kandidaten im Papierkorb, darunter alle ja-Quellen mit Kanji-Headlines.
Lösung: ein eigener kleiner Haiku-Batch-Call vor dem Topic-Filter übersetzt
die Headlines (+ erste 240 Zeichen Content) fremdsprachiger Artikel ins
Englische und hängt sie als article["headline_en_for_topic"] /
"content_en_for_topic" an. Der Topic-Filter zeigt sie zusätzlich zum Original
und beurteilt damit ja/zh/ko/ar/he/ru/fa-Artikel fair.
- agents/translator.py: neue Funktion translate_headlines_for_topic_filter,
unabhängig vom TRANSLATOR_ENABLED-Flag (Pflicht für korrekten Topic-Filter).
- agents/analyzer.py: filter_relevant_articles zeigt Übersetzungen mit an;
Prompt-Regel erweitert.
- agents/orchestrator.py: Aufruf direkt vor dem Topic-Filter-Schritt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bisher generierte Haiku Keywords nur in DE/EN/Romaji. Japanische RSS-Feeds
(z.B. MOD-GNews mit "防衛省・自衛隊の宇宙政策") matchten daher nie, weil
"jieitai" ≠ "自衛隊". Arabische/persische Telegram-Channels matchten nur
durch Zufall (lateinische Eigennamen in Hashtags/URLs).
Drei zusammenhängende Änderungen:
1. get_feeds_with_metadata liefert primary_language pro Feed mit.
2. FEED_SELECTION_PROMPT_TEMPLATE und KEYWORD_EXTRACTION_PROMPT verlangen
sprach-gruppierte Keywords ({de:[...], en:[...], ja:[...], ru:[...], ...}).
"en" enthält lateinische Eigennamen (universell). Andere Sprachen werden
nur gegen Feeds derselben Sprache gematcht.
3. RSS- und Telegram-Parser kombinieren pro Feed/Channel die "en"-Universalbegriffe
mit den Keywords der Quellsprache. Die Spezifik-Schwelle (1-Treffer-Match)
greift jetzt auch ab 3 Zeichen bei Non-ASCII (CJK, Arabisch, Kyrillisch).
Backward-kompatibel: flache Keyword-Listen werden weiter akzeptiert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- POST /api/sources/upload-pdf: tenant-scoped Upload, gleiche Speicher-
Konvention wie der Verwaltungs-Endpoint (<dirname(DB)>/pdfs/{sha}.pdf).
Duplikat-Check beruecksichtigt globale Quellen.
- dashboard.html: +PDF-Button in der Quellenverwaltungs-Toolbar +
eigenes Modal modal-pdf-upload (closeModal-Quotes via ').
- app.js: App.openPdfUpload + _bindPdfUploadFormOnce (Submit nur einmal
binden).
- api.js: API.upload(path, formData) Helper analog Verwaltung.
- SOURCE_TYPE_PATTERN um pdf_document erweitert
- src/services/pdf_ingest.py: pdfplumber + Tesseract-OCR-Fallback,
Uebersetzung nach DE+EN, ein Pool-Artikel pro PDF
- Scheduler-Job pdf_ingest laeuft im Minuten-Takt und verarbeitet
pdf_document-Quellen mit processed_at IS NULL
- scripts/migrate_pdf_source.py: idempotente DB-Migration
(sources.pdf_path/pdf_sha256/processed_at, articles.headline_en/content_en)
- requirements.txt: pdfplumber, pytesseract, pdf2image, Pillow
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).
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.)
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.