Commit graph

76 Commits

Autor SHA1 Nachricht Datum
Claude Code
7777b77abd feat(pipeline): Translator als Pipeline-Step + Watchdog-Limits erhoehen
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>
2026-05-26 00:22:34 +00:00
Claude Code
9c50439785 feat(x): X (Twitter) als Bezugsquelle pro Lage
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>
2026-05-22 06:52:19 +00:00
86b12a156e feat(recency): Frische-Suchfeed (when:14d) + Aktualitaets-Score
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>
2026-05-22 02:32:55 +02:00
0e4c78d50a feat(recall): dynamische Google-News-Volltext-Suchfeeds pro Lage
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>
2026-05-22 01:02:47 +02:00
16d1133442 feat(public-mood): Haiku-Moderationspass fuer Foren-Beitraege
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>
2026-05-22 00:28:30 +02:00
d65f0180d9 feat(public-mood): Stimmungs-Kachel aus Foren-Quellen
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>
2026-05-22 00:20:17 +02:00
379d14518c feat(multitenancy): Sprach-Whitelist + Translator-Override + Forum-Quellenklasse
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>
2026-05-22 00:12:56 +02:00
claude-dev
a41c8ae529 feat(articles): headline_en persistieren + Sprache aus Quell-Konfig übernehmen
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>
2026-05-21 17:05:47 +00:00
b4898614c4 feat(topic-filter): Pre-Topic-Headline-Übersetzung für fremdsprachige Quellen
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>
2026-05-21 01:43:27 +02:00
3345743aa5 feat(rss/telegram): sprach-aware Keyword-Matching für nicht-lateinische Quellen
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>
2026-05-21 00:29:49 +02:00
Claude Code
4e51834163 feat(emails): zweisprachige E-Mail-Templates + Notification-Texte org-relativ
- 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).
2026-05-13 21:08:32 +00:00
Claude Code
f68d25dbce feat(pipeline): output_language pro Org durch die Pipeline reichen
- 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).
2026-05-13 20:54:28 +00:00
Claude Code
7f220a9b65 feat(orchestrator): Faktencheck vor Lagebild mit Fallback (sequenziell)
Bislang liefen factcheck + analyze parallel via asyncio.gather. Folge:
Lagebild konnte Aussagen treffen, die der Faktencheck im selben Refresh als
contradicted markiert. Inkonsistenz zwischen Lagebild-Tab und Faktencheck-
Tab; im PDF/DOCX-Export schon kritisch.

Variante 1 aus der Diskussion: strikt sequenziell, mit Fallback bei
Faktencheck-Fail (Refresh bricht NICHT ab, Lagebild laeuft dann ohne
Faktenkontext wie bisher, ein Logeintrag dokumentiert den Fallback).

Aenderungen:
- analyzer.build_fact_context_block(): neuer Helper, baut den
  GEPRUEFTE-FAKTEN-Block aus existing_facts + neuen/aktualisierten
  Fakten. Status-Domaenen adhoc/research vereinheitlicht zu Bestaetigt /
  Umstritten / Unbestaetigt / Entwicklung. Max 20 Fakten, sortiert nach
  Status-Prioritaet desc und sources_count desc. Bei leerer Eingabe
  leerer String -> Fallback-Pfad.
- analyzer.analyze() / analyze_incremental(): neuer Optional-Parameter
  fact_context_block (default leer, Backward-Compat). 4 Prompt-Templates
  bekommen {fact_context_block}-Platzhalter sowie eine AUSSAGE-DISZIPLIN-
  Sektion: bestaetigte Fakten als Geruest, Umstrittenes explizit machen,
  Unbestaetigtes klar einordnen, kein Spekulieren ueber ungedecktes.
- orchestrator: asyncio.gather durch sequenzielle Logik ersetzt.
  Faktencheck zuerst, Pipeline-Step 6 done direkt nach dem Aufruf
  (count_value ist Schaetzung; finale DB-Zahlen stehen spaeter). Lagebild
  danach (Step 7) mit fact_context_block. _do_analysis-Closure um den
  Parameter erweitert, kein toter Inline-Block.
- spaeteres _pipe_done(factcheck) entfernt -- der Step wird jetzt frueher
  geschlossen, der spaetere Persistierungsblock laesst ihn unberuehrt.

UI-Pipeline zeigt automatisch sequenzielle Aktivitaet statt beide Steps
gleichzeitig -- keine Frontend-Aenderung noetig.

Latenz pro Refresh steigt um die factcheck-Dauer. Bewusst akzeptiert:
Konsistenz vor Geschwindigkeit.
2026-05-07 00:13:39 +00:00
Claude Code
f4c0c930b8 fix(orchestrator): aktive Pipeline-Schritte beim Cancel mitschliessen
Beim User-Cancel wurde nur refresh_log auf cancelled gesetzt, der zuletzt
aktive refresh_pipeline_steps-Eintrag blieb verwaist. Der
/api/incidents/<id>/pipeline-Endpoint liefert daraus dauerhaft
"Schritt X laeuft" an die UI, auch lange nach dem Cancel.

