Quellen-Boxen waren bisher reine Anzeige. Jetzt sind sie klickbar:
beim Klick erscheint direkt unter der Box (über die volle Grid-Breite)
eine Liste der Artikel-Headlines dieser Quelle, jede mit Link zum
Originalartikel. Mutual-exclusive — Klick auf eine andere Quelle
schließt die vorherige automatisch.
- components.js: Item bekommt data-source, onclick + Tastatur-Support
(Enter/Space), aria-expanded.
- app.js: toggleSourceOverviewDetail filtert _currentArticles nach
Quelle, sortiert chronologisch absteigend, fügt das Detail-Element
via insertAdjacentElement direkt nach dem geklickten Item ein.
- CSS: aktiver Item-Status (Glow + Tint), Detail-Block mit
grid-column 1/-1 (volle Breite) + max-height 320px scrollbar bei
vielen Artikeln + dezente Slide-In-Animation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problem: Beim Anlegen einer neuen Lage verschwand der Blur-Effekt auf dem
Hintergrund-Inhalt sobald das Browserfenster in der Groesse veraendert wurde.
Zudem blieb der Lagen-Titel im Header offen sichtbar, waehrend der Inhalt
darunter geblurrt war — der wechselnde Titel war also klar lesbar.
Ursache:
- Blur lag auf .tab-panels und parallel auf .tab-panel — zwei verschachtelte
Composite-Layer, die bei jedem Reflow neu berechnet werden.
- transition: filter 0.4s ease auf .tab-panel — bei Resize lief die Transition
oft rueckwaerts oder pausierte, was den Blur visuell verschwinden liess.
- .incident-header-strip lag ausserhalb von .tab-panels und war dadurch nie
geblurrt (Titel/Aktionen/Beschreibung blieben offen sichtbar).
Aenderungen:
- Blur-Anker hochgezogen auf #incident-view (Klasse refresh-blurred), so dass
Header und Tab-Panels gemeinsam unscharf werden.
- Nur noch eine Filter-Ebene (filter: blur(8px)).
- Transition entfernt — Blur soll schlagartig kommen und gehen, kein
lesbarer Zwischenzustand beim Reflow.
- will-change: filter; transform: translateZ(0); — erzwingt einen persistenten
GPU-Composite-Layer, der bei Window-Resize stabil bleibt.
Headless-Tests bestaetigen: alte Variante 89.8% Pixel-Stabilitaet ueber 6
Resize-Zyklen mit Content-Mutation, neue Variante 97.0% (Rest = Content-Diff).
Vorheriger Fix in selectIncident griff nicht beim handleRefresh-Pfad
(manueller Aktualisieren-Klick), weil dieser direkt UI.showProgress aufruft
ohne selectIncident zu durchlaufen. Damit blieb eine Lage, deren erster
Refresh per Klick angestossen wurde, unblurred.
rAF mit add("blurred") jetzt direkt in _showPopupProgress (components.js),
sobald state.isFirst gesetzt ist. Damit greift der Blur in jedem Pfad, der
durch _showPopupProgress laeuft — selectIncident, handleRefresh,
handleStatusUpdate (WebSocket), Initial-Restore.
Der zentrale rAF in selectIncident ist redundant und wieder entfernt.
Der _willReBlur-Skip von remove("blurred") in selectIncident bleibt
erhalten — verhindert ueberfluessiges remove+add im selben Tick.
cache-bust components.js auf v=20260427a, app.js auf v=20260427c.
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>
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).
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.
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.
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).
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.
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.
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.
- 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
Research-Lagen: ZUSAMMENFASSUNG-Sektion wird aus dem Bericht
extrahiert und in eigener Kachel oberhalb des Recherchberichts
angezeigt. Der Recherchebericht zeigt den Rest ohne Dopplung.
- Neue Kachel mit gs-id="zusammenfassung" im GridStack
- Toggle-Button in der Layout-Leiste
- extractZusammenfassung() und renderZusammenfassung() in UI
- Adhoc/Live-Lagen: Kachel wird automatisch ausgeblendet
- Export nutzt weiterhin _extract_zusammenfassung() aus dem Backend
Wenn ein Fall aus der Queue entfernt wird (Cancel, Fehler, Abschluss),
bleiben die #-Nummern der verbleibenden Eintraege jetzt nicht mehr
stecken. _reindexQueuePositions() sortiert nach alter Position und
nummeriert sequentiell neu (#1, #2, ...).
Aufgerufen in: handleRefreshCancelled, handleRefreshError,
handleRefreshComplete.
Analyzer-Prompts erlauben jetzt Tabellen wenn Daten sich strukturiert
vergleichen lassen (Produkte, Modelle, Kennzahlen etc.).
Frontend parst Markdown-Tabellensyntax und rendert sie als HTML-Tabellen
mit passendem Styling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Position aus _progressState lesen als Fallback wenn extra.queue_position
nicht verfuegbar ist (z.B. bei Lagenwechsel-Restore).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Progress-State wird jetzt fuer alle refreshenden Lagen angelegt,
nicht nur fuer die aktuell ausgewaehlte. Sidebar-Update passiert
vor dem Early-Return fuer nicht-aktuelle Lagen.
Bei WebSocket-Reconnect (auch nach Ctrl+Shift+R) wird der State
fuer bereits laufende Refreshes korrekt wiederhergestellt.
Sidebar-Cleanup bei Refresh-Abschluss fuer alle Lagen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
renderIncidentItem() baut den Refresh-Status (Gold-Rand, Spinner,
Statustext, Warteschlange-Position) direkt ins HTML ein. Ueberlebt
jetzt renderSidebar()-Aufrufe bei Lagenwechsel und Aktualisierungen.
Sidebar wird nach jedem WebSocket-Status-Update neu gerendert,
damit der Status fuer ALLE Lagen immer sichtbar bleibt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Klick irgendwo ausserhalb des Popups minimiert es zur Mini-Bar.
Beim ersten Durchlauf (blocking) bleibt das Popup offen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-Minimize entfernt. Popup erscheint sowohl bei erstem Durchlauf
als auch bei Aktualisierung sofort. Kann dann manuell minimiert werden.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Aktualisierungen starten minimiert (Mini-Bar), Popup nur per Klick.
Verhindert Ueberlagerung von Bearbeiten/Export-Buttons.
2. Erster Durchlauf: Bearbeiten/Export/Archivieren/Loeschen gesperrt,
nur Abbrechen moeglich.
3. Sidebar: Warteschlange-Lagen zeigen Position (#1, #2...) mit
eigenem visuellen Stil (gedimmt, pulsierender Dot).
4. Sidebar-Status (Recherchiert/Analysiert/Faktencheck) wird fuer
ALLE laufenden Lagen angezeigt, nicht nur die aktuelle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ladebalken ersetzt durch zentriertes Popup-Fenster mit Checkbox-Checkliste
(Warteschlange, Recherche, Analyse, Faktencheck) und Echtzeit-Timer.
Erster Durchlauf: Popup nicht wegklickbar, Blur-Effekt auf Kacheln.
Aktualisierung: Popup minimierbar zu kompakter Status-Leiste.
Timer laeuft pro Lage im Hintergrund weiter bei Lagenwechsel.
Gesamtzeit wird am Ende im Abschluss-Popup angezeigt.
Sidebar: Animierter Gold-Rand und Fortschrittstext (Recherchiert/
Analysiert/Faktencheck) unter dem Lage-Namen bei laufendem Refresh.
Zusaetzlicher Cancel-Checkpoint im Orchestrator nach Uebersetzung.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Research-Lagen (Tiefenrecherche) führen jetzt bei einem Klick auf
Aktualisieren automatisch 3 aufeinanderfolgende Refresh-Zyklen durch:
1. Breite Erfassung (initiale 4-Phasen-Recherche)
2. Vertiefung (andere Quellen, inkrementelle Analyse)
3. Konsolidierung (letzte Lücken, Fakten-Upgrade auf established)
Die Progress-Bar zeigt den aktuellen Durchlauf an (Durchlauf 1/3 etc.).
Cancel funktioniert zwischen und innerhalb der Durchläufe.
Adhoc-Lagen (Live-Monitoring) sind nicht betroffen.
Wird als eigenstaendige Anwendung auf separater Subdomain neu aufgebaut.
Alle GEOINT-Dateien entfernt, dashboard.html/components.js/main.py
auf pre-GEOINT Stand zurueckgesetzt.
The LLM occasionally generates source references with letter suffixes
(e.g. [1383a], [1396b]) despite being instructed not to. This caused
broken links because the sources array only contained integer nr values.
Backend: Add _sanitize_sources() to strip letter suffixes after parsing
and deduplicate, preferring entries with valid URLs.
Frontend: Add fallback in citation renderer - when a suffix reference
like [1383a] has no matching source with URL, fall back to the base
number [1383].
Also cleaned up 99 broken suffix entries and 44 suffix references in
the Irankonflikt incident (ID 6) database records.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Probleme:
- Frontend-Regex matchte nur reine Zahlen, nicht [389a]-Style Refs
- 17 alphanumerische Quellen im Irankonflikt blieben unverlinkt
- Orchestrator-Validierung erkannte diese Refs nicht als fehlend
Fixes:
- Frontend: Regex erweitert auf [\d+a-z?], Vergleich mit String und Number
- Orchestrator: Validierung erkennt jetzt auch alphanumerische Refs
- Analyzer-Prompts: Explizite Anweisung, nur ganze Zahlen als Nr zu verwenden
- 822a und 859a in Irankonflikt sources_json nachgetragen
- Cache-Buster aktualisiert
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ursache: Claude liefert teilweise Quellennummern als String statt Integer.
Der Frontend-Vergleich (===) schlug dann fehl: "574" !== 574.
Fixes:
- 95 String-Nummern in Irankonflikt sources_json zu Integer konvertiert
- 5 Duplikate entfernt
- Frontend: Number() statt parseInt/=== fuer robusten Vergleich
- Orchestrator: Automatische Konvertierung von String-Nr zu Integer vor DB-Speicherung
- Cache-Buster aktualisiert
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DB-Migration: language und bias Spalten zur sources-Tabelle hinzugefuegt
- Alle 254 Quellen mit Metadaten befuellt (Sprache, politische Ausrichtung)
- SourceResponse-Modell um language/bias Felder erweitert
- Info-Icon (i) mit Hover-Tooltip nach source-group-name eingefuegt
- Tooltip zeigt Typ, Sprache und Ausrichtung der jeweiligen Quelle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Telegram-Quellen haben kein domain-Feld, daher wurde der interne
Gruppierungsschluessel _single_<id> als Anzeigename verwendet.
Jetzt wird bei _single_-Prefix auf den tatsaechlichen Kanalnamen zurueckgegriffen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 feste Farbstufen (primary/secondary/tertiary/mentioned) mit
variablen Labels pro Lage, die von Haiku generiert werden.
- DB: category_labels Spalte in incidents, alte Kategorien migriert
(target->primary, response/retaliation->secondary, actor->tertiary)
- Geoparsing: generate_category_labels() + neuer Prompt mit neuen Keys
- QC: Kategorieprüfung auf neue Keys umgestellt
- Orchestrator: Tuple-Rückgabe + Labels in DB speichern
- API: category_labels im Locations- und Lagebild-Response
- Frontend: Dynamische Legende aus API-Labels mit Fallback-Defaults
- Migrationsskript für bestehende Lagen
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CSS: min-height:0 und height:100% für Flex-Container
- JS: Höhe wird immer explizit aus der Gridstack-Kachel berechnet,
nicht nur als Fallback unter 50px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phasen haben nur noch Mindest-Floors (queued 2%, recherche 12%,
analyse 35%, faktencheck 65%), keinen Deckel mehr. Der Balken
kriecht global asymptotisch Richtung 95% (0.5% des Restwegs pro
500ms), sodass er nie stehenbleibt. Phasenwechsel geben sichtbare
Boosts nach oben, complete snappt auf 100%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Balken kriecht jetzt asymptotisch innerhalb jeder Phase
(queued 2-10%, recherche 12-30%, analyse 33-60%, faktencheck 65-92%)
statt bei Phasenwechsel hart zu springen. 3% des Restwegs pro
500ms-Tick sorgt für gleichmäßige Bewegung unabhängig von der
Phasendauer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Neue Tabelle user_excluded_domains für benutzerspezifische Ausschlüsse
- Domain-Ausschlüsse wirken nur für den jeweiligen User, nicht org-weit
- user_id wird durch die gesamte Pipeline geschleust (Orchestrator → Researcher → RSS-Parser)
- Grundquellen (is_global) können nicht mehr bearbeitet/gelöscht werden im Frontend
- Grundquelle-Badge bei globalen Quellen statt Edit/Delete-Buttons
- Filter Von mir ausgeschlossen im Quellen-Modal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- geoparsing.py: Komplett-Rewrite (spaCy NER + Nominatim -> Haiku + geonamescache)
- orchestrator.py: incident_context an geoparse_articles, category in INSERT
- incidents.py: incident_context aus DB laden und an Geoparsing uebergeben
- public_api.py: Locations aggregiert im Lagebild-Endpoint
- components.js: response-Kategorie neben retaliation (beide akzeptiert)
- requirements.txt: spaCy und geopy entfernt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
parseUTC() hängte blind ein 'Z' (UTC) an alle Timestamps, aber
die DB speichert Lokalzeit (Europe/Berlin). Bei CET (UTC+1) wurden
alle Zeiten 1 Stunde zu spät angezeigt.
Neue Logik: Timestamps mit 'Z' oder '+' werden direkt geparst.
Timestamps ohne Zeitzonen-Info werden als Europe/Berlin interpretiert
mit korrektem Offset (auch bei Sommer-/Winterzeit-Wechsel).
Betrifft: Refresh-Verlauf, Artikel-Zeitstempel, Snapshots, Lagebild-Stand
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ursache: Server sendete started_at als Lokalzeit (Europe/Berlin),
aber der Client interpretierte es als UTC via parseUTC().
Bei UTC+1 lag die Startzeit dadurch 1 Stunde in der Zukunft.
- orchestrator.py: started_at in WebSocket-Nachrichten als echtes UTC
(ISO 8601 mit Z-Suffix) senden, DB-Timestamps bleiben Lokalzeit
- components.js: elapsed auf min. 0 clampen als Sicherheitsnetz
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Frontend-Dateien auf Zustand vor i18n zurückgesetzt.
lang.js entfernt, CSP bereinigt. Backend-Umlaut-Fix bleibt erhalten.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>