# AegisSight-Monitor

> OSINT-Lagemonitoring mit KI-gestützter Nachrichtenanalyse

## Übersicht

```yaml
projekt: AegisSight-Monitor
url: https://monitor.aegis-sight.de
server: ssh monitor (46.225.141.13, User: claude-dev)
pfad: /home/claude-dev/AegisSight-Monitor
quellcode: /home/claude-dev/AegisSight-Monitor/src/
datenbank: /home/claude-dev/osint-data/osint.db (SQLite WAL, geteilt mit Verwaltungsportal + Globe; data/ im Projekt ist ein Symlink darauf)
gitea: https://gitea-undso.aegis-sight.de/AegisSight/AegisSight-Monitor
service: aegis-monitor.service (systemd, Port 8891, Nginx Reverse Proxy, EIN uvicorn-Prozess - Orchestrator und WebSockets halten Zustand im Speicher, niemals --workers setzen)
venv: /home/claude-dev/.venvs/osint/ (Python 3.12)
```

## Technologie-Stack

```yaml
backend:
  framework: FastAPI + Uvicorn
  datenbank: SQLite WAL (aiosqlite, async)
  auth: Magic-Link-Login per E-Mail (JWT HS256, 24h)
  scheduler: APScheduler (PDF-Ingest 1min, Auto-Refresh 1min, Cleanup 1h, Health-Check taeglich 04:00, Telegram-Status taeglich 04:30)
  websocket: FastAPI native (Echtzeit-Updates an Clients)
  ki: Claude CLI als Subprocess (WebSearch + WebFetch Tools)
  ki_backend_eu: |
    EU-Umbau Phase 1 (seit 2026-07-29): zweiter Modellweg ueber AWS Bedrock
    (EU-Inferenzprofile Frankfurt, agents/bedrock_client.py). Umschaltbar je
    Lage (incidents.ai_backend) > Organisation (org_setting 'ai_backend') >
    global (ENV AI_BACKEND, Default 'cli'). Aufloesung je Refresh im
    Orchestrator via ContextVar _ai_backend_var (gemeinsame Aufloesung mit der
    Lane-Wahl, siehe _resolve_ai_backend). Seit Phase 2 (2026-07-30) laeuft im
    EU-Modus auch die RECHERCHE europaeisch: agents/eu_researcher.py steuert
    eine Schleife aus Bedrock-Planung und staan-Suchen (services/staan_client.py)
    und liefert dasselbe JSON wie die CLI-WebSearch. Werkzeuglose Aufrufe gehen
    direkt ueber bedrock_client. Seit Phase 3 (2026-07-30) laufen auch
    Faktencheck und Lagebild im EU-Modus komplett ueber Bedrock: der
    Faktenchecker bekommt vorab eine staan-Stuetzsuche (eu_call_with_search in
    eu_researcher.py, Haiku plant Verifikations-Queries, Treffer als
    Kontextblock im Prompt), das Lagebild arbeitet rein auf dem
    Meldungsbestand. Im EU-Modus gibt es damit KEINE CLI-Aufrufe mehr.
    Doppelspur: eine Orchestrator-Lane je
    (Organisation, Backend) — CLI- und EU-Lauf derselben Org laufen gleichzeitig,
    gleiche Backends seriell. Kosten aus Token x BEDROCK_PRICING plus
    STAAN_COST_PER_QUERY_USD, Credits-System unveraendert. AWS-Zugang +
    STAAN_API_KEY liegen in der Staging-.env. boto3 muss im venv installiert
    sein (beim Promote nach Live auch dort nachziehen!).
  ki_modelle:
    schnell: CLAUDE_MODEL_FAST (Haiku) — Feed-Selektion, Topic-Filter, Geoparsing, Uebersetzung, Chat, QC
    mittel: CLAUDE_MODEL_MEDIUM (Sonnet) — nur Netzwerkanalyse (entity_extractor hat aktuell keinen Aufrufer in der App)
    standard: CLAUDE_MODEL_STANDARD (Opus) — Recherche, Lagebild, Faktencheck
  email: aiosmtplib (smtp.ionos.de:587 TLS)

frontend:
  typ: Vanilla JS (kein Framework, kein Build-Step)
  design: AegisSight Dark/Light Theme (Navy/Gold)
  fonts: Poppins (Titel), Inter (Body)
  layout: Reiter-Ansicht je Lage (layout.js; gridstack wurde abgeloest, nur Legacy-Stubs uebrig)
  karte: Leaflet + MarkerCluster (lokal in vendor/, Kacheln von tile.openstreetmap.de)
  echtzeit: WebSocket mit Auto-Reconnect und Ping/Pong
```

