Commit graph

139 Commits

Autor SHA1 Nachricht Datum
claude-dev
bf967216de fix(analyse): vollstaendigen Bericht nehmen, wenn das Modell sich selbst korrigiert
Der Recherchebericht der EU-Fassung enthielt seit zwei Laeufen nur noch den
Abschnitt ZUSAMMENFASSUNG, 1513 statt der ueblichen mehreren tausend Zeichen.
Die Abschnitte HINTERGRUND, AKTEURE, AKTUELLE LAGE, EINSCHAETZUNG und
QUELLENQUALITAET fehlten vollstaendig.

Ursache. Das Modell lieferte zuerst ein knappes JSON, bemerkte den Fehler
selbst ("Wait, I need to include the full briefing content in the summary
field. Let me redo this properly with all sections") und haengte danach eine
zweite, vollstaendige Fassung an. Unsere Auswertung nahm das erste Objekt und
verwarf alles danach. Der Bericht war also da, wir haben ihn weggeworfen.

Behoben. json_utils bekommt extract_json_objects und extract_json_arrays, die
alle vollstaendigen Bloecke einer Antwort liefern statt nur den ersten. Der
Analyzer waehlt daraus den Bericht mit dem laengsten summary, der Faktencheck
die Liste mit den meisten Fakten. Beide protokollieren, wenn mehrere Fassungen
auftauchen, damit der Fall sichtbar bleibt. Die bisherigen Einzelabfragen
extract_json_object und extract_json_array bleiben unveraendert, damit sich an
den uebrigen Aufrufstellen nichts aendert.

Gegenprobe an der echten Antwort aus Lauf 50. Vorher 1513 Zeichen mit einem
Abschnitt, jetzt 8189 Zeichen mit allen sechs.

Neu sind 16 Pruefungen, insgesamt laufen 96 ohne Netzzugriff und ohne Kosten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:49:15 +00:00
claude-dev
0f39154e39 fix(eu): drei Fehler beheben, die den Ertrag des Faktenchecks aufzehrten
Der Vergleichslauf zur Innenministerkonferenz (Lage 49) zeigte, dass der
verbesserte Faktencheck zwar zehn Fakten lieferte, davon aber nur drei
uebrig blieben. Die Ursachen lagen in drei nachgelagerten Stellen.

1. Quellenzuordnung stufte alle zehn Fakten herab.
   Durch den groesseren Belegblock stuetzt das Modell seine Fakten jetzt
   staerker auf die europaeische Suche als auf die gespeicherten Meldungen.
   Es zitiert dabei die Belegnummern statt vollstaendiger Adressen. Die
   Quellenzuordnung suchte daraufhin vergeblich nach passenden Meldungen und
   stufte jeden Fakt herab. Im Vergleich: Lauf 45 hatte null Herabstufungen,
   Lauf 44 eine, Lauf 49 zehn von zehn.
   Behoben an zwei Stellen. Die Belegsuche gibt die gefundenen Quellen jetzt
   einzeln zurueck, der Faktencheck reicht sie an die Zuordnung durch, und
   der Auftrag verlangt ausdruecklich die vollstaendige Adresse je Beleg.

2. Duplikatpruefung loeschte sieben von zehn Fakten.
   Das Clustering warf inhaltlich verschiedene Beschluesse in eine Gruppe,
   weil viele Behauptungen gleich beginnen. Es gab keinerlei Absicherung, bei
   kleinen Faktenmengen entschied das Modell sogar voellig allein.
   Zwei Sicherungen greifen jetzt. Eine Gruppe, die mehr als 40 Prozent aller
   Fakten umfasst, wird verworfen. Und jeder Loeschkandidat muss dem
   behaltenen Fakt auch rechnerisch aehnlich sein, Grenze 0,55 im selben Mass
   wie im Vorfilter. Beide Werte sind ueber Umgebungsvariablen einstellbar.

3. Leerrunden in der Recherche.
   War die Treffer-Obergrenze erreicht, brach nur die innere Schleife ab. Die
   Rundenschleife lief bis zum Ende weiter und befragte jedes Mal das teuerste
   Modell, ohne noch eine einzige Suche auszufuehren. Das kostete rund 0,17
   USD je Durchgang, bei drei Durchgaengen etwa 0,50 USD pro Aktualisierung.

Der Weg ueber die Anthropic-CLI bleibt unveraendert. Neu sind 16 Pruefungen,
darunter der nachgebildete Fall aus Lage 49, insgesamt laufen jetzt 80
Pruefungen ohne Netzzugriff und ohne Kosten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:26:32 +00:00
claude-dev
c4e19777f9 feat(eu): Faktencheck der EU-Fassung sucht breiter, gezielter und hakt nach
Der Faktencheck ueber Bedrock und staan lieferte spuerbar weniger belegte
Fakten als der bisherige Weg ueber die Anthropic-CLI. Ursache war die Bauart.
Der bisherige Weg sucht waehrend der Pruefung selbst und kann bei duenner
Beleglage nachfassen, die EU-Fassung bekam dagegen einen festen, vorab
beschafften Stapel Belege und hatte genau einen Versuch.

Drei Aenderungen schliessen die Luecke.

1. Mehr Belege je Pruefung. Statt 3 Anfragen mit je 3 Treffern laufen jetzt
   7 Anfragen mit je 6 Treffern, und bis zu 14 statt 8 Pruefpunkte gehen in
   die Planung ein. Gleiche Quellen werden nur einmal in den Kontext gelegt.

2. Punktbezogene Planung. Das planende Modell ordnet jeder Suchanfrage zu,
   welche Behauptung sie belegen soll, statt allgemein zum Thema zu suchen.

3. Echte Nachhak-Runde. Bleiben nach der Pruefung mindestens zwei
   Behauptungen unbelegt, wird fuer genau diese noch einmal gesucht, mit
   anderen Anfragen als zuvor, und nur diese Punkte werden neu bewertet.
   Ein Status wird dabei nur angehoben, nie gesenkt, und faellt die Runde
   aus, bleiben die Fakten unveraendert.

Die Nachhak-Runde greift in allen drei Pruefwegen, also bei der Erstpruefung,
der inkrementellen Pruefung und der Pruefung in Themengruppen. Gruppen nutzen
kleinere Suchmengen, da sie parallel laufen und je nur ein Teilthema abdecken.
Alle Mengen sind ueber Umgebungsvariablen einstellbar.

Der Weg ueber die Anthropic-CLI bleibt unveraendert, das sichern sechs
Regressionstests ab. Neu sind zwei Testdateien mit zusammen 55 Pruefungen,
die ohne Netzzugriff und ohne Kosten laufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 12:59:26 +00:00
claude-dev
768150c302 fix(agents): nicht maskierte Anfuehrungszeichen in Modell-JSON reparieren
Beobachtet am 31.07.2026 auf Staging: Modelle schliessen deutsche Zitate
gelegentlich mit einem geraden Anfuehrungszeichen, das das JSON an dieser
Stelle bricht. Die Folgen waren still und teuer:
  - Lagebild von Lage 47 brach mitten im Satz ab (738 statt ~4000 Zeichen),
    weil der Regex-Fallback nur bis zum Stoerzeichen rettete
  - ein kompletter Recherche-Durchlauf von Lage 45 ging verloren
  - sechs weitere Durchlaeufe fielen auf die verlustbehaftete
    Einzelobjekt-Rettung zurueck
Betroffen war vor allem der EU-Weg (Opus 4.6), der CLI-Weg aber ebenfalls.

Neu: src/json_utils.py repariert solche Antworten, indem es beim Durchlauf
mitfuehrt, ob eine Zeichenkette offen ist, und ein Anfuehrungszeichen maskiert,
auf das kein gueltiger JSON-Fortsetzer folgt (Komma nur dann als Ende, wenn
danach wirklich ein Wert oder Schluessel beginnt). Roh eingebettete
Zeilenumbrueche werden ebenfalls maskiert. Repariert wird ausschliesslich als
Rueckfallebene, nachdem der normale Parser gescheitert ist, gueltiges JSON
bleibt unangetastet.

Eingebunden in analyzer, factchecker und researcher. Zusaetzlich meldet der
Analyzer jetzt als ERROR, wenn der letzte Fallback ein erkennbar
abgeschnittenes Lagebild liefert, statt das still hinzunehmen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 11:32:35 +00:00
claude-dev
b32c54b22c feat(eu-umbau): Faktencheck und Lagebild auf die EU-Suche umgestellt (Phase 3)
Im EU-Modus laufen jetzt auch Faktencheck und Lagebild komplett ueber Bedrock,
damit gibt es dort keine CLI-Aufrufe mehr. Der Faktenchecker bekommt vorab
eine gezielte staan-Stuetzsuche (eu_call_with_search: Haiku plant
Verifikations-Queries zu offenen Behauptungen, Treffer wandern als
Kontextblock in den Prompt und werden als Zweitquellen zitiert). Das Lagebild
arbeitet im EU-Modus rein auf dem Meldungsbestand, die EU-Recherche hat
unmittelbar davor gesammelt. Alle fuenf Aufrufstellen (Erst-/inkrementeller
Faktencheck, Gruppen-Verifikation, Erst-/inkrementelle Analyse) haben die
Weiche, der CLI-Modus bleibt unveraendert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 21:08:34 +00:00
claude-dev
676741b1c4 fix(eu-umbau): SQLite-Parallelitaet der Doppelspur abgesichert
busy_timeout von 5s auf 30s (zwei gleichzeitig schreibende Lanes) und
'database is locked' im Orchestrator als transienter Fehler mit Retry
statt als permanenter Abbruch. Gefunden im Doppelspur-Paralleltest
(Lage-Summary-Update kollidierte mit der Schreiblast der zweiten Lane).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:39:41 +00:00
claude-dev
a7f1aa29ab feat(eu-umbau): staan-Recherche-Schleife und Doppelspur (Phase 2)
Im EU-Modus laeuft die Recherche jetzt europaeisch: agents/eu_researcher.py
steuert eine Schleife aus Bedrock-Planung (Opus, EU-Profile) und staan-Suchen
(services/staan_client.py, Suche + Volltexte via full_content=markdown,
Pflicht-Domain-Ausschlussliste je Anfrage). Bildet die 4-Phasen-Tiefenrecherche
nach und liefert exakt das JSON der bisherigen CLI-WebSearch, Parsing und
Filter in researcher.search bleiben unveraendert. Anti-Halluzination: nur URLs
aus echten staan-Treffern werden akzeptiert; published_at nur wenn aus Inhalt
oder URL ableitbar (staan liefert keine Daten).

Doppelspur im Orchestrator: Lane-Schluessel ist jetzt (Organisation, Backend),
Aufloesung zentral in _resolve_ai_backend (Lage > Org-Setting > ENV). CLI- und
EU-Lauf derselben Organisation laufen gleichzeitig, gleiche Backends seriell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:12:31 +00:00
claude-dev
fc33cccd44 feat(eu-umbau): Bedrock als zweiter KI-Modellweg (Phase 1)
Zweiter Modellweg ueber AWS Bedrock (EU-Inferenzprofile Frankfurt), umschaltbar
je Lage (incidents.ai_backend) > Organisation (org_setting ai_backend) > global
(ENV AI_BACKEND, Default cli). Neue Datei agents/bedrock_client.py (Converse
API, nur eu.-Profile, Token-zu-USD-Umrechnung ueber BEDROCK_PRICING, gleiche
Fehlerkategorien wie das CLI). Routing zentral in call_claude; Aufrufe mit
WebSearch/WebFetch laufen bis zur staan-Schleife (Phase 2) weiter uebers CLI.
Migration: Spalte incidents.ai_backend. requirements: boto3.
Hinweis Promote: boto3 muss im Live-venv installiert werden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 19:48:52 +00:00
claude-dev
1bbbd891ef feat(abrechnung): Monatskontingent in Credits mit festen Sätzen, Hard-Stop und Warnschwelle
Portierung aus dem Lokal-Fork (AegisSight-Monitor-Local, Commit d2b9168,
per Cherry-Pick übernommen und an den Server-Stand angepasst).

- Feste Sätze je Aktion (BILLING_MODE=flat) statt echter Kosten geteilt
  durch cost_per_credit. Sätze auf die am 23.07.2026 beschlossenen
  Verkaufswerte gesetzt. Live-Lauf 45, Recherche je Durchlauf 40,
  Studio-Bausteine 12, Chat/Beschreibung/Globe 1. Per ENV überschreibbar,
  der alte Modus bleibt als BILLING_MODE=actual erhalten.
- Abrechnungsperiode. licenses.credits_period trennt monthly von total,
  träger Monatsreset bei der nächsten Lizenzprüfung, optionaler Übertrag
  (credits_rollover, gedeckelt auf ein Monatskontingent). Bestandslizenzen
  ohne Periodenmarke behalten ihren Verbrauch.
- Hard-Stop gegen das verfügbare Monatskontingent (Kontingent plus
  Übertrag) statt gegen das Lebenszeit-Total.
- Warnschwelle budget_warning_percent (Default 80 Prozent) wird erstmals
  ausgewertet, einmalige Meldung an alle aktiven Nutzer der Organisation.
- DB-Migration additiv und idempotent (credits_period, credits_period_start,
  credits_rollover, credits_carried, budget_warning_sent, unlimited_budget).
- /api/auth/me liefert credits_available als Bezugsgröße plus
  credits_period, das Credits-Widget zeigt "Credits diesen Monat".
- Wortlaut überall Credits (nicht Guthaben/Einheiten), docs/ABRECHNUNG.md
  auf den Online-Stand gebracht.

Abweichungen zur Fork-Vorlage. Die Takt-Änderungen (Untergrenze 30 Min,
Kostenvorschau im Anlege-Dialog, Fork-Commit 5b0b578) sind bewusst NICHT
enthalten, die Sätze stehen auf 45/40 statt der Fork-Defaults 24/33.

Getestet gegen eine Kopie der Staging-DB, 20 Prüfungen bestanden.
Migration, Flat-Buchung adhoc/research/chat, Warnschwelle einmalig,
Hard-Stop, Monatsreset, Übertrag gedeckelt, Bestandslizenz ohne Marke.
Die vier Live-Lizenzen stehen auf unlimited_budget und sind unbeeinflusst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 12:26:54 +00:00
claude-dev
ee31702015 feat(studio): Phase 3 - Fall-Chat (RAG) mit Guardrails, Timeline, Recherche-Angebot
Fall-Chat und Ereignis-Timeline, alles ueber Claude, strikt fallorientiert.

incidents.py:
- GET /{id}/events: Aktivitaets-Timeline (article_ingest/refresh/analysis/chat_qa/source_change)
- POST /{id}/ask: Fall-Chat/RAG mit Guardrails:
  * tools=None (kein Netz/kein Werkzeug) = zentrale Anti-Exfil-Abwehr (GitLost)
  * nur Materialien DIESES Falls (scope=alle entfaellt), [n]-Zitate, strikt aus Materialien
  * Claude-Vorauswahl der relevanten Artikel statt lokalem Embedding (kein lokales Modell)
  * _escape_prompt_content + Ausgabe-Leak-Filter + EchoLeak-Haertung (externe Bilder/Links/URLs raus)
  * Verbotsliste: kein Backend/App/Modell/Anbieter, kein Fremd-Fall
  * chat_qa in incident_events protokolliert
- Recherche-Angebot: liefert die Materialien die Frage nicht, haengt der Analyst einen
  needs_research-Block an (focus + description_addition, NUR aus der Frage abgeleitet).
- POST /{id}/clarify (nur auf Nutzer-Bestaetigung): Beschreibung ergaenzen + fokussierte
  Folge-Recherche AUSSCHLIESSLICH zur Frage (Researcher title=focus/description=frage),
  url-dedupliziert in den Fall. Laeuft als Hintergrund-Job (stage_runners.start_job).

stage_runners.py: generischer start_job() fuer Hintergrund-Jobs (run-status-sichtbar).
api.js: clarify(). studio.js + studio.css: Angebot rendern, bestaetigen, run-status pollen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:34:52 +02:00
claude-dev
b686685f43 feat(studio): Phase 2 - Studio-Bausteine, Backend-Kern (Sammeln/Analyse/Faktencheck)
Backend fuer die modularen Studio-Pipeline-Bausteine, alles ueber Claude.

DB (additiv, idempotent):
- Tabellen incident_events + fact_check_runs (+ Indizes)
- Funktion log_incident_event()
- Spalten incidents.summary_at, incidents.executive_summary, articles.geoparsed_at

agents/stage_runners.py (neu): isolierte Bausteine analyze + factcheck auf dem
vorhandenen Bestand, komplett neu, Historie bleibt (Snapshot/fact_check_runs).

orchestrator.py: collect_only in die Lane-Multi-Tenant-Queue eingebaut (enqueue_refresh
4-Tupel, _worker-Entpacken, Multi-Pass-Gate, _run_refresh-Signatur + Analyse/FC-Skip).
Zusaetzlich summary_at beim Schreiben des Lagebilds gestempelt (Studio-Freshness).

incidents.py: Endpunkte POST /{id}/run/{stage} (collect|analyze|factcheck),
GET /{id}/run-status, GET /{id}/freshness, GET /{id}/factcheck-runs[/{run_id}].
Busy-Check auf Online-Orchestrator (_current_tasks), Tenant-Zugriffspruefung.

Kein lokaler LLM-Unterbau, kein lokales Modell. /ask + /events folgen in Phase 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 23:18:15 +02:00
Claude Code
3a3076c4cf Orchestrator: eigener Worker pro Organisation (parallele Lanes)
Ersetzt die globale Warteschlange mit einem einzelnen Worker durch
Per-Tenant-Lanes: jede Organisation bekommt eine eigene Queue und einen
eigenen Worker-Task, die unabhaengig und parallel laufen. Lanes werden
lazy angelegt und bei Leerlauf (60s) wieder beendet.

- Skalarer Zustand (_current_task, _cancel_event) -> Maps pro incident_id
  (_current_tasks, _cancel_events); _queued_ids/_cancel_requested bleiben
  global (IDs sind lane-uebergreifend eindeutig).
- enqueue_refresh/cancel_refresh: Signatur unveraendert, routen intern per
  tenant_id in die passende Lane. Oeffentliche Lagen (tenant_id NULL) in
  fester PUBLIC_LANE.
- Optionales Ventil ORCHESTRATOR_MAX_PARALLEL (Default 0 = unbegrenzt)
  begrenzt bei Bedarf gleichzeitige Recherchen ueber alle Lanes.
- /refreshing-Endpoint auf _current_tasks-Map umgestellt, Response-Format
  unveraendert (pro Tenant laeuft weiterhin hoechstens eine Lage).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:12:03 +00:00
Claude Code
e20b3de0fa Lagebild: keine Stichwort-Fragmente und blanken Quellennummern-Dumps
Folgeregel zum Neu-am-Fix: verbietet Telegramm-Verkuerzungen
(Stichwort + [Nr]) und Auffangbloecke ohne Aussage (Fruehere Belege
[...]). Jede Quellennummer muss an einem vollstaendigen Satz haengen,
am Abschnitts- oder Lagebild-Ende keine reine Quellennummern-Liste.
2026-06-02 16:15:15 +00:00
Claude Code
d570e13dc6 Lagebild: keine datierten Neu-am-Verlaufsbloecke mehr
Der inkrementelle Analyse-Prompt liess das LLM neue Erkenntnisse als
datierte Changelog-Bloecke (Neu am DD.MM.) anhaengen, die nie eingefaltet
wurden. Beim Iran-Lagebild summierten sich so 151 solcher Bloecke. Punkt 3
fordert jetzt das Einarbeiten in den thematischen Abschnitt; zusaetzliche
STRUKTUR-Regel loest bestehende Neu-am-Bloecke auf. Die chronologische Sicht
bleibt der separaten Kachel Neueste Entwicklungen vorbehalten.
2026-06-02 15:52:59 +00:00
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
309c97f40a fix(geo+recall): Länder-Centroid statt Hauptstadt + Eigennamen in GNews-Query
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>
2026-05-22 02:13:30 +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
75038939b4 feat(topic-filter): jeden verworfenen Artikel einzeln loggen + Pre-Topic-Content auf 500 Zeichen erhöhen
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>
2026-05-21 19:03:36 +00: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
f22c8dbc61 fix: removepaywalls.com -> removepaywall.com (Singular ist die echte Domain)
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.
2026-05-09 05:00:11 +00:00
Claude Code
5a123ef3b8 fix(researcher): Lagentitel-Eigennamen als Pflicht-Keywords (Bug 2 Buckelwal-Diagnose)
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.
2026-05-09 03:52:36 +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
f73c21235e feat(translator): Feature-Flag TRANSLATOR_ENABLED zum Abschalten (siehe main) 2026-05-03 20:43:40 +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
6293cef91e Banner-Text + AI-Disclaimer-Modal + Translator-Robustheit
#28 Banner-Text bei Token-Budget aufgebraucht:
- middleware/license_check.py + static/js/app.js: Statt "Bitte Verwaltung
  kontaktieren" jetzt konkreter Upgrade-Pfad mit info@aegis-sight.de.

#29 AI-Hallucination-Disclaimer:
- Neue static/js/ai-disclaimer.js (analog zu update-system.js):
  IIFE-Modul, localStorage-versioniert (aegis_ai_disclaimer_seen=v1),
  inline-CSS mit Theme-Variablen, Modal mit Lucide-Info-Icon.
- Wird beim ersten Login einmalig gezeigt; ueber Header-User-Dropdown
  Eintrag "Ueber KI-Inhalte" jederzeit erneut oeffenbar.
- dashboard.html: Script-Tag + Dropdown-Button mit Lucide-SVG.
- style.css: kleiner Stil-Block fuer .header-dropdown-action.

Translator-Robustheit (Bonus):
- agents/translator.py: Parser akzeptiert jetzt auch von Claude wrapped
  Antworten ({{translations: [...]}}, {{items: [...]}}, einzelnes
  Object). Behebt Wrapper-Bug der gestern beim Backfill 75% der Calls
  fehlschlagen liess.
- Prompt deutlicher: "flaches JSON-Array, kein Wrapper".
2026-05-03 13:29:19 +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
c73541cdbe Block C: Prompt-Umlaute korrigiert + Timeout parametrisiert
- ENHANCE_PROMPT_ADHOC und ENHANCE_PROMPT_RESEARCH: Umschreibungen durch
  echte Umlaute ersetzt (fuer -> fuer, praezises -> praezises, ...). Behebt
  den Widerspruch, dass der Prompt "echte Umlaute verwenden" forderte,
  die Anweisung selbst aber ae/oe/ue/ss nutzte.
- call_claude() bekommt neuen timeout-Parameter. None = Fallback auf
  CLAUDE_TIMEOUT (1800s), sonst Override in Sekunden. asyncio.wait_for
  und die cancel-aware Variante nutzen durchgaengig den effective_timeout.
- Enhance-Endpoint ruft call_claude mit timeout=60 auf (Haiku-Single-Shot,
  vorher global 1800s).
- chat.py _call_claude_chat: Timeout von 60s auf 120s erhoeht (Chat-Antworten
  koennen etwas laenger dauern, haben aber keinen Anspruch auf 30 Min).
2026-04-23 17:56:28 +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
34be98edaf Latest-Developments: Bullet-Format Name|URL statt nur Name
Problem: Pill-Link verwies auf falschen Post, weil sources_json fuer
Telegram-Kanaele viele Eintraege mit gleichem Namen aber unterschiedlichen
Post-URLs hat. Der Name-Match traf den ersten Eintrag (falschen Post).

Fix: Bullet-Format von {Name, Name} auf {Name|URL, Name|URL} erweitert.
Backend-Parser loest {M<ID>} nun zu Name|URL auf, URL kommt direkt vom
articles.source_url des belegenden Artikels. Kein sources_json-Lookup
noetig, keine Name-Kollision mehr moeglich.

Backend (analyzer.py):
- _parse_latest_developments: articles_by_id speichert (name, url) Tuple,
  Items werden als Name|URL gespeichert. Uebernommene Klammer-Items mit
  Pipe werden akzeptiert. Legacy-Items ohne Pipe bleiben als reiner Name.
- Prompt-Regel und Output-Beispiel auf {Name|URL, Name|URL} erweitert.

Frontend (components.js):
- buildPill-Aufruf vor Pipe-Split: Name und URL getrennt, wenn URL vorhanden
  wird Pseudo-src {name, url} uebergeben — eindeutiger Klicklink. Ohne URL
  Fallback auf lookupByName in sources_json (fuer Legacy-Bullets).
2026-04-18 23:19:02 +00:00
claude-dev
8a888a17a5 Live-Monitoring: Parser toleranter (Dash optional, Datum ohne zweiten Punkt) + Backfill-Script
Claude Haiku 4.5 laesst gelegentlich den fuehrenden Dash oder den zweiten
Datums-Punkt im Bullet-Format weg (z.B. "[18.04 21:49]" statt
"- [18.04. 21:49]"). Der strikte Parser-Regex verwarf dadurch alle Bullets.

- Regex akzeptiert nun Dash als optional und zweiten Datums-Punkt als optional
- Parser normalisiert Datum + Zeit auf kanonisches Format "DD.MM. HH:MM" mit Zero-Padding
- Frontend-Regex analog toleranter (auch fuer Altdaten-Mix)
- OUTPUT-FORMAT-Hinweis im Prompt verschaerft ("JEDE Zeile beginnt mit - ")

Backfill-Skript (scripts/backfill_latest_developments.py): Laedt die N
neuesten Artikel einer Lage aus der DB und ruft generate_latest_developments
mit previous_developments=None auf — nuetzlich nach DB-Cleanups, wenn die
inkrementelle Logik zu wenige Bullets liefert.

Einmaliger Run fuer Lage #66 (Militaerblogger): 8 Bullets vom 18.04. mit
aufgeloesten Quellen (Spiegel, Guardian, Bloomberg, n-tv, Telegram-Kanaele).
2026-04-18 21:14:44 +00:00