- pipeline_tracker.cancel_active_steps(): neuer Bulk-Helper, setzt alle
  noch active-Schritte eines refresh_log_id auf cancelled mit completed_at
- _mark_refresh_cancelled holt die refresh_log_id, macht das refresh_log-
  Update wie bisher und ruft danach cancel_active_steps auf

Reproduziert bei Lage 80 (Bjoern Hoecke), refresh_log 1273. Frontend-
CSS kennt status-cancelled nicht, faellt auf den neutralen Default-Style
zurueck (kein Spinner mehr, kein Haken, korrekt ent-hangen).
2026-05-06 23:40:39 +00:00
Claude Code
9078489d0a fix(orchestrator): Auto-Refresh nicht direkt nach Cancel/Error neu einreihen
- main.py: Auto-Refresh-Filter beruecksichtigt jetzt auch cancelled und error
- orchestrator.py: Queue-Cancels schreiben jetzt einen cancelled-Eintrag ins
  refresh_log via _log_queued_cancellation

Wirkung: Nach Cancel oder Error startet die Lage erst beim naechsten
regulaeren Slot wieder. refresh_mode bleibt unveraendert.

(Identisch zu Commit auf main, develop nachgezogen.)
2026-05-03 19:30:04 +00:00
Claude Code
a6f36be9c6 Translator-Agent: dedizierter Haiku-Pass fuer fehlende DE-Uebersetzungen
Bisher haben translations als Teil der Analyzer-JSON-Antwort gelebt
("translations": [...]). Bei vielen Artikeln pro Refresh hat das LLM die
Translations regelmaessig weggelassen (Output-Token-Druck), insbesondere
content_de (lange Texte werden zuerst gestrichen). Folge: viele englische
Artikel ohne deutsche Headline/Inhalt im Frontend.

Aenderungen:
- Neuer Agent src/agents/translator.py:
  * translate_articles_batch / translate_articles
  * Nutzt CLAUDE_MODEL_FAST (Haiku) - billig
  * Batch-Size 5 (mit Reserve gegen Output-Truncate)
  * Robustes JSON-Parsing: Markdown-Codefence, Truncate-Fallback,
    extrahiert auch unvollstaendige Antworten
  * Idempotent: Caller filtert auf fehlende headline_de/content_de
- analyzer.py: translations aus 4 Prompt-Templates entfernt (adhoc/research
  x analyze/enhance) und Fallback-Return-Dict bereinigt -> Analyzer-Output
  wird kompakter und zuverlaessiger
- orchestrator.py:
  * Alter Translation-INSERT-Block entfernt (analysis.translations wird
    nicht mehr genutzt)
  * Nach Analyse + db.commit + cancel-check neuer Translator-Call:
    SELECT WHERE language!=de AND (headline_de OR content_de fehlt),
    translate_articles, normalize_german_umlauts, COALESCE-UPDATE
  * Vor post_refresh_qc -> normalize_umlaut_articles greift auch frische
    Uebersetzungen
  * Failure-tolerant: Translator-Fehler bricht Refresh nicht ab

Backfill: migrations/migrate_translations_2026-05-03.py im Verwaltungs-Repo.
2026-05-03 00:04:59 +00:00
Claude Code
98c9da64b0 Umlaut-Normalisierung an drei Stellen + auch articles im QC
Fix fuer ASCII-Umlaute in Headlines/Inhalten (Gespraeche statt Gespraeche).
Zwei Quellen des Problems:
1. Quellen wie dpa-AFX, Telegram TASS/RIA liefern Headlines schon ASCII-fiziert
2. LLM-Uebersetzungen drift en gelegentlich zu ae/oe/ue trotz Prompt

Aenderungen:
- rss_parser.py: nach html_to_text auch normalize_german_umlauts auf
  title und summary anwenden (sicher, hunspell-Dict ignoriert englische
  Woerter wie Boeing/Business)
- orchestrator.py:1418 Translation-INSERT: headline_de und content_de
  durch normalize_german_umlauts schicken (LLM-Drift abfangen)
- post_refresh_qc.py: neue Funktion normalize_umlaut_articles als Sicher-
  heitsnetz analog zu normalize_umlaut_fields. Behandelt headline_de und
  content_de aller Artikel des Incidents; bei language=de zusaetzlich
  headline und content_original. Wird in run_post_refresh_qc nach
  normalize_umlaut_fields aufgerufen.

Backfill: migrations/migrate_umlauts_2026-05-03.py (im Verwaltungs-Repo)
2026-05-02 23:26:19 +00:00
8c4ef6b2cf CATEGORY_REPUTATION: Schlüssel an aktuelle DB-Werte angepasst
Die Reputation-Map nutzte veraltete Schlüssel (presseagenturen,
behoerden, nachrichten_de/int), die nirgends in der DB vorkamen — die
DB hat nachrichtenagentur, behoerde, oeffentlich-rechtlich,
qualitaetszeitung, think-tank, regional, telegram, boulevard. Folge
war ein stiller Bug: alle hochwertigen Quellen (Reuters, ZDF,
tagesschau, Spiegel, FAZ, BMI etc.) bekamen den Default-Score 0.4 wie
"sonstige" und wurden in der Relevanz-Sortierung nicht bevorzugt.