## Projektstruktur

```yaml
src/:
  main.py: "FastAPI App, WebSocketManager, Scheduler, Lifespan, statische Routen"
  config.py: "Konfiguration (JWT, Claude-Modelle, SMTP, RSS-Feeds, Zeitzone)"
  auth.py: "JWT erstellen/verifizieren, Magic-Link/Code, get_current_user Dependency"
  database.py: "SQLite Schema (25+ Tabellen), Migrationen, init_db(), get_db()"
  models.py: "Pydantic Request/Response-Schemas"
  source_rules.py: "Domain-Kategorisierung, RSS-Feed-Discovery, Claude-Feed-Bewertung"
  report_generator.py: "PDF (WeasyPrint) + DOCX (python-docx) Export"

  routers/:
    auth.py: "Magic-Link-Login, Token-Verify, /api/auth/me"
    incidents.py: "CRUD Lagen, Refresh, Artikel, Snapshots, Faktenchecks, Export, E-Mail-Abos, Refresh-Log, Beschreibung generieren (Prompt Enhancement)"
    sources.py: "CRUD Quellen, Discovery (Single/Multi), Domain sperren, Telegram-Validierung"
    chat.py: "KI-Assistent (Haiku), Injection-Schutz, Tech-Leak-Filter"
    public_api.py: "API-Key Auth, Globe-Feed (GeoJSON), Globe-Ingest, Snapshot-Abruf"
    notifications.py: "CRUD Benachrichtigungen, Unread-Count, Mark-Read"
    feedback.py: "E-Mail-Feedback mit Bild-Anhaengen"
    tutorial.py: "Tutorial-Fortschritt pro User"

  routes/:
    version_router.py: "GET /api/version + /api/release-notes (OHNE Auth, fuer das Update-Fenster im Frontend). Historischer Zweitordner neben routers/"

  agents/:
    orchestrator.py: "Queue-basierte Refresh-Steuerung (eine Lane je Organisation), Research Multi-Pass (3 Durchlaeufe), Retry, Cancel, Credits-Buchung. _run_refresh ist ~1450 Zeilen, Aenderungen dort nur mit Vorsicht"
    stage_runners.py: "Studio-Bausteine: analyze/factcheck einzeln auf vorhandenem Datenbestand, bewusst NEBEN _run_refresh gebaut"
    researcher.py: "WebSearch-Recherche (Standard + 4-Phasen-Tiefenrecherche), Feed-/Kanal-/Account-Selektion, Keyword-Extraktion, Google-News-Suchfeeds je Sprache"
    analyzer.py: "Analyse-Agent (Lagebild/Briefing, Erst- + inkrementell, Inline-Zitate, Topic-Filter, Neueste Entwicklungen, Stimmungsbild)"
    factchecker.py: "Faktencheck (Erst/Inkrementell/Zwei-Phasen mit Haiku-Triage ab 25 Fakten), Claim-Matching, Dedup, Schutz gegen Massen-Downgrades. Belegtiefe: zaehle_medienhaeuser zaehlt unabhaengige Verlage statt Fundstellen, pruefe_belegtiefe stuft Fakten ab, deren Status mehr Beleg behauptet als vorliegt (confirmed 2, established 3, Primaerbeleg 1), bewertungsregeln() haengt den verbindlichen Regelblock an jeden Auftrag"
    geoparsing.py: "Haiku-basierte Ortsextraktion, Geocoding offline via geonamescache"
    entity_extractor.py: "Netzwerkanalyse (Sonnet). ACHTUNG: aktuell ohne Aufrufer in der App, nur regenerate_relations.py im Repo-Root nutzt es"
    translator.py: "Haiku-Uebersetzung in Batches a 5 (groessere Batches rissen den JSON-Output ab)"
    claude_client.py: "Shared Claude CLI Client, Usage-Tracking (Token, Kosten), Rate-Limit-Erkennung, Cancel via ContextVar. Routet je nach _ai_backend_var werkzeuglose Aufrufe zu bedrock_client"
    bedrock_client.py: "EU-Modellweg ueber AWS Bedrock Converse (nur eu.-Inferenzprofile, Token-zu-USD-Umrechnung ueber BEDROCK_PRICING, gleiche Fehlerkategorien wie das CLI). EU-Umbau Phase 1. Wiederholt Kapazitaets- (503) und Kontingentfehler (429) nach BEDROCK_RETRY_WAITS (5/15/30s), begrenzt durch das Zeitbudget des Aufrufs und BEDROCK_RETRY_BUDGET_S je Refresh; Stoerungen landen im Refresh-Protokoll (refresh_log.error_message) und waehrend der Wartezeit als status_update in der Oberflaeche"
    eu_researcher.py: "EU-Recherche-Schleife (Phase 2): Bedrock-Modell schlaegt Suchanfragen vor, Code fragt staan, Modell entscheidet weiter/fertig. Trefferaufnahme ueber ein Kontingent je Runde (EU_RESEARCH_MAX_NEW_PER_ROUND) statt eines harten Gesamtstopps, spaetere Runden verdraengen bei vollem Korb die schwaechsten Treffer frueherer Runden; Abbruch nur bei echter Saettigung. Bildet die 4-Phasen-Tiefenrecherche nach, liefert denselben JSON-Array-Text wie die CLI-WebSearch (Einstieg in researcher.search), akzeptiert NUR URLs aus echten staan-Treffern"

  feeds/:
    rss_parser.py: "RSS-Feed-Parsing (feedparser + httpx), adaptive Keyword-Schwelle, Frische-Bonus, Domain-Cap"
    telegram_parser.py: "Telethon-basierter Telegram-Parser (eine gemeinsame Session), Kanal-Validierung"
    x_parser.py: "X/Twitter via twscrape (Account-Store ~/.x-scraper/accounts.db, optional Mobilfunk-Proxy)"
    podcast_parser.py: "Podcast-Feeds inkl. Transkript-Extraktoren (transcript_extractors/)"

  services/:
    pipeline_tracker.py: "Die 11 Pipeline-Schritte (DE/EN, Laien-Tooltips), schreibt refresh_pipeline_steps + WebSocket-Events"
    post_refresh_qc.py: "Post-Refresh Quality Check: Faktencheck-Duplikate, Location-Korrektur, Umlaut-Normalisierung"
    fact_consolidation.py: "Periodisches Haiku-Clustering. ACHTUNG: als 6h-Job dokumentiert, aber NICHT im Scheduler eingeplant"
    source_health.py: "Quellen-Health-Checks (Erreichbarkeit, Feed-Validitaet, Stale, fetch_strategy-Logik)"
    source_suggester.py: "KI-Quellen-Vorschlaege via Haiku + Karteileichen-Heuristik"
    pdf_ingest.py: "Minutenjob: hochgeladene PDFs einlesen (pdfplumber + OCR-Fallback), uebersetzen, als Pool-Artikel ablegen"
    org_settings.py: "Key-Value-Einstellungen je Organisation (output_language etc.) mit 60s-Cache"
    media_registry.py: "Kanonische Medien-Identitaet je Domain (registrable_domain, namen_je_domain, Aggregator-Liste, Sprache aus der Adresse). Sorgt dafuer, dass ein Verlag im Bericht unter einem Namen gefuehrt und einmal gezaehlt wird"
    staan_client.py: "staan.ai Such-Client (EU-Umbau Phase 2): Suche + Volltexte (full_content=markdown) ueber den europaeischen Index, Pflicht-Domain-Ausschlussliste je Anfrage, max 10 Ausschluss-Domains"
    license_service.py: "Lizenz-Pruefung, Credits-Buchung (charge_usage_to_tenant), Periodenwechsel, Budget-Warnung. expire_licenses() existiert, hat aber KEINEN Scheduler-Job"

  middleware/:
    license_check.py: "Dependencies: require_active_license, require_writable_license"

  email_utils/:
    sender.py: "Async SMTP Versand"
    templates.py: "HTML-Templates (Magic-Link, Benachrichtigungen)"
    rate_limiter.py: "Rate-Limiting Magic-Links"

  migration/:
    migrate_to_multitenancy.py: "Einmal-Migration Single->Multi-Tenant"

  report_templates/:
    report.html: "HTML-Template fuer PDF/DOCX-Export"

  static/:
    index.html: "Login-Seite (Magic-Link), JS inline"
    dashboard.html: "Hauptdashboard (Sidebar + Reiter-Ansicht + 8 Modals + Chat-Widget)"
    studio.html: "Studio (3-Spalten-Werkstatt, online nur fuer info@ sichtbar - Gating rein clientseitig). Reiter Einstellungen ersetzt das Bearbeiten-Modal der klassischen Ansicht und enthaelt die Wahl des KI-Wegs je Fall. Spalte 3 hat genau EINEN Startpunkt (#ls-btn), darunter stehen die vier Schritte als reine Anzeige. Einzelne Bausteine lassen sich aus der Oberflaeche nicht mehr starten, der Server kann es weiterhin."
    css/:
      style.css: "AegisSight Design System (Dark/Light Theme, alle Komponenten)"
      studio.css: "Studio-spezifische Styles"
    js/:
      api.js: "REST-API-Client (fetch, Auth-Header, 30s Timeout, 403 -> Nur-Lese-Modus)"
      app.js: "Hauptlogik: ThemeManager, NotificationCenter, App-Objekt (181 KB)"
      components.js: "UI-Rendering: Sidebar, Faktenchecks, Toasts, Progress, Karte (geteilt mit Studio)"
      studio.js: "Kompletter Studio-Controller, enthaelt den Einstellungen-Reiter je Fall und die Lauf-Anzeige (_renderLauf, _schrittZustaende, laufKnopf)"
      chat.js: "Chat-Assistent Widget (Bedienfragen)"
      pipeline.js: "Grafische Analysepipeline (11 Schritte, Live-Animation)"
      layout.js: "Reiter-Umschalter je Lage (merkt letzten Reiter; enthaelt noch gridstack-Legacy-Stubs)"
      tutorial.js: "32-Schritte-Rundgang (140 KB). AKTUELL DEAKTIVIERT, alle Einstiege auskommentiert"
      a11y.js: "Barrierefreiheits-Panel (identische Datei wie im Verwaltungsportal)"
      update-system.js: "Was-ist-neu-Modal aus RELEASES.json + Update-Banner"
      ai-disclaimer.js: "KI-Haftungshinweis beim ersten Besuch"
      i18n.js: "Mini-Uebersetzung de/en (nur teilweise verdrahtet, Studio/Login komplett deutsch)"
      ws.js: "WebSocket-Client (Reconnect, Ping/Pong)"
    vendor/:
      leaflet.js: "Karten-Bibliothek"
      leaflet.markercluster.js: "Marker-Clustering"

tests/:
  hinweis: "Laufen ohne Netzzugriff und ohne Kosten, Modell, Suche und Datenbank sind durch Testdoubles ersetzt. Aufruf aus dem Projektstamm, zum Beispiel venv/bin/python tests/test_eu_factcheck.py"
  test_eu_factcheck.py: "EU-Faktencheck, Nachhak-Runde, Quellenzuordnung, plus Regressionsschutz fuer den Anthropic-Weg"
  test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten"
  test_faktenspanne.py: "Richtwert fuer die Zahl der Faktenaussagen, waechst mit der Menge der Meldungen"
  test_qc_und_runden.py: "Absicherung der Duplikatpruefung und Abbruch der Suchrunden bei erreichter Treffergrenze"
  test_mehrfachantwort.py: "Antworten, in denen sich das Modell selbst korrigiert und mehrere Fassungen liefert, die ausfuehrlichste muss gewinnen"
  test_faktencheck_belegtiefe.py: "Belegzaehlung nach Medienhaus, Nachbedingung fuer bestaetigte Fakten, Sperre gegen das Hochstufen von developing, getrennte Bezeichnungen fuer Widerspruch und Widerlegung"
  test_rss_treffer.py: "Keyword-Matching der RSS-Auswertung: Mehrwort-Begriffe treffen wortweise, Auftrag verlangt Einzelbegriffe und Eigennamen"
  test_bedrock_wiederholung.py: "Wiederholung des EU-Wegs bei Kapazitaets- und Kontingentfehlern, Staffelung, Zeit- und Wartebudget, Sichtbarkeit im Refresh-Protokoll"
  test_sammelaktionen.js: "Sammelaktionen der Seitenleiste im klassischen Dashboard, Auswahlmodus, Filterbeachtung, Loeschbestaetigung (Aufruf mit node)"
  test_falleinstellungen.js: "Einstellungen-Reiter im Studio, Formular fuellen, nur Geaendertes speichern, KI-Weg umstellen samt Warnhinweis, Abo nachladen (Aufruf mit node)"
  test_studio_struktur.py: "Strukturpruefung beider Oberflaechen ohne Browser, eindeutige IDs, jedes onclick zeigt auf eine vorhandene Methode, Cache-Buster, Design-Tokens"
  test_lauf.js: "Lauf-Anzeige der Studio-Spalte, Zustand des Startknopfs, Kette, Schrittzustaende aus /freshness, Nachweis dass kein Schritt etwas startet (Aufruf mit node)"
  test_quellenausgabe.py: "Ausgabetreue des Quellenverzeichnisses: Nummern aus dem Lagebild statt Zeilennummern, keine Kappung, ein Verlag ein Name, Statistik deckungsgleich mit dem Verzeichnis"
```

