Commit graph

377 Commits

Autor SHA1 Nachricht Datum
Claude Code
14d1062583 Fix: Blur greift bei jedem ersten Durchlauf der ausgewaehlten Lage
Vorheriger Patch hatte den rAF nur im Create-Flow gesetzt. Damit funktionierte
zwar das Anlegen, aber das Auswaehlen einer existierenden Lage, deren erste
Recherche gerade laeuft (oder nach einem manuellen ersten Refresh), blieb
unblurred.

Loesung: rAF mit add("blurred") jetzt zentral in selectIncident, sobald der
Progress-State isFirst=true und nicht minimiert ist. Damit greift der Blur
in allen Pfaden (Anlegen, manueller Refresh, Auswahl einer laufenden Lage,
Wechsel zwischen Faellen, Initial-Load via savedId).

Der zusaetzliche rAF in createIncident von 2ee90a4 ist damit redundant und
wieder entfernt — der zentrale Hook in selectIncident deckt den Fall mit ab.

cache-bust app.js auf v=20260427b.
2026-04-27 19:43:18 +00:00
Claude Code
2ee90a4b3b Fix: Blur greift sofort beim Anlegen einer neuen Lage
Im Create-Flow wurde .blurred in selectIncident() erst entfernt und gleich
darauf via _showPopupProgress wieder gesetzt. CSS filter:blur greift in der
Kombination (frischer isFirst-State + selectIncident im selben Tick + viel
vorausgehende DOM-Manipulation durch Modal-Close + renderSidebar) nicht
zuverlaessig im selben Frame — der Fall war bis zum Wegklicken/Zurueckklicken
unblurred.

selectIncident: remove("blurred") wird uebersprungen, wenn der zustaendige
Progress-State isFirst=true und nicht minimiert ist (Blur soll bestehen
bleiben statt remove+add im selben Tick).

createIncident: zusaetzlich requestAnimationFrame mit grid.classList.add im
naechsten Frame, falls der Browser den ersten add-Aufruf in selectIncident
nicht visuell uebernommen hat. Doppeltes Setzen ist idempotent.

dashboard.html: cache-bust app.js auf v=20260427a.
2026-04-27 19:29:00 +00:00
d1f88c9e9f Release-Notes: Update-Modal beim ersten Besuch 2026-04-26 23:08:07 +02:00
Claude Code
ad53786a24 Update-System: Modal auch beim ersten Besuch zeigen
Frueher wurde beim allerersten Treffen mit dem Update-System der lastSeen-
Marker stillschweigend gesetzt, ohne Modal — User sah erst beim ZWEITEN
Update was. Fuer Kunden-Onboarding ist das suboptimal: sie sollen direkt
sehen, dass das System existiert und welche Updates aktuell sind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:43:24 +00:00
a9806a586b Release-Notes: Updatenachricht bei Deployment 2026-04-26 22:40:34 +02:00
Claude Code
2aaa51e2a8 Update-System Frontend: Banner + Was-ist-neu-Modal
Beim ersten Login nach einem Update zeigt der Monitor nun ein Modal mit den
Release-Notes des Updates (aus RELEASES.json). Wenn waehrend einer laufenden
Sitzung ein neues Update live geht, erscheint unten rechts ein Banner mit
einem Aktualisieren-Knopf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:32:50 +00:00
Claude Code
2df37cb617 Update-System: /api/version + /api/release-notes + RELEASES.json
Frontend kann jetzt erkennen, wann eine neue Version live ist, und dem Nutzer
einen passenden Hinweis sowie die Release-Notes anzeigen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:28:10 +00:00
Claude Code
5473ba3ed7 WICHTIG: DB_PATH per ENV ueberschreibbar; data-Symlink aus Repo entfernt
Verhindert dass Staging und Live versehentlich dieselbe DB nutzen
(Symlink data wurde frueher beim git clone mitgeklont und zeigte
auf das gleiche physische Verzeichnis /home/claude-dev/osint-data).
Staging muss jetzt DB_PATH in der .env explizit setzen.
2026-04-26 19:42:33 +00:00
Claude Code
8042639d20 CLAUDE.md: Auto-Deploy + Promote-UI + Live-systemd dokumentiert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:14:35 +00:00
claude-dev
ec53ab27cd CLAUDE.md: Staging-Umgebung dokumentiert (Service, DB, .env, Workflow)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:56:29 +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
c8a8e10020 Chat-Doku aktualisiert + Tutorial-Einstieg temporaer deaktiviert
- Chat-System-Prompt: Aktualisierungs-Modi (Minuten/Stunden/Tage/Wochen, 10-Min-Minimum, Startzeit), 5 Faktencheck-Status (Bestaetigt, Gesichert, Unbestaetigt, Umstritten, Widerlegt), Export mit PDF/DOCX und Bereichsauswahl
- Tutorial-Button in Sidebar auskommentiert (Ueberarbeitung)
- Tutorial-Trigger im Chat auskommentiert (Opener-Hinweis und Keyword-Erkennung)
2026-04-23 17:43:27 +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
efae707fa9 Fix: Blur + Aktions-Lock beim Anlegen eines Falls sofort aktiv
Beim Create-Flow wurde selectIncident() aufgerufen, BEVOR der Fall
als refreshend markiert wurde. Dadurch entfernte selectIncident den
'.blurred'-Zustand des Tab-Containers und rief _lockActionsIfFirst(false)
auf — der Fallinhalt war zwischen Oeffnen und Eintreffen der ersten
WebSocket-Statusnachricht kurzzeitig klickbar und unblurred.