Map jetzt vollständig auf aktuelle Kategorie-Werte:
- nachrichtenagentur, behoerde:    1.00
- oeffentlich-rechtlich:           0.95
- qualitaetszeitung, think-tank:   0.85
- fachmedien:                      0.80
- international:                   0.75
- regional:                        0.65
- telegram:                        0.50
- sonstige:                        0.40
- boulevard:                       0.30

Test mit 200 zufälligen Artikeln aus der Live-DB:
155 besser bewertet, 0 schlechter, 45 unverändert.
Stärkster Effekt bei ÖR (+0.165), Nachrichtenagenturen (+0.18),
Qualitätszeitungen (+0.135).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:52:02 +02:00
0a6208c289 WebSearch: eingetragene Web-Quellen via Haiku vorselektieren
Bisher hatten Quellen vom Typ web_source keine praktische Wirkung auf
die Recherche - sie lagen nur als Marker in der DB. Jetzt werden sie
aktiv in den Recherche-Prompt eingebunden.

Ablauf:
1. Vor dem Hauptaufruf an Opus prüft ein günstiger Haiku-Call alle
   aktiven Web-Quellen des Tenants (plus globale) und wählt die
   thematisch passenden aus. Leere Selektion ist ausdrücklich erlaubt.
2. Die ausgewählten Domains werden dem Recherche-Prompt als
   "EINGETRAGENE WEB-QUELLEN" Block beigegeben mit der Empfehlung,
   gezielt mit "site:domain query" zu suchen, falls thematisch passend.
3. site: ist Empfehlung, kein Zwang - Claude bleibt flexibel und
   ergänzt seine sonstige Recherche.

- source_rules.get_feeds_with_metadata: SELECT um notes-Feld erweitert,
  damit der Selektor besseren Kontext zur Quelle hat.
- ResearcherAgent.select_relevant_web_sources: neuer Helper analog zu
  select_relevant_feeds, mit Skip-Optimierung wenn ≤3 Quellen.
- WEB_SOURCE_SELECTION_PROMPT: explizite Regel "lieber leer als
  pauschal alle", verhindert Token-Verschwendung.
- ResearcherAgent.search: neuer Parameter preferred_sources, beide
  Templates (RESEARCH + DEEP_RESEARCH) bekommen optionalen
  preferred_sources_block.
- Orchestrator._web_search_pipeline: Vorselektion vor researcher.search,
  Token-Usage in usage_acc, Logging der gewählten Domains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:45:17 +02:00
7b5adccf2b Analysepipeline: echte Umlaute und ASCII-Bindestriche
Em-dashes und Umlaut-Umschreibungen aus den Pipeline-Aenderungen
entfernt: Tooltip-Texte, HTML-Empty-State, JS-Kommentare,
Count-Status-Platzhalter, Orchestrator-Kommentare und CSS-Kommentare.

Anstelle von typografischen Gedankenstrichen werden jetzt Kommas oder
Punkte gesetzt, "uebersprungen" -> "uebersprungen" mit echtem Umlaut,
"laeuft" usw. analog. UI-Text "&mdash; Refresh starten" wird zu zwei
Saetzen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:16:28 +02:00
3a346ba2ec Analysepipeline: Visualisierung der Refresh-Schritte
Neuer Tab "Analysepipeline" zwischen Faktencheck und Quellenuebersicht.
Zeigt 9 Verarbeitungsschritte als n8n-artige Blockkette: Quellen sichten,
Nachrichten sammeln, Doppeltes filtern, Relevanz bewerten, Orte erkennen,
Lagebild verfassen, Fakten pruefen, Qualitaetscheck, Benachrichtigen.

- Backend: refresh_pipeline_steps-Tabelle persistiert pro Refresh+Pass die
  Status- und Zahlen-Werte. pipeline_tracker.py kapselt Start/Done/Skip/Error
  inkl. WebSocket-Broadcast (Event-Typ pipeline_step). 9 Hooks im Orchestrator
  speisen die Anzeige.
- API: GET /api/incidents/{id}/pipeline liefert Definition + letzten Stand
  (Zahlen aus letztem Refresh, Multi-Pass-Konsolidierung).
- Frontend: pipeline.js rendert Vollbild-Blockkette mit pulsierendem Glow am
  aktiven Block, animierten Pfeilen bei Datenfluss, Haekchen am fertigen Block.
  Hover-Tooltip mit Erklaerung in Nutzersprache, Klick oeffnet Detail-Popup.
  Bei Research-Lagen leuchtet ein Schleifen-Pfeil pro Mehrfach-Durchlauf auf.
  Mini-Variante (nur Icons) im Refresh-Progress-Popup.