## Architektur

```yaml
incident_typen:
  adhoc:
    label: "Live-Monitoring"
    quellen: "RSS + WebSearch + optional Telegram"
    analyse: "Fliesstext-Lagebild"
    faktencheck_status: "confirmed/unconfirmed/contradicted/false/developing (contradicted = Belege widersprechen einander, false = nachweislich widerlegt)"
    refresh: "Manuell oder automatisch (Intervall konfigurierbar)"
  research:
    label: "Recherche"
    quellen: "Nur WebSearch 4-Phasen-Tiefenrecherche (kein RSS)"
    analyse: "Strukturiertes Briefing (Ueberblick, Hintergrund, Akteure, Lage, Einschaetzung, Quellenqualitaet)"
    faktencheck_status: "established/unverified/disputed/false/developing"
    refresh: "Immer manuell, erster Refresh automatisch 3 Durchlaeufe (Multi-Pass)"
    multi_pass:
      durchlaeufe: 3
      labels: ["Breite Erfassung", "Vertiefung", "Konsolidierung"]
      bedingung: "Nur beim ersten Refresh (kein Summary vorhanden)"
      cancel: "Zwischen und innerhalb der Durchlaeufe moeglich"

refresh_pipeline:
  hinweis: "Die 11 nutzersichtbaren Schritte definiert services/pipeline_tracker.py. Interner Ablauf in _run_refresh:"
  1: "Feed-/Kanal-/Account-Selektion (Haiku) + dynamische Keywords je Sprache"
  2: "Parallel sammeln: RSS + Google-News-Suchfeeds + WebSearch (Opus) + optional Telegram + X + Podcasts (nur adhoc)"
  3: "URL-Verifizierung (HEAD-Requests, tote URLs werden zu site:-Suchlinks repariert)"
  4: "Duplikaterkennung (URL + Headline, dann gegen DB-Bestand)"
  5: "Relevanz-Scoring + semantischer Topic-Filter (Haiku)"
  6: "Geoparsing (Haiku + geonamescache offline)"
  7: "Faktencheck ZUERST (liefert Faktenkontext), DANN Lagebild/Briefing (Opus). Optional Stimmungsbild aus Foren-Quellen"
  8: "Uebersetzung fehlender DE-Texte (Haiku, nur wenn TRANSLATOR_ENABLED) + Neueste Entwicklungen (nur adhoc)"
  9: "Post-Refresh QC (Fakten-Dubletten, Karten-Kategorien, Umlaute)"
  10: "Notifications (DB + E-Mail + WebSocket), Credits-Buchung (flat: adhoc 45 / research 40 je Durchlauf)"
  11: "Background: Source-Discovery + Executive Summary"

multi_tenancy: |
  Mandantentrennung ueber tenant_id-Filter je Abfrage (keine Middleware). Bei Lagen zentral
  ueber _check_incident_access. ACHTUNG, nicht alle Tabellen haben tenant_id
  (source_health_checks, source_suggestions, incident_subscriptions, billing_tariff),
  und der WebSocket-Broadcast filtert Stand 07/2026 NICHT nach Mandant.

lage_reiter:
  - "Neueste Entwicklungen (research: Zusammenfassung)"
  - "Lagebild (research: Recherchebericht, Markdown + Inline-Zitate)"
  - "Ereignis-Timeline (horizontale Achse, Bucketing, Filter)"
  - "Geografische Verteilung (Leaflet, Kategorie-Marker, Legende)"
  - "Faktencheck (Status-Icons, Evidence, Filter)"
  - "Oeffentliche Stimmung (nur bei Foren-Quellen)"
  - "Analysepipeline (grafisch, 11 Schritte)"
  - "Quellenuebersicht (nach Domain gruppiert)"
```

## Datenbank (30+ Tabellen)

```yaml
kern: "organizations, licenses, users, magic_links, organization_settings, portal_admins (gehoert dem Portal)"
lagen: "incidents, articles, incident_snapshots, fact_checks, incident_events, fact_check_runs"
pipeline: "refresh_log, refresh_pipeline_steps"
quellen: "sources, source_alignments, source_health_checks, source_suggestions, user_excluded_domains, podcast_transcripts"
geo: "article_locations"
netzwerk: "network_analyses, network_analysis_incidents, network_entities, network_entity_mentions, network_relations, network_generation_log (alle leer, kein Endpoint im Monitor)"
abrechnung: "token_usage_monthly, billing_tariff, user_activity_days"
system: "notifications, incident_subscriptions, system_status"
hinweise:
  - "Es gibt KEINE feedback-Tabelle, Feedback geht direkt per E-Mail raus"
  - "Portal-Tabellen (portal_audit_log, portal_magic_links, source_health_history u.a.) liegen in derselben Live-DB, werden aber nur vom Verwaltungsportal angelegt/genutzt"
  - "users.is_global_admin/globe_access/network_access legt NUR das Portal an. Eine frisch per init_db erzeugte Monitor-DB kann daher keinen Login (no such column)"
```

## Verwandte Projekte (gleicher Server)

```yaml
verwaltungsportal:
  pfad: /home/claude-dev/AegisSight-Monitor-Verwaltung
  url: https://monitor-verwaltung.aegis-sight.de
  service: verwaltungsportal.service (Port 8892)
  geteilte_db: ja

globe:
  pfad: /home/claude-dev/AegisSight-Globe
  url: https://globe.aegis-sight.de
  service: globe.service (Port 8890)
  geteilte_db: ja

netzwerkanalyse:
  pfad: /home/claude-dev/AegisSight-Netzwerkanalyse
  url: https://netzwerkanalyse.aegis-sight.de
  service: netzwerkanalyse.service (Port 8893)
```

## Regeln

```yaml
regeln:
  - "Jede Aenderung MUSS sofort committed und nach Gitea gepusht werden"
  - "Echte Umlaute in UI-Texten (ue, ae, oe, ss), keine Umschreibungen"
  - "Keine Passwoerter oder Secrets in den Code committen"
  - "Service nach Backend-Aenderungen: sudo systemctl restart aegis-monitor (Staging startet der Auto-Deploy selbst neu)"
  - "Frontend-Aenderungen (HTML/JS/CSS) brauchen keinen Neustart"
  - "Backup-Dateien (.bak) nicht committen, vor Push loeschen"
```

## UI-Sync mit dem Lokal-Fork (verbindlich)

> Seit 2026-07-25. Die Oberflächen von Online-Monitor und Lokal-Fork bleiben angeglichen.

```yaml
ui_sync:
  regel: "Jede Änderung unter src/static wird noch in derselben Sitzung ins jeweils andere Repo portiert."
  lokal_repo: "AegisSight/AegisSight-Monitor-Local (Branch main, läuft auf dem Windows-Rechner des Nutzers)"
  portieren:
    lokal_nach_online: |
      In einem temporären Klon dieses Repos (Branch develop) den Lokal-Fork als Remote
      hinzufügen, fetchen, git cherry-pick <sha> (gemeinsame Historie, Drei-Wege-Merge
      funktioniert), dann push origin develop. NIE auf main pushen, Live nur per Promote-UI.
    online_nach_lokal: "Im Lokal-Fork: git fetch online && git cherry-pick <sha> (Remote 'online' ist dort eingerichtet, fetch-only)."
  drift_check: "Im Lokal-Fork: bash scripts/ui-drift-check.sh (vergleicht src/static beider Repos, Zeilenenden ignoriert)"
  wortlaut: "Die Verbrauchseinheit heißt in BEIDEN Monitoren 'Credits' (Entscheidung Nutzer 2026-07-25). Nicht Guthaben, nicht Einheiten."
  gewollte_unterschiede:
    - "Online: Studio nur für info@aegis-sight.de freigegeben (Gating in studio.js init, Studio-Link im Dashboard-Header versteckt). Lokal ohne Gating, Knopf immer sichtbar."
    - "Lokal: X-Zugänge-Oberfläche (twscrape) in Sidebar/Modal/Quellenübersicht. Online bewusst nicht vorhanden (kein x-Router im Online-Backend)."
    - "Lokal: Auto-Login über /api/auth/dev-login (Demo-Modus). Online ausschließlich Magic-Link."
    - "Lokal: Kostenvorschau im Anlege-Dialog (updateIntervalCostHint, Fork-Commit 5b0b578). Online noch nicht portiert."
    - "Online: Umschalter 'KI-Verarbeitung' im Anlege-/Bearbeiten-Dialog (Auswahl inc-ai-backend: Standard/Anthropic/EU) plus EU-Kennzeichen in der Lagen-Kopfzeile (incident-eu-badge). Bewusst NICHT im Lokal-Fork, dort gibt es kein Bedrock-Backend und das Feld waere funktionslos (Stand 2026-07-30)."
  bekannter_drift_stand_2026_07_26:
    - "Die Takt-Untergrenze ist inzwischen AUCH online (dort _getMinIntervalMinutes, 30 Min; lokal _intervalMinMinutes mit Vorgabe 12 Std). Ältere Angaben, sie fehle online, sind überholt."
    - "Online-only, noch nicht in den Fork portiert: Reiter 'Öffentliche Stimmung' (renderPublicMood) und die Fall-Chat-Rückfrage /clarify."
    - "Echter Inhalts-Drift in rund 11 Dateien (app.js, components.js, api.js, studio.js, style.css u.a.), weitere ~7 Dateien unterscheiden sich nur durch Umlaute in Kommentaren."
  stolperfalle_zeilenenden: |
    Die Frontend-Dateien (src/static) sind CRLF, die Python-Dateien dieses Repos sind LF
    (im Lokal-Fork teils anders). Beim Portieren keine Werkzeuge einsetzen, die Zeilenenden
    pauschal umschreiben (z.B. sed -i unter Git Bash), und den Diff vor dem Commit auf
    Plausibilität prüfen. Ein Riesen-Diff ist fast immer ein Zeilenenden-Unfall.
```

## Staging-Umgebung

```yaml
staging:
  url: https://staging.monitor.aegis-sight.de
  server: 46.225.141.13 (gleicher Host wie Live)
  pfad: /home/claude-dev/AegisSight-Monitor-staging
  branch: develop
  port: 18891 (Live: 8891)
  service: aegis-monitor-staging.service (systemd)
  venv: /home/claude-dev/AegisSight-Monitor-staging/venv (eigenes venv)
  zugriff: Magic-Link-Login an info@aegis-sight.de (Cookie 30 Tage)

  datenbank:
    pfad: /home/claude-dev/osint-data-staging/osint.db (per DB_PATH in .env gesetzt)
    achtung: "~/AegisSight-Monitor-staging/data/osint.db ist eine UNBENUTZTE Altkopie"
    initial: einmalige Kopie der Live-DB
    drift: gewollt - Aenderungen in Staging beeinflussen Live nicht
    reseed_von_live: |
      sudo systemctl stop aegis-monitor-staging
      cp /home/claude-dev/osint-data/osint.db /home/claude-dev/osint-data-staging/osint.db
      sudo systemctl start aegis-monitor-staging

  besonderheiten_env:
    JWT_SECRET: eigener fuer Staging (nicht Live-JWT)
    MAGIC_LINK_BASE_URL: https://staging.monitor.aegis-sight.de (sonst leitet App zu Live)
    DB_PATH: /home/claude-dev/osint-data-staging/osint.db (Live setzt KEIN DB_PATH, dort greift der Default data/ = Symlink auf ~/osint-data)
    STAGING_MODE: 1 (unlimited_budget, kein Credits-Hard-Stop)
    TRANSLATOR_ENABLED: false
    TELEGRAM_SESSION_PATH: ~/.telegram-staging/ (eigene Session, dort ist NIEMAND eingeloggt - Telegram-Recherche auf Staging liefert nichts, die Status-Karte im Portal zeigt das ehrlich an)
    AI_BACKEND: "(optional) 'cli' oder 'bedrock', globaler Default fuer den EU-Modellweg. Ohne Eintrag gilt 'cli'"
    AWS_ACCESS_KEY_ID_usw: "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION=eu-central-1 und STAAN_API_KEY liegen seit 2026-07-29 in der Staging-.env (IAM-Benutzer nur fuer Bedrock, staan-Suchschluessel fuer Phase 2)"

  auth_service:
    pfad: /opt/aegis-staging-auth
    service: aegis-monitor-staging-auth.service
    port: 127.0.0.1:8095
    cookie_domain: staging.monitor.aegis-sight.de
    cookie_name: aegis_monitor_staging_auth
    code_quelle: identisch zum Service auf 46.225.225.49 (eigene Konfig)
```

### Workflow Staging -> Live

Der gueltige Ablauf steht unten unter "Vollstaendiger Workflow": develop pushen,
Auto-Deploy aktualisiert Staging von selbst, auf Staging pruefen, Promote ueber
https://deploy.aegis-sight.de, Live-Check. Manuelles git pull auf dem Server ist
NICHT noetig und sollte unterbleiben (der Deploy-Listener macht git reset --hard,
lokale Aenderungen in den Server-Verzeichnissen gehen dabei verloren).

Stolperfalle bei der Warteschlangen-Pruefung: das Staging-Verzeichnis fetcht per
Fetch-Regel NUR develop. Fuer "wie viele Commits warten auf Promote" immer erst
git fetch origin main ausfuehren, sonst zeigt main..develop veraltete Zahlen.

### Bekannte Baustellen (Stand 2026-07-26)

- WebSocket-Broadcast filtert nicht nach Mandant (main.py broadcast_for_incident bekommt tenant_id, nutzt ihn nicht)
- POST /api/public/globe-ingest schreibt ohne Eigentuemer-Pruefung in beliebige Lagen, Artikel ohne tenant_id
- fact_consolidation und expire_licenses existieren, haben aber keinen Scheduler-Job
- entity_extractor/Netzwerkanalyse ohne Aufrufer in der App (nur regenerate_relations.py)
- tutorial.js deaktiviert (Einstiege auskommentiert), laedt aber weiter bei jedem Login
- dashboard.html laedt nicht existierende cluster-data.js (404) und ungenutztes d3.js vom CDN
- Reiter "Oeffentliche Stimmung" fehlt in layout.js TAB_ORDER und ist dadurch nicht anklickbar
- X-Schalter im Anlege-Dialog hat online keine Funktion (kein X-Backend online)
- i18n unvollstaendig (Studio und Login nur deutsch), Begriffs-Mix Fall/Lage/Vorfall, zweimal "Token-Budget" statt "Credits" in app.js
- Unter 768px verschwindet die Sidebar ersatzlos (mobil unbenutzbar)
- Budget-Warnung geht nur in die Glocke, nicht per E-Mail (siehe docs/ABRECHNUNG.md)

## Auto-Deploy + Promote-UI

```yaml
auto_deploy:
  listener_service:
    pfad: /opt/aegis-staging-deploy
    service: aegis-staging-deploy.service
    port: 127.0.0.1:8096
    deployments:
      staging: develop -> ~/AegisSight-Monitor-staging (restartet aegis-monitor-staging)
      live:    main    -> ~/AegisSight-Monitor          (restartet aegis-monitor)
    endpoints:
      "POST /__deploy":      staging via Gitea-Webhook (HMAC)
      "POST /__deploy/live": live via Promote-UI (HMAC)
    secrets: /opt/aegis-staging-deploy/.env (nicht im Repo)

  gitea_webhook:
    repo: AegisSight/AegisSight-Monitor
    url: https://staging.monitor.aegis-sight.de/__deploy
    branch_filter: develop

live_systemd:
  service: aegis-monitor.service
  hinweis: |
    Live-Monitor laeuft seit 2026-04-26 als systemd-Service (vorher loser
    uvicorn-Prozess). Manueller Restart bei Backend-Aenderungen:
      sudo systemctl restart aegis-monitor
    Beim Promote via UI passiert das automatisch.

promote_ui:
  url: https://deploy.aegis-sight.de
  laeuft_auf: 46.225.225.49 (zentral fuer alle Services)
  zugriff: Magic-Link-Login an info@aegis-sight.de
  funktion: |
    Live- vs. Staging-Stand pro Service inkl. Liste der ausstehenden Commits.
    Promote-Knopf -> Gitea-PR develop->main wird auto-gemerged + Live-Listener
    pullt main + restartet aegis-monitor.
```

### Vollstaendiger Workflow (Aenderung am Monitor)

1. **Entwickeln in develop**:
   ```bash
   cd ~/AegisSight-Monitor-staging
   git checkout develop
   # Aenderung
   git add . && git commit -m "..." && git push origin develop
   # Auto-Deploy pullt automatisch + restartet aegis-monitor-staging
   ```

2. **Auf https://staging.monitor.aegis-sight.de pruefen**

3. **Promoten via https://deploy.aegis-sight.de** (Klick auf Monitor-Karte)
   → Gitea merged develop→main → Listener pullt main → `systemctl restart aegis-monitor`

4. **Live-Check auf https://monitor.aegis-sight.de**