Jetzt wird der Refresh-Status und ein Initial-State mit isFirst=true
schon VOR selectIncident gesetzt. selectIncident erkennt isRefreshing
und ruft _showPopupProgress + _lockActionsIfFirst(true) mit dem
bestehenden State auf — Blur und Lock greifen sofort.
2026-04-21 14:02:52 +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
285df86c7b Export-Metadaten: Umlaut-Fix, xmpMM:VersionID + History
- dc:rights und xmpRights:UsageTerms: Empfaenger -> Empfänger (echte Umlaute)
- Scope-Labels: Vollstaendiger Bericht -> Vollständiger Bericht (zwei Stellen)
- DOCX-Fallback-Text: verfuegbar -> verfügbar
- xmpMM:VersionID: Snapshot-Count der Lage (Proxy fuer Berichts-Revision).
  Router laedt COUNT(*) FROM incident_snapshots und reicht es durch.
- xmpMM:History: Audit-Event pro Export als rdf:Seq-Eintrag mit Timestamp,
  softwareAgent, InstanceID, Scope und Version. Single-Event-Format aus
  pragmatischem Grund (pikepdf-API unterstuetzt keine nativen stEvt-
  Strukturen; Raw-XML-Injection waere dafuer noetig).
2026-04-20 19:33:18 +00:00
claude-dev
5add8d9d59 Export-Metadaten: Dublin Core, xmpRights und xmpMM nachruesten
Zusaetzliche XMP-Felder im PDF:
- dc:publisher (Organisation, Fallback AegisSight)
- dc:identifier (urn:aegissight:incident:<id>:<timestamp>)
- dc:date (Dokumentendatum, ergaenzend zu xmp:CreateDate)
- dc:format (application/pdf)
- dc:type (Report)
- dc:rights (Vertraulichkeitshinweis)
- pdf:Producer im XMP gespiegelt
- xmpRights:Marked (True) und xmpRights:UsageTerms (= dc:rights)
- xmpMM:DocumentID + xmpMM:InstanceID (UUIDs, frisch pro Export)

Damit koennen DMS-Systeme die Berichte versionieren, eindeutig
identifizieren und Vertraulichkeitshinweise anzeigen.
2026-04-20 19:23:54 +00:00
claude-dev
949df868ff Export: XMP-Metadatenblock und CreationDate/ModDate via pikepdf nachziehen
WeasyPrint 68.1 schreibt weder XMP noch Create-/ModDate ins PDF. Das Post-
Processing via pikepdf ergaenzt beide:

- Info-Dict: /CreationDate + /ModDate im PDF-Standardformat
  (D:YYYYMMDDHHmmSS+HHmm) aus Incident.created_at / updated_at
- XMP-Block mit Dublin Core (dc:title, dc:creator, dc:description,
  dc:subject, dc:language), PDF (pdf:Keywords) und XMP (CreatorTool,
  CreateDate, ModifyDate, MetadataDate) Namespaces

Damit werden die Exporte sowohl von klassischen Tools (Explorer, Finder)
als auch von DMS-Systemen (SharePoint, Bridge, Acrobat) vollstaendig
indexiert. Fallback: Bei Fehler im Post-Processing wird das Original-PDF
zurueckgegeben, Export schlaegt nie fehl.
2026-04-20 19:15:14 +00:00
claude-dev
9293e66d01 Export-Metadaten: category_labels JSON-robust parsen, Keyword-Sanitizer
- category_labels ist in der DB ein JSON-Dict (primary/secondary/tertiary/
  mentioned), nicht ein Komma-String. Der bisherige split(",") fuehrte dazu,
  dass ein nacktes { als Keyword durchrutschte. WeasyPrint bricht den
  PDF-Keywords-Stream an dieser Stelle ab, weil { in PDF-Syntax eine
  Sonderbedeutung hat — Ergebnis war "OSINT, Live-Monitoring, AegisSight, {".
- Neuer Parser: erst JSON (Dict oder Liste), Fallback auf Komma-String.
- _sanitize_keyword(): filtert {, }, [, ], Backslash und normalisiert
  Whitespace in allen Keywords (Defense in Depth).
2026-04-20 19:09:38 +00:00
claude-dev
c0f68e40a5 Export: PDF/DOCX-Dateimetadaten (Title, Author, Subject, Keywords, Category, Comments)
- Neue Helper-Funktion _build_export_metadata baut einheitliches Metadaten-Dict
- PDF via HTML-Meta-Tags (title, author, description, keywords, generator, lang)
- DOCX via doc.core_properties (title, author, subject, keywords, comments,
  category, last_modified_by, language, content_status, created, modified)
- Keywords aus OSINT + Typ + Organisation + category_labels + Top-5-Orten
- Comments-Feld mit strukturiertem Block (Incident-ID, Typ, Scope, Umfang, Orte)
- Router laedt Organisation + Top-Orte aus article_locations und reicht sie durch
2026-04-20 18:58:34 +00:00
0d6ad8ea90 Incident-Response: sources_json nur noch via Lazy-Endpunkt, Sidebar schlank
Backend:
- IncidentResponse: sources_json-Feld entfernt (Detail-GET liefert es
  nicht mehr mit).
- Neues Schema IncidentListItem fuer GET /incidents (Sidebar):
  Ohne summary, ohne sources_json. Ein has_summary-Bit fuer
  Erster-Refresh-Erkennung, description bleibt fuer das Edit-Modal.
- list_incidents selektiert nur die noetigen Spalten (kein SELECT *)
  — spart bei grossen Lagen Speicher + Serialisierung.
- Neuer Endpunkt GET /incidents/{id}/sources liefert geparstes
  Sources-Array fuer Zitate-Lookups (Lazy).

Frontend:
- api.js: getIncidentSources(id).
- app.js: loadIncidentDetail laedt /sources parallel, speichert Array
  in _currentSources. Alle renderSummary/Zusammenfassung/
  LatestDevelopments-Aufrufe bekommen jetzt _currentSources statt
  incident.sources_json. inc.summary-Checks -> inc.has_summary.
- components.js: _parseSources(input) akzeptiert Array ODER String
  (Rueckwaertskompatibilitaet). renderZusammenfassung, renderSummary,
  renderLatestDevelopments nutzen den Helper.

Hintergrund: Die Sidebar-Liste lieferte bei 17 Lagen 1,23 MB
(Iran allein 386 KB wegen sources_json + summary). Detail-Endpunkt
lieferte sources_json (324 KB bei Iran) bei jedem Oeffnen mit.
Beides jetzt radikal kleiner — die 324 KB Sources gibt's nur
einmalig auf Anfrage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 00:07:46 +02:00
a302790777 Locations: Aggregation in SQL (GROUP BY + Window)
Ersetzt den rohen JOIN ueber article_locations x articles (bei Iran
21.814 Zeilen, 11 MB Payload) durch drei kleine aggregierte Queries:
  1. Orte per GROUP BY (name, lat, lon) — direkt die Ergebnismenge.
  2. Kategorien pro Ort per GROUP BY fuer die dominante Kategorie.
  3. Sample-Artikel (max. 10 pro Ort) via ROW_NUMBER() OVER PARTITION BY.

Response-Shape unveraendert ({category_labels, locations: [...]}), keine
Frontend-Aenderung noetig. Priorisierung primary > secondary > tertiary >
mentioned bleibt erhalten.

Erwarteter Effekt: Iran-Locations 11 MB -> <500 KB; Query-Zeit sinkt
zusaetzlich, da kein 21k-Zeilen-JOIN mehr materialisiert werden muss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:47:50 +02:00
9a43dffa6c Articles: Paginierung, Timeline-Buckets, Sources-Summary-Endpunkt
Backend:
- GET /{id}/articles paginiert jetzt per limit/offset (Default 500,
  Max 1000) und unterstuetzt optionalen search-Parameter (LIKE ueber
  headline/source/content). Response-Shape: {total, articles}.
- Neuer Endpunkt GET /{id}/articles/sources-summary liefert pro Quelle
  {source, article_count, languages} sowie language_counts gesamt —
  serverseitige Aggregation, unabhaengig von Artikel-Paginierung.
- Neuer Endpunkt GET /{id}/articles/timeline-buckets?granularity=hour|day|week|month
  aggregiert Artikel + Snapshot-Counts pro Zeitbucket (fuer spaetere
  Timeline-Zaehler ueber die volle Historie).
- database.py: Index idx_articles_incident_collected auf
  (incident_id, collected_at DESC) fuer schnelleres ORDER BY + Pagination.

Frontend:
- api.js: getArticles({limit, offset, search}),
  getArticlesSourcesSummary(), getArticlesTimelineBuckets().
- app.js: loadIncidentDetail laedt erste Seite (500 Artikel), startet
  _loadSourcesSummary parallel und zieht restliche Artikel
  batchweise (500er Bloecke) im Hintergrund nach, bis _currentArticlesTotal
  erreicht ist. rerenderTimeline nach jedem Batch.
- components.js: renderSourceOverviewFromSummary(data) rendert aus
  Aggregat-Daten (ersetzt clientseitige Zaehlung ueber geladene Artikel).

Hintergrund: /articles lieferte bei der Iran-Lage 22 MB (17.286 Artikel
mit SELECT *). Die Erstantwort sinkt auf ~650 KB (500 Artikel), weitere
werden progressiv im Hintergrund nachgeladen. Quellenuebersicht zeigt
dank Aggregat-Endpunkt sofort alle Quellen + Sprachen komplett.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:46:40 +02:00
194790899c Snapshots: Liste ohne Volltext, Lazy-Load + serverseitige Suche
Backend:
- GET /{id}/snapshots liefert nur noch schlanke Shape (Metadaten +
  SUBSTR(summary,1,300) AS summary_preview), kein Volltext, kein sources_json.
- Neuer Endpunkt GET /{id}/snapshots/{snapshot_id} fuer Volltext-Lazy-Load.
- Neuer Endpunkt GET /{id}/snapshots/search?q=... fuer serverseitige
  Volltextsuche ueber alle Snapshots einer Lage.

Frontend:
- api.js: getSnapshot() und searchSnapshots() ergaenzt.
- app.js: _snapshotFullCache, Volltext wird beim Aufklappen eines
  Snapshot-Eintrags per lazyLoadSnapshotDetail() nachgeladen und gecacht.
- Suche ueber Snapshots filtert weiterhin clientseitig ueber summary_preview.

Hintergrund: Bei grossen Lagen (Iran-Lage: 347 Snapshots) fiel die
Snapshots-Listenantwort mit Volltext-Summaries auf ~54 MB. Die Liste
faellt damit auf ~150 KB; Volltexte werden nur on-demand geladen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:42:08 +02: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).
pre-incident-perf-fix
2026-04-18 23:19:02 +00:00
claude-dev
82e46792c7 Source-Pill: Clip entfernt, langer Kanal-Pfad vollstaendig sichtbar
max-width/overflow-hidden/text-overflow-ellipsis aus .dev-source-pill raus.
Stattdessen white-space: normal + overflow-wrap: anywhere — Pill waechst
mit Inhalt, Zeile kann umbrechen. Beispiel Telegram-Kanal iranmilitarymag
war vorher bei (t.me/iranmilitaryma... abgeschnitten.
2026-04-18 22:57:54 +00:00
claude-dev
e495fa8e61 Telegram-Pill: Kanal-Pfad statt generisches Label
Statt allgemeinem (Telegram-Link) wird jetzt der tatsaechliche Kanal-Pfad
angezeigt, z.B. (t.me/iranmilitarymag) — extrahiert aus der Source-URL
per Regex. Damit ist der Kanal auf einen Blick erkennbar, auch wenn der
Quellenname in nichtlateinischer Schrift vorliegt.
2026-04-18 22:53:09 +00:00
claude-dev
e15ed0c21e Dashboard: GridStack durch Tab-Navigation ersetzen
Der Monitor-Dashboard zeigte bisher alle sechs Kacheln gleichzeitig in
einem GridStack-Layout (Drag/Resize, je Kachel eigenes Scrolling). Nutzer-
wunsch: Analog zur Lagebild-Seite nur ein Tab-Panel gleichzeitig, maximiert
auf volle Breite, Seiten-Scroll statt interne Scrollbars.

Aenderungen:
- dashboard.html: Layout-Toolbar + grid-stack-Wrapper entfernt; neue tab-nav
  mit 6 Buttons + tab-panels mit 6 Panels. GridStack CDN-Links raus.
- layout.js: GridStack-Init/toggleTile/reset komplett entfernt. Neu:
  switchTab(tabId) + restoreTabFor(incidentId) mit localStorage-Persistenz
  pro Lage osint_tab_id. applyTypeLabels fuer adhoc vs. research. Legacy-
  Methoden sind No-Op-Stubs.
- app.js: renderIncidentDetail ruft LayoutManager.restoreTabFor und
  applyTypeLabels auf. openContentModal-Trigger aus Card-Titeln raus.
  Tile-Resize-Bloecke fuer Quellen und Timeline entfernt.
- components.js: Telegram-Pills bekommen Suffix Telegram-Link, wenn die
  URL auf t.me verweist.
- style.css: grid-stack/layout-toggle Klassen raus; neue tab-nav/tab-btn/
  tab-panel Klassen. Internes Scrolling entfernt. map-container 600px.

Alte osint_layout-Eintraege werden ignoriert.
2026-04-18 22:34:36 +00:00
claude-dev
3b9e9e25c2 public_api: latest_developments in Incident-Response aufnehmen
Die oeffentliche API (/api/public/lagebild) liefert jetzt latest_developments
als Feld im Incident-Objekt. Damit kann der Website-Sync das Feld in
current.json und summary.json uebertragen, und die Lagebild-Seite kann einen
Tab Neueste Entwicklungen rendern.
2026-04-18 22:05:45 +00:00
claude-dev
f05bd1a064 QC: Umlaut-Dict aus hunspell-de-de generieren (statt handkuratiert)
Loest das Abdeckungs-Problem des handkuratierten Dicts (~300 Eintraege,
~95%). Neu: vollautomatisch erzeugtes Korpus-Dict aus hunspell-de-de
mit 153.869 Eintraegen (>99% Abdeckung), plus schlankes Supplement
fuer Komposita, die hunspell nicht liefert.

Build-Skript (scripts/build_umlaut_dict.py):
- ruft /usr/bin/unmunch gegen /usr/share/hunspell/de_DE.dic+aff auf
- filtert Woerter mit echten Umlauten (ä/ö/ü/ß)
- generiert je Wort die Umschreibungsform (ae/oe/ue/ss) + Capitalize
- Mehrdeutigkeits-Check: skippt Paare wo die Umschreibung selbst
  ein gueltiges deutsches Wort ist (z. B. dass/daß, Masse/Maße, Busse/Buße)
- Ergebnis: 153.869 Eintraege, 27 mehrdeutige Formen ausgefiltert
- Alphabetisch sortiertes JSON (diff-freundlich)

Laufzeit-Refactor (src/services/post_refresh_qc.py):
- _UMLAUT_BASE Dict (handkuratiert) entfernt, dafuer JSON-Loader
  beim Modul-Import aus src/services/umlaut_dict.json
- _MANUAL_SUPPLEMENT fuer Luecken (Konjunktiv saeen, Amtstitel-
  Komposita wie Aussenminister/Parlamentspraesident, Strassen-
  Komposita, Fuehrungs-Komposita) — ueberlagert Korpus-Dict
- _UMLAUT_WHITELIST erweitert um englische Fremdwoerter (Boeing,
  Business, Access, Process, Message, Password, Miss, Boss, Goethe,
  Yahoo, Israel, Israels)
- Regex-Strategie umgestellt: statt riesigem alternierenden Pattern
  ueber alle Keys jetzt Tokenizer (_WORD_PATTERN) + O(1) Dict-Lookup
  pro Wort. Deutlich performanter bei 150k+ Eintraegen.
- normalize_german_umlauts() Signatur unveraendert
- normalize_umlaut_fields() unveraendert
- Einhaengung in run_post_refresh_qc() unveraendert

Daten-Artefakt (src/services/umlaut_dict.json):
- 4.88 MB alphabetisch sortiertes JSON
- Im Repo committet zwecks Reproduzierbarkeit und kein hunspell-
  Laufzeit-Abhaengigkeit im Container

Verwerfbarkeit voll erhalten:
- git revert entfernt alle drei neuen Elemente
- Bestand in DB bleibt repariert (korrektes Deutsch, kein Schaden)
- hunspell-Paket kann bleiben oder mit apt purge entfernt werden

Bootstrap-Rerun mit neuem Dict:
- 7 Lagen aktualisiert, 306 zusaetzliche Ersetzungen
- Lage #6 (Irankonflikt) von 140 ursprungs- und 15 Rest-Treffern
  nach voriger Runde jetzt auf 0 Hard-Hits
- andere aktive Lagen insgesamt 8 verbleibende Rest-Treffer
  (spezielle Eigennamen, koennen bei Bedarf ins Supplement)

Performance:
- Dict-Load beim Modul-Import: ~100 ms
- Gesamt Unit-Tests (11 Faelle): 161 ms
- Refresh-Pfad unveraendert schnell: O(Wortzahl) mit Hashmap-Lookup
2026-04-18 21:17:46 +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
claude-dev
89ab158202 Live-Monitoring: Quellen-IDs deterministisch aufloesen, Bias-Markierung raus
Aenderung am Grund-Mechanismus: LLM liefert pro Bullet die Meldungs-IDs
im Format {M<ID>, M<ID>}, das Backend loest die IDs gegen new_articles
zu Quellen-Namen auf und schreibt {Reuters, Rybar} in die DB. Uebernommene
Bullets aus previous_developments behalten ihre bestehende {Name}-Klammer.

Bullets ohne Quellen-Klammer oder mit unaufloesbarer Klammer werden vom
Parser verworfen — dadurch existiert "Keine Quelle" nicht mehr.

Frontend: Bias-Farbcodierung (pro-RU, staatsnah) + zugehoerige Heuristik
_classifyBias/_biasLabel entfernt. Kein Sonderfall-Rendering fuer leere
Pills mehr.
2026-04-18 20:50:46 +00:00
claude-dev
5c95d85871 Live-Monitoring: Quellen-Namen pro Bullet (Prompt + Frontend-Parser)
Der LATEST_DEVELOPMENTS-Prompt produzierte Bullets ohne Citations — das
Frontend zeigte daher "Keine Quelle". Prompt ergaenzt: jedes Bullet endet mit
{Quellenname1, Quellenname2} (geschweifte Klammern, exakte Schreibweise aus
Quelle:-Zeile). Frontend-Parser extrahiert diese Klammer, matcht Namen
case-insensitive gegen sources_json und erstellt klickbare Pills.

Fallback fuer Legacy-Bullets: Inline-[N]-Citations werden weiterhin erkannt.
Altbestand-Bullets ohne Marker erhalten beim naechsten Refresh Quellen.
2026-04-18 20:27:16 +00:00
claude-dev
2ae8b9a341 Live-Monitoring: Neueste Entwicklungen als Karten mit Quellen-Pills
Der Bullet-Render fuer Live-Monitoring (adhoc) zeigt nun pro Eintrag eine
Karte mit klickbaren Quellen-Pills (Quellname statt nur [N]) im Header und
dezentem Zeitstempel rechts oben. Der Ereignistext steht darunter ohne
Inline-Citations. Bias-Markierung (pro-RU, staatsnah) als kleines Suffix.

Recherchen behalten den bisherigen renderZusammenfassung-Render unveraendert.
2026-04-18 19:53:21 +00:00
claude-dev
15a650bfc9 QC: Umlaut-Normalisierung + Prompt-Ergaenzung
Drei unabhaengige Schutzschichten gegen falsche Umschreibungen
(ae/oe/ue/ss statt ä/ö/ü/ß) im Lagebild:

1. Prompt-Ergaenzung in INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE und
   INCREMENTAL_BRIEFING_PROMPT_TEMPLATE (analyzer.py): explizite
   Priorisierung, dass die Regel "echte UTF-8-Umlaute" Vorrang vor
   "bestehende Formulierungen beibehalten" hat. Adressiert den Fall,
   dass Claude beim inkrementellen Update Altlasten weitertraegt.

2. Deterministische Normalisierung in post_refresh_qc.py:
   - normalize_german_umlauts(text) - Regex mit Wortgrenzen, case-
     preserving, Whitelist-tauglich, ~140 Eintraege im Woerterbuch
     abgeleitet aus den 140 Hard-Hits in Lage #6
   - normalize_umlaut_fields(db, incident_id) - laedt summary und
     latest_developments, normalisiert, schreibt nur bei Aenderungen
     zurueck (idempotent)
   - Eingehaengt in run_post_refresh_qc() nach dem Location-Check,
     Fehler stoppen die Pipeline nicht (identisches Muster wie
     bestehende Checks)

3. scripts/bootstrap_umlaut_repair.py - Einmal-Skript zur
   Bestandsbereinigung der bereits gespeicherten summary-Felder.
   Idempotent. Beim initialen Lauf auf Produktiv-DB: 14 Lagen
   aktualisiert, 431 Ersetzungen insgesamt, Lage #6 von 140 auf
   15 Rest-Treffer reduziert.

Whitelist (leer): aktuell kein Konflikt zwischen deutschen Ziel-
Woertern und englischen Fremdwoertern. Kann bei Bedarf erweitert
werden ohne Schema-Aenderung.

Verifikation:
- py_compile OK fuer alle drei Dateien
- Service-Restart ohne Errors
- Unit-Tests: positive Faelle ("Oeffnung der Strasse" -> 4 Ersetzungen),
  Whitelist ("Boeing liefert Business-Access" -> 0 Ersetzungen),
  Komposita ("Wasserstrasse", "Parlamentspraesident") korrekt
- Bootstrap 2x ausgefuehrt (erster Lauf 288 Ersetzungen, zweiter 143
  nach Dict-Erweiterung), kumulativ 431

Architektur bleibt dormant ohne Daten-Altlasten: wenn keine Lage
Umschreibungen enthaelt, arbeitet normalize_umlaut_fields in <1ms
und schreibt nichts. Kein Overhead im Refresh-Pfad.
2026-04-18 14:00:00 +00:00
claude-dev
ed2ab1f3fc YouTube-Fallback aus Podcast-Kaskade entfernt
Der geplante YouTube-Captions-Fallback (Phase 2 via yt-dlp) wird nicht
umgesetzt. Begruendung: strategische Entscheidung, keinen YouTube-Scrape
als Quelle zu nutzen.

Geaendert:
- src/feeds/transcript_extractors/__init__.py:
  - try/except-Import fuer youtube-Modul entfernt (nie existiert)
  - Modul-Docstring aktualisiert (Stufen 1+2, kein 3)
  - source-Enum-Kommentar: nur noch rss_native / website_scrape

Konsequenz: Episoden, die weder Podcasting-2.0-Tag noch Sender-Manuskript
haben (z. B. Paywall-Inhalte bei FAZ/Handelsblatt), werden dauerhaft
verworfen. Fuer deutsche Qualitaetsmedien-Podcasts (Dlf, NDR, SZ, Spiegel,
ZEIT wo frei) reichen die zwei aktiven Stufen.
2026-04-18 12:30:28 +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
acfc74ffe7 Standard-Opus auf claude-opus-4-7 festlegen (statt CLI-Default) 2026-04-16 22:19:26 +00:00
Claude
0ea7f9e305 report-export: verlinkte Zitate in Zusammenfassung und Bericht 2026-04-14 17:55:01 +00:00
claude-dev
def12ecf11 Scrolling fuer Zusammenfassung-Kachel hinzugefuegt
Zusammenfassung-Content-Container bekommt overflow-y: auto und
Scrollbar-Styling analog zu Lagebild, Faktencheck und Timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 11:25:14 +00:00
claude-dev
3379151fa7 Export: Alle verbleibenden Grautöne auf Navy #0a1832 für Drucklesbarkeit
- PDF: Seitenzahlen, Timeline-Datum/-Quelle, Report-Footer, Lagebild-Timestamp
- DOCX: Dokument-Footer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 01:17:57 +00:00
claude-dev
048c347616 PDF-Export: Inhaltsverzeichnis + Seitenumbrüche pro Abschnitt
- Seite 2: Dynamisches Inhaltsverzeichnis mit klickbaren Anker-Links
- Nur ausgewählte Bereiche erscheinen im Verzeichnis (CSS Counter)
- Jeder Abschnitt beginnt auf neuer Seite (page-break-before)
- Redundante Inline-Styles für Seitenumbrüche entfernt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 01:15:23 +00:00
claude-dev
96463824a7 Export: Executive Summary → Zusammenfassung, Deckblatt-Farben druckfähig
- Alle sichtbaren "Executive Summary"-Bezeichnungen durch "Zusammenfassung" ersetzt
  (PDF/DOCX-Überschrift, Dateiname, Fallback-Texte)
- Deckblatt-Farben von #888/#aaa auf Navy #0a1832 geändert für
  bessere Lesbarkeit beim Druck (PDF-Template + DOCX)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:42:55 +00:00
claude-dev
4358020c83 Export-Fix: published_at als int abfangen (TypeError)
pub kann int sein statt String -- str() Konvertierung vor Slicing.
2026-04-11 22:51:26 +00:00
claude-dev
509165484e Zusammenfassung-Kachel: Quellenverweise als klickbare Links
renderZusammenfassung bekommt jetzt sourcesJson und rendert
[1], [2] etc. als klickbare Links -- identisch zu renderSummary.
2026-04-11 22:34:04 +00:00
claude-dev
db662f4538 Zusammenfassung: Kompatibilitaet mit bestehendem ÜBERBLICK
- Frontend + Backend erkennen jetzt sowohl ## ZUSAMMENFASSUNG als
  auch ## ÜBERBLICK als Zusammenfassungs-Sektion
- Inkrementelles Prompt weist Modell an, ÜBERBLICK in
  ZUSAMMENFASSUNG umzubenennen und als Bullet-Points zu formatieren
- Bestehende Lagen zeigen Zusammenfassung sofort in der Kachel
2026-04-11 22:12:23 +00:00