- CSS: Light/Dark-Theme-fest, dezenter Circuit-Hintergrund (5% Opacity),
  Mobile-vertikale Stapelung unter 900px, prefers-reduced-motion respektiert.
- Uebersprungene Schritte (z.B. Geoparsing ohne neue Artikel) werden
  ausgeblendet, brandneue Lagen ohne Refresh zeigen Hinweis.

Tooltips bewusst in normaler Sprache ohne Internas (keine Modellnamen,
keine Toolnamen, keine Phasen-Labels).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:53:44 +02:00
Claude Code
88b18d0775 fix(researcher): Robusteres JSON-Parsing der Claude-Antworten
Behebt das Symptom, dass Recherche-Lagen wie staging Lage 6 "Friedrich Merz"
trotz erfolgreichem Refresh leer blieben. Claude lieferte nicht-leere Antworten
(1226-2125 Zeichen), die der bisherige Regex-Parser nicht extrahieren konnte —
die Recherche meldete "0 Artikel" und der Refresh wurde stumm als Erfolg
verbucht.

Aenderungen:
- _parse_response, select_relevant_feeds, extract_dynamic_keywords und
  select_relevant_telegram_channels nutzen jetzt json.JSONDecoder.raw_decode
  ueber Modul-Helper _extract_json_array/_extract_json_object. Damit werden
  auch JSON-Bloecke mit Vor-/Nachtext, Markdown-Fences oder verschachtelten
  Objekten zuverlaessig erkannt.
- Bei Parse-Fehlschlag wird jetzt ein gekuerztes Sample der Claude-Antwort
  geloggt, damit kuenftige Faelle direkt debuggbar sind.
- Neue ResearcherParseError-Exception unterscheidet "echt 0 Treffer" von
  "Antwort kaputt". search() gibt zusaetzlich ein parse_failed-Flag zurueck.
- Orchestrator-Multi-Pass: wenn alle 3 research-Durchlaeufe 0 neue Artikel
  ergeben UND mindestens einer am Parser scheiterte, wird der Refresh als
  Fehler markiert (statt als stiller Erfolg). Der WebSocket-refresh_error
  loest dann die sichtbare UI-Meldung aus.

Adhoc-Lagen sind unveraendert: dort fangen RSS und Telegram die kaputte
Claude-Antwort auf, dafuer ist nur die Diagnose im Log neu.
2026-04-30 20:45:41 +00:00
claude-dev
5d5ec7c924 Block B: ClaudeCliError + differenzierte HTTP-Status + Rate-Limit-Retry
- Neue Exception-Klasse ClaudeCliError(error_type, message) in claude_client.py
  mit Kategorien rate_limit / auth_error / timeout / cli_error.
- _classify_cli_error() als geteilter Klassifikator (Keywords fuer Rate-Limit
  und Auth-Fehler wie "does not have access", "login again").
- call_claude() erkennt jetzt auch is_error=true im JSON bei returncode=0
  (Hauptursache des Ausfalls vom 22.04.: CLI liefert "Your organization
  does not have access" mit is_error=true statt Exit-Code).
- Orchestrator: ClaudeCliError mit rate_limit/timeout als transient behandelt
  (3 Retries mit Backoff 0s/120s/300s). auth_error/cli_error brechen sofort
  ab ohne Retry. Behebt den bestehenden Bug, dass Rate-Limit-Fehler gar nicht
  retried wurden.
- routers/incidents.py Enhance-Endpoint: ClaudeCliError wird auf
  503 (auth_error) / 429 (rate_limit) gemappt, TimeoutError auf 504.
- routers/chat.py _call_claude_chat(): wirft jetzt ClaudeCliError statt
  generischem RuntimeError. Chat-Endpoint mappt auth_error auf 503.
- Frontend: neue ApiError-Klasse in api.js mit status+detail.
  generateDescription() in app.js zeigt differenzierte Toasts nach
  HTTP-Status (503/429/504/403).
- dashboard.html: Cache-Bust api.js + app.js auf v=20260423a
2026-04-23 17:54:13 +00:00
claude-dev
e8ac0d0c50 Block A: License-Check + Credits-Tracking fuer Enhance und Chat
- Neuer Helper charge_usage_to_tenant() in services/license_service.py:
  UPSERT in token_usage_monthly und Credits-Abzug aus licenses.credits_used.
  Wiederverwendbar fuer alle Claude-Call-Verursacher.
- Orchestrator: Inline-Buchungslogik (35 Zeilen) durch Helper-Aufruf ersetzt.
- routers/incidents.py POST /enhance-description: require_writable_license
  statt get_current_user, db_dependency hinzugefuegt, Credits-Buchung mit
  source="enhance" nach jedem Claude-Call.
- routers/chat.py POST /: analog require_writable_license + Credits-Buchung
  mit source="chat". _call_claude_chat() gibt jetzt zusaetzlich ClaudeUsage
  zurueck.

Abgelaufene/gesperrte Lizenzen koennen damit keine Haiku-Calls mehr ausloesen,
und alle Kosten werden konsistent auf Tenant-Ebene verbucht.
2026-04-23 17:49:32 +00:00
claude-dev
a579e2c275 Neueste Entwicklungen aus Lagebild statt aus Artikel-Strom
Bisher extrahierte der Generator Bullets direkt aus den neu eingesammelten
Artikeln und mergte sie mit den bestehenden Developments. Das fuehrte zu
zwei wiederkehrenden Problemen:

1. Off-topic Artikel, die den Keyword-Prefilter aber nicht den Topic-Filter
   passiert hatten, konnten als Bullet landen (die Kachel bildete dann
   Nebenschauplaetze des Weltgeschehens ab statt der Lage).
2. Alte Bullets blieben stehen, auch wenn sie laengst nicht mehr die
   'neuesten' Entwicklungen waren — nur sehr ueberholte Eintraege fielen
   durch das 8-Bullet-Cap raus.

Neue Logik: Der Generator nimmt das frisch erzeugte Lagebild als autoritative
inhaltliche Grundlage und waehlt daraus Bullets aus, die durch eine aktuelle
belegende Meldung (<~7 Tage) gestuetzt sind. Dadurch:

- Thematisch sauber: Lagebild enthaelt bereits nur relevante Inhalte.
- Echt 'neueste': Alte Hintergrund-Erwaehnungen im Lagebild fallen raus,
  weil kein aktueller Artikel sie belegt.
- Klar datiert: Zeitstempel zwingend aus article.published_at der
  belegenden Meldung.
- Kompakt: 4-6 Bullets (vorher 8), nach Zeitstempel absteigend.

Kein Merge mit previous_developments mehr — bei jedem Refresh neu generiert
(behebt das Drift-Problem). previous_developments bleibt nur als Fallback,
falls der Generator im Einzelfall 0 Bullets parst.
2026-04-21 14:23:18 +00:00
claude-dev
05b60ffb35 Fix: Timer springt beim Seiten-Reload nicht mehr zurueck
Bei Research-Multi-Pass (3 Durchlaeufe) und bei Retry-Versuchen wird
pro Pass/Retry ein neuer refresh_log-Eintrag mit frischem started_at
angelegt. /incidents/refreshing gab dadurch beim Reload den spaeteren
started_at zurueck statt des urspruenglichen Session-Starts — der
Frontend-Timer sprang auf 0:00 zurueck.

Orchestrator traegt jetzt _current_task_started_at in-memory, gesetzt
beim Queue-Pickup und geraeumt im finally. /incidents/refreshing liefert
diesen Session-Start fuer den aktuell laufenden Task (Fallback: letzter
refresh_log-Eintrag, falls der Server zwischenzeitlich neu gestartet
wurde).
2026-04-21 13:42:51 +00:00
claude-dev
60b8646fe4 Semantischer Topic-Filter gegen off-topic Keyword-Zufallstreffer
Neue Artikel passieren jetzt vor DB-Speicherung einen Haiku-Relevanzfilter
(AnalyzerAgent.filter_relevant_articles), der Artikel verwirft, die nur
auf generische Keywords matchen, aber das Kernthema der Lage nicht
inhaltlich behandeln. Bei Parsing-/API-Fehler oder 100%-Rejection: Fallback
auf unveraenderte Kandidatenliste.

Orchestrator trennt DB-Dedup und INSERT, damit der Filter nur auf neue
Kandidaten laeuft (Kostenoptimierung). LATEST_DEVELOPMENTS-Prompt erhaelt
zusaetzliche Relevanz-Gate-Regel als zweite Sicherung.

Hintergrund: Incident 'Russische Militaerblogger' sammelte bisher Iran-,
Nahost- und allgemeine Ukraine-Artikel ein, weil Keyword-Match ab 2 von 8
Begriffen ('iran', 'russland', 'drohne', ...) genuegt. Der semantische
Filter verwirft solche Zufallstreffer.
2026-04-21 12:01:56 +00:00
claude-dev
5127e0a42d Podcast-Integration Phase 1: Feed-Tag + Senderseiten
Podcasts werden wie normale RSS-Quellen behandelt (source_type=podcast_feed).
Kein externer bezahlter Dienst, keine lokale Transkription — Monitor nutzt
ausschliesslich vorhandene Transkripte.

Kaskade fuer Transkript-Bezug:
 1. Podcasting-2.0-Tag <podcast:transcript> im Feed (SRT/VTT/HTML/JSON)
 2. Redaktionelles Manuskript auf der Episodenseite
    (Adapter: Dlf, SZ, Spiegel, NDR)
 3. YouTube-Captions — Phase 2, optional per yt-dlp

Kein Stufen-Treffer -> Episode verworfen (graceful, kein Error).

Neu:
- src/feeds/podcast_parser.py (eigener Parser, RSS-Heisspfad unveraendert)
- src/feeds/transcript_extractors/ (Plugin-Muster):
    __init__.py        Dispatcher, Cache-Lookup gegen podcast_transcripts
    _common.py         HTML-Extraktion, Domain-Matching, httpx-Helper
    rss_native.py      Stufe 1: Feed-Tag-Parser (SRT/VTT/JSON/HTML)
    website_dlf.py     Stufe 2: deutschlandfunk.de + Schwester-Domains
    website_sz.py      Stufe 2: sz.de / sueddeutsche.de
    website_spiegel.py Stufe 2: spiegel.de / manager-magazin.de
    website_ndr.py     Stufe 2: ndr.de

Geaendert:
- src/database.py: idempotente Migration, Tabelle podcast_transcripts als
  URL-Cache gegen Mehrfach-Scrape zwischen Lagen
- src/models.py: Pydantic-Pattern von source_type um podcast_feed erweitert
- src/source_rules.py: get_feeds_with_metadata() nimmt source_type-Parameter,
  Default rss_feed (RSS-Pfad unveraendert)
- src/agents/orchestrator.py: neue _podcast_pipeline() parallel zu RSS,
  WebSearch und Telegram; nur fuer adhoc-Lagen; ohne Podcast-Quellen dormant

Verifikation:
- Migration auf Live-DB erfolgreich (Log: Tabelle podcast_transcripts angelegt)
- Import-/Instanziierungs-Test aller Module bestanden
- can_handle-Tests pro Sender-Adapter positiv + negativ OK
- Live-Scrape gegen Dlf: 22710 Zeichen, gegen SZ: 24918 Zeichen
- Dormant-Test: 0 Podcast-Quellen -> keine neue Codezeile im Refresh

Verwerfbarkeit: rein additiv, RSS-Pfad unberuehrt, Rollback in drei
Schritten (Quellen disablen, git revert, DROP TABLE podcast_transcripts).
2026-04-18 12:06:54 +00:00
claude-dev
d6c541cb95 Neueste Entwicklungen: Kachel fuer adhoc-Lagen
- DB-Migration: Spalte latest_developments (TEXT) in incidents
- Analyzer: neuer Prompt LATEST_DEVELOPMENTS_PROMPT_TEMPLATE und
  Methode generate_latest_developments() liefert chronologische
  Bullet-Liste (max. 8, neueste oben, Zeitstempel DD.MM. HH:MM)
- Orchestrator: nach Analyse+Faktencheck ein Extra-Schritt nur fuer
  incident_type=adhoc, der die neue Kachel fortschreibt
- Analyzer-Prompts (Erst- und inkrementell): erzeugen KEINE
  Zusammenfassung-Sektion mehr im Lagebild (vermeidet Duplikat mit
  der neuen Kachel)
- models.IncidentResponse um latest_developments erweitert
- Frontend: Rendering der Kachel in app.js
2026-04-18 11:47:10 +00:00
claude-dev
f4f1df916e Sofortiger Cancel: Laufende Claude-Prozesse per Event abbrechen
Bisher war Cancel kooperativ (Flag-basiert) -- der Code pruefte das Flag
nur an wenigen Checkpoints. Laufende Claude CLI Subprozesse (WebSearch,
Analyse, Faktencheck) liefen bis zum Ende weiter, was minutenlanges
Warten beim Abbrechen verursachte.

Neuer Ansatz:
- ContextVar _cancel_event_var in claude_client.py
- Orchestrator setzt asyncio.Event vor jedem Refresh
- call_claude wartet parallel auf Prozess UND cancel_event
- Bei Cancel: process.kill() + CancelledError sofort
- Kein Durchreichen durch Agent-Methoden noetig (contextvars)
2026-04-11 19:29:01 +00:00
claude-dev
6cddb05b83 fix: Quellen-Suffix-Refs ([22b]) auf Basisquelle auflösen statt Platzhalter
Claude vergibt manchmal Buchstaben-Suffixe an Quellennummern (z.B. [22b] statt [22]). Bisher wurden dafür leere Platzhalter-Quellen erstellt. Jetzt wird geprüft ob die Basisnummer existiert und die Referenz im Text korrigiert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:51:33 +00:00
claude-dev
e2e6a1ed7e Perf: Executive Summary nach Refresh im Hintergrund vorberechnen
Statt beim PDF-Export 30+ Sekunden auf die KI-Zusammenfassung zu
warten, wird sie jetzt automatisch nach jedem Refresh generiert.
Beim Export ist sie dann sofort verfuegbar (gecacht in DB).
Summary-Aenderungen invalidieren den Cache automatisch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:31:20 +00:00
claude-dev
521d6ac357 Fix: Artikel inline nachladen wenn all_articles_preloaded fehlt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:12:06 +00:00
Claude Dev
55c0307e68 Fix: Faktencheck bei fehlenden existing_facts trotz vorhandener Summary
Wenn ein vorheriger Refresh die Summary gespeichert hat aber die
Faktenchecks durch einen Crash verloren gingen, wurden bei allen
Folge-Refreshes keine Artikel an den Factchecker uebergeben
(all_articles_preloaded blieb None), was zu leeren Ergebnissen fuehrte.

Betroffen: Incidents 56, 57, 58 (alle mit 0 Faktenchecks trotz Artikeln).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:15:58 +02:00
Claude Dev
3bf4f3debb Fix: Redundanten lokalen Import von find_matching_claim entfernt
Der lokale Import in der Massen-Downgrade-Pruefung ueberschattete den
Top-Level-Import und verursachte: cannot access local variable
find_matching_claim where it is not associated with a value.

Dadurch scheiterte der Faktencheck komplett (0 Faktenchecks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:31:31 +02:00
Claude Dev
fb0c47eee4 Fix: Abbrechen-Dialog, Overlay-Stacking, Queue-Cancel
1. Confirm-Dialog z-index ueber Progress-Popup (10000 > 9000)
2. Progress-Popup wird ausgeblendet waehrend Confirm-Dialog offen
3. Kein dunkler-werdendes Overlay bei mehrfachem Abbrechen-Klick
4. Abbrechen funktioniert jetzt auch fuer Lagen in der Warteschlange
   (werden direkt aus der Queue entfernt statt auf Start zu warten)
5. Cancel-Status wird im Popup-Titel angezeigt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:50:30 +02:00
Claude Dev
3f88d00b8c Fortschrittsanzeige: Popup mit Checkboxen, Blur, Pro-Lage-Timer
Ladebalken ersetzt durch zentriertes Popup-Fenster mit Checkbox-Checkliste
(Warteschlange, Recherche, Analyse, Faktencheck) und Echtzeit-Timer.

Erster Durchlauf: Popup nicht wegklickbar, Blur-Effekt auf Kacheln.
Aktualisierung: Popup minimierbar zu kompakter Status-Leiste.
Timer laeuft pro Lage im Hintergrund weiter bei Lagenwechsel.
Gesamtzeit wird am Ende im Abschluss-Popup angezeigt.

Sidebar: Animierter Gold-Rand und Fortschrittstext (Recherchiert/
Analysiert/Faktencheck) unter dem Lage-Namen bei laufendem Refresh.

Zusaetzlicher Cancel-Checkpoint im Orchestrator nach Uebersetzung.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:59 +02:00
Claude Dev
3356ba1ae5 E-Mail-Benachrichtigungen: Lagebild vs. Recherche unterscheiden
Template passt Text je nach incident_type an:
- adhoc: "Neues Lagebild - Benachrichtigung" / "Neuigkeiten zur Lage"
- research: "Recherche - Benachrichtigung" / "Neuigkeiten zur Recherche"

incident_type wird durch die gesamte Notification-Kette durchgereicht.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:49:59 +02:00
Claude Dev
ac3fe5f22b Fix: Massen-Downgrade von Faktenchecks verhindern
Zwei Bugs behoben die dazu fuehrten, dass alle established Faktenchecks
bei einem inkrementellen Refresh auf unverified zurueckgesetzt wurden:

1. _format_existing_facts() uebergibt jetzt Evidence-Kontext an den LLM,
   damit bestehende Claims im inkrementellen Modus verifiziert bleiben.
2. Neuer Schutz im Orchestrator: Wenn >50% der established Fakten
   herabgestuft wuerden, werden die FC-Ergebnisse komplett verworfen.

Root Cause: Inkrementeller Faktencheck hatte nur Claims+Status aber
keine Evidence. Der LLM konnte bestehende Fakten nicht verifizieren
und gab unverified fuer alles zurueck.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:46:16 +02:00
Claude Dev
bd5952b9ae fix: Multi-Pass nur beim ersten Refresh einer Research-Lage
3 automatische Durchläufe laufen nur wenn noch kein Briefing existiert
(erster Refresh). Folge-Refreshes machen wie bisher einen einzelnen
Durchlauf, um unnötige Token-Kosten zu vermeiden.
2026-03-27 18:48:43 +01:00
Claude Dev
506965e3e2 feat: Research-Modus führt automatisch 3 Durchläufe durch
Research-Lagen (Tiefenrecherche) führen jetzt bei einem Klick auf
Aktualisieren automatisch 3 aufeinanderfolgende Refresh-Zyklen durch:
1. Breite Erfassung (initiale 4-Phasen-Recherche)
2. Vertiefung (andere Quellen, inkrementelle Analyse)
3. Konsolidierung (letzte Lücken, Fakten-Upgrade auf established)

Die Progress-Bar zeigt den aktuellen Durchlauf an (Durchlauf 1/3 etc.).
Cancel funktioniert zwischen und innerhalb der Durchläufe.
Adhoc-Lagen (Live-Monitoring) sind nicht betroffen.
2026-03-27 18:38:06 +01:00
Claude Dev
912257ceef refactor: Token-Tracking mit source=monitor fuer Split-Anzeige
token_usage_monthly hat jetzt UNIQUE(org_id, year_month, source).
Monitor schreibt mit source=monitor, Globe mit source=globe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:44:12 +01:00
Claude Dev
7bfa1d29cf feat: Credits-System mit Verbrauchsanzeige im User-Dropdown
- DB-Migration: credits_total/credits_used/cost_per_credit auf licenses, token_usage_monthly Tabelle
- Orchestrator: Monatliche Token-Aggregation + Credits-Abzug nach Refresh
- Auth: Credits-Daten im /me Endpoint + Bugfix fehlende Klammer in get()
- Frontend: Credits-Balken im User-Dropdown mit Farbwechsel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:53:19 +01:00
Claude Dev
474e2beca9 fix: URL-Verifizierung fuer WebSearch-Ergebnisse
- Prompt-Verbesserung: Claude muss exakte URLs aus WebSearch kopieren, keine konstruierten URLs
- Neue _verify_article_urls() Funktion im Orchestrator
- HEAD-Request auf jede WebSearch-URL, GET-Fallback bei 405
- Bei 404/unerreichbar: Ersetzung durch Google-Suchlink (site:domain headline)
- Nur WebSearch-URLs werden geprueft, RSS-URLs sind bereits verifiziert
2026-03-17 10:22:01 +01:00
Claude Dev
bbd4821011 fix: Quellenlinks mit Buchstaben-Suffix ([389a] etc.) korrekt verlinken
Probleme:
- Frontend-Regex matchte nur reine Zahlen, nicht [389a]-Style Refs
- 17 alphanumerische Quellen im Irankonflikt blieben unverlinkt
- Orchestrator-Validierung erkannte diese Refs nicht als fehlend

Fixes:
- Frontend: Regex erweitert auf [\d+a-z?], Vergleich mit String und Number
- Orchestrator: Validierung erkennt jetzt auch alphanumerische Refs
- Analyzer-Prompts: Explizite Anweisung, nur ganze Zahlen als Nr zu verwenden
- 822a und 859a in Irankonflikt sources_json nachgetragen
- Cache-Buster aktualisiert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:34:55 +01:00
Claude Dev
599102740a fix: updated_at wird jetzt immer nach Refresh aktualisiert
Bisher wurde updated_at nur gesetzt wenn die Claude-Analyse erfolgreich
war. Bei fehlgeschlagenem JSON-Parsing blieb der alte Timestamp stehen,
obwohl neue Artikel gesammelt und Faktenchecks durchgefuehrt wurden.
Jetzt wird updated_at am Ende jedes Refreshs gesetzt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:29:59 +01:00
Claude Dev
b38ae9e1b1 fix: Quellenlinks bei String-Nr repariert (574, 610, 611, 617 etc.)
Ursache: Claude liefert teilweise Quellennummern als String statt Integer.
Der Frontend-Vergleich (===) schlug dann fehl: "574" !== 574.

Fixes:
- 95 String-Nummern in Irankonflikt sources_json zu Integer konvertiert
- 5 Duplikate entfernt
- Frontend: Number() statt parseInt/=== fuer robusten Vergleich
- Orchestrator: Automatische Konvertierung von String-Nr zu Integer vor DB-Speicherung
- Cache-Buster aktualisiert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:00:00 +01:00
Claude Dev
aad473a568 feat: Quelleneinordnung (Bias) in Lageberichten fuer kritische Quellen
Parteiische Quellen (pro-russisch, pro-iranisch, rechtsextrem etc.)
werden jetzt im Lagebericht-Fliesstext als solche gekennzeichnet,
damit der Leser die Informationen einordnen kann.

Aenderungen:
- Orchestrator reichert Artikel mit source_bias aus der sources-Tabelle an
- Analyzer zeigt Einordnung im Artikel-Kontext fuer den Claude-Prompt
- Alle 4 Prompt-Templates enthalten neue Regel zur Quellenkennzeichnung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:53:36 +01:00
Claude Dev
bf21bc4e2c fix: Fehlende Quellenverweise in Lageberichten repariert und Validierung ergaenzt
17 fehlende Quellen (815-831) im Irankonflikt und 11 fehlende (77-87)
im Ukraine-Konflikt in sources_json nachgetragen. Ursache: Claude
referenziert Quellen im Summary-Text, liefert sie aber nicht immer
im sources-Array mit. Neue Validierung im Orchestrator erkennt
fehlende Quellennummern nach dem Merge und fuegt Platzhalter ein.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 10:46:25 +01:00
Claude Dev
6a2bd9e9c9 feat: Adhoc-Recherche bekommt bestehende Artikel als Kontext
- RESEARCH_PROMPT_TEMPLATE: {existing_context} Platzhalter eingefügt
- search(): Baut bei Adhoc-Folge-Refreshes Kontextblock mit bis zu 30 bekannten Headlines auf
- orchestrator: Übergibt bestehende Artikel jetzt für ALLE Incident-Typen, nicht nur Research

Effekt: Bei Adhoc-Auto-Refreshes findet Claude WebSearch gezielt neue Quellen statt immer dieselben Mainstream-Treffer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:07:13 +01:00