Nach Phase 16 (Monitor-source_health.py auf Phase-2-Stand) sind alle
4 shared/-Dateien wieder identisch zwischen Monitor und Verwaltung.
Der Lock auf source_health.py war nur fuer den Zeitraum noetig, in dem
die Verwaltung die History-Logik schon hatte und der Monitor noch nicht.
Bisher waren die DB-Felder sources.language und sources.bias zwar gepflegt
(254/275 Quellen mit bias, 254 mit language), aber in der Verwaltung nicht
sichtbar. Der Admin konnte nicht filtern oder editieren.
Backend (routers/sources.py)
- GlobalSourceCreate + GlobalSourceUpdate Pydantic-Modelle: language +
bias als Optional[str] erweitert (max 100 / 500 Zeichen).
- SOURCE_UPDATE_COLUMNS: language + bias hinzu.
- INSERT in create_global_source: schreibt language + bias mit.
- Neuer Endpoint GET /api/sources/global/languages: distinct language-Werte
fuer Frontend-Filter-Dropdown.
Frontend HTML (dashboard.html)
- Grundquellen-Filter-Bar: Sprachen-Dropdown ergaenzt.
- Grundquellen-Tabellenkopf: 2 neue Spalten Sprache (sortable) + Bias.
- modalSource: 2 neue Felder language (mit datalist Vorschlaegen) + bias.
- Kundenquellen-Filter-Bar: Sprachen-Dropdown.
- Kundenquellen-Tabellenkopf: Sprache (sortable) + Bias.
Frontend JS (sources.js)
- loadGlobalSources lädt /languages parallel zu /global + /global/stats,
populiert beide Sprache-Dropdowns + datalist im Edit-Modal.
- renderGlobalSources: cols 11 -> 13, language+bias-Zellen
(Bias mit Tooltip fuer Lang-Texte).
- filterGlobalSources: Sprache-Filter, Bias in Suche.
- editGlobalSource: language + bias laden.
- Form-Submit: language + bias mitgesendet.
- renderTenantSources: cols 8 -> 10, language+bias-Zellen.
- tenantFilters um language erweitert, applyTenantFilterAndSort prueft.
Cache-Buster ?v=20260509 (heute) bleibt - Tag wechselt erst morgen.
Live-Symptom: User sah leere Audit-Tabelle obwohl Backend 22 Eintraege
lieferte. Ursache: Browser hatte alte audit.js gecached (von vor Phase 5/8b),
in der die Audit-Render-Logik anders war oder fehlte.
Aktuell ohne Cache-Buster cacht der Browser die JS aggressiv. Mit ?v=YYYYMMDD
laedt der Browser bei jedem Bump die neue Version.
Beim naechsten Frontend-Patch in dieser Verwaltung: Cache-Buster auf neues
Datum bumpen, damit alle Browser wieder neu laden.
15 pyflakes-Warnings entfernt:
- src/audit.py: HTTPException (in router import statt helper, war hier ungenutzt)
- src/routers/auth.py: status (FastAPI-status ungenutzt)
- src/routers/audit.py: HTTPException (ungenutzt)
- src/routers/users.py: MAGIC_LINK_EXPIRE_MINUTES (ungenutzt)
- src/routers/sources.py: row_to_dict, _extract_domain, _detect_category,
urlparse, status (alle ungenutzt - status.HTTP_* wird nirgendwo aufgerufen)
- src/routers/sources.py: 2x f-string ohne Placeholder (URL aktualisiert,
Verbindung fehlgeschlagen) zu normalen Strings
- src/routers/sources.py: except httpx.ConnectError as e -> e ungenutzt, weg
- src/database.py: os ungenutzt
- src/models.py: EmailStr ungenutzt
Audit-Coverage geprueft: alle write-Endpoints in users.py rufen
_toggle_field() auf, das die log_action-Aufrufe macht. Keine Audit-Luecken.
Alle anderen Routers (organizations/licenses/dashboard/token_usage)
hatten bereits saubere Audit-Coverage.
Mojibake-Diagnose ueber alle src/*.py: 0 Treffer.
Phase 8a (Hook):
- scripts/git-hooks/pre-commit: prueft bei Commits mit src/shared/-Aenderungen
den Drift-Stand via sync_shared.py --check und gibt eine Warnung aus
(blockiert NICHT - User entscheidet selbst, ob er zurueck will).
- scripts/install-hooks.sh: kopiert Hooks aus scripts/git-hooks/ nach
.git/hooks/ (idempotent, ueberspringt user-eigene Hooks).
Phase 8b (Audit-UI):
- dashboard.html: Resource-ID Eingabefeld neben den anderen Audit-Filtern.
- audit.js: Filter-Listen erweitern, params um resource_id ergaenzt
(Backend hatte den Filter seit Phase 5 schon).
- Damit ist die Audit-Spur einer einzelnen Ressource auch im Audit-Log-Tab
filterbar (vorher nur per Direkt-URL bzw. per Quellen-Audit-Modal).
scripts/sync_shared.py: hält src/shared/ in sync mit dem Monitor-Repo
- --check: Drift-Diagnose ohne Schreiben (Exit 1 bei auto-sync-Drift, 0 bei
nur LOCKED-Drift = informativ)
- --apply: schreibt Drift, ueberspringt LOCKED_FILES
- Mojibake-Schutz via ftfy (Monitor-Originale haben teilweise noch Doppel-
Encoded UTF-8, das fixed wird beim Sync)
- Imports-Patch: from agents. -> from shared.agents. (etc.) damit Module
innerhalb von src/shared/ ihre Geschwister korrekt finden
LOCKED_FILES (nicht auto-syncbar):
- src/shared/services/source_health.py (Phase-2-Fork: tenant_id-Filter weg,
History-Archivierung, Config-Konstanten - waere im Monitor unsinnig)
Hintergrund: Phase 1 hat src/shared/ als 1:1-Kopie aus dem Monitor angelegt.
Phase 2 hat source_health.py spezifisch fuer die Verwaltung erweitert.
Ein blinder Sync wuerde Phase-2-Aenderungen ueberschreiben - Lock-Mechanismus
verhindert das, meldet aber Drift zur Information.
CLAUDE.md: Sektion Shared-Module-Sync mit Workflow-Doku.
Backend
- routers/audit.py: GET /api/audit-log nimmt jetzt resource_id als Filter
(zusätzlich zu resource_type, action, admin_id, from_ts/to_ts).
Frontend
- dashboard.html: modalAudit (Modal) für die Audit-Spur einer Ressource.
- style.css: audit-entry Styles (action-Badge mit Farbcode pro Action-Typ,
Diff als <details>-Block mit JSON-Pre).
- sources.js:
- showSourceAudit(id, name) öffnet Modal, lädt /audit-log?resource_type=source&resource_id=...
- renderAuditEntries: pro Eintrag Action-Badge + Meta (ts/admin/ip) +
optional ausklappbarer Diff (before/after-JSON)
- formatDateTime Helper
- Audit-Button in der Aktionen-Spalte der Grundquellen-Tabelle
Backend
- routers/sources.py: POST /api/sources/tenant/bulk-promote NEU
Nimmt Liste von source_ids, promotet jede einzeln zur Grundquelle.
Returns {promoted, skipped[{id,name,reason}], failed[{id,error}]}.
Ueberspringt Quellen die schon Grundquellen sind oder deren URL bereits
als Grundquelle existiert.
Frontend
- dashboard.html sub-tenant-sources: action-bar erweitert um
3 Filter-Selects (Typ, Kategorie, Org), Bulk-Promote-Button.
Tabelle bekommt Checkbox-Spalte + sortable Spalten (Sort-Icons).
- sources.js: tenant-Tab komplett refactored
- State: tenantFilters, tenantSort, tenantSelected (Set)
- applyTenantFilterAndSort: zentraler Render-Pfad mit allen Filtern + Sort
- populateTenantFilters: Org-Liste aus Daten, Typ/Kategorie aus META
- toggleTenantSelect / toggleTenantSelectAll: Selection-Logik
- bulkPromoteSelected: showConfirm -> POST -> Toast mit Ergebnis
- renderTenantSources: Checkbox-Spalte, dynamische typeLabel/categoryLabel
- Counter zeigt jetzt N gefiltert / Gesamt
- src/source_meta.py NEU: SOURCE_CATEGORIES + SOURCE_TYPES als
Single Source of Truth (Liste mit {key, label}). category_label/type_label
Lookup-Funktionen, get_meta() liefert das gesamte Set.
- src/routers/sources.py: GET /api/sources/meta ergänzt (admin-auth,
liefert Kategorien + Typen)
- src/static/js/app.js: window.META + loadMeta() + categoryLabel/typeLabel +
populateSelect Helper. Beim DOMContentLoaded wird Meta geladen, befüllt
globale CATEGORY_LABELS und TYPE_LABELS.
- src/static/js/sources.js: hardcoded const CATEGORY_LABELS und TYPE_LABELS
entfernt - werden jetzt aus app.js loadMeta() global gesetzt.
loadGlobalSources() ruft populateSelect() für die Filter-Dropdowns auf.
- src/static/js/source-health.js: gleiche hardcoded Listen entfernt.
- src/static/dashboard.html: <option>-Listen für globalFilterCategory und
globalFilterType entfernt (nur noch default Alle). JS befüllt sie dynamisch.
Ergebnis: Bei einer neuen Kategorie nur source_meta.py anpassen,
keine 3-fach-Pflege mehr in HTML+sources.js+source-health.js.
- migrations/2026-05-09d_source_health_history.py NEU: source_health_history-Tabelle
(Append-only Verlauf der Health-Check-Runs mit run_id und archived_at)
- shared/services/source_health.py:
- tenant_id IS NULL Filter raus -> auch Tenant-Quellen werden gecheckt
- Mojibake (Triple-Encoded UTF-8) via ftfy gefixt
- DELETE FROM source_health_checks: vorher Stand mit run_id (uuid4) in
source_health_history archivieren -> kein Datenverlust mehr
- User-Agent + Timeout aus config.HEALTH_CHECK_* statt hardcoded
- routers/sources.py /health/run-stream: gleiche Änderungen wie oben
- config.py: HEALTH_CHECK_USER_AGENT + HEALTH_CHECK_TIMEOUT_S ergänzt
Idempotente Migration mit --db Parameter, Backup vor Lauf, Sample-Output.
Behandelt headline_de + content_de bei allen Artikeln; bei language=de
zusaetzlich headline + content_original. Nutzt das gleiche hunspell-Dict
wie der Live-QC.
Idempotente Migration mit --db Parameter (Live + Staging benutzbar),
Backup vor Lauf, Verifikations-Check nach Lauf. Selektiert alle Artikel
mit HTML-Tag-Pattern, strippt via html_to_text-Helper aus dem Monitor-Repo,
respektiert 1000-Zeichen-Cap.
- Schema-Migration: ON DELETE SET NULL fuer incidents.created_by, magic_links.user_id,
network_analyses.created_by (behebt 500er beim User-Loeschen). Neue Spalte
licenses.unlimited_budget. Neue Tabellen portal_audit_log, portal_login_attempts.
- Audit-Log: alle CREATE/UPDATE/DELETE auf Org/User/Lizenz/Quelle + Login-Events
werden mit before/after-Diff in portal_audit_log geschrieben.
- Brute-Force-Schutz: 5 Fehlversuche pro IP+Username/15min -> 429 mit Retry-After.
- Token-Budget: expliziter Schalter unlimited_budget pro Lizenz. UI zeigt ehrlich
>100%-Verbrauch (kein Math.min mehr) und ungebremste Anzeige bei unlimited.
- Neuer Audit-Log Tab mit Filter (Aktion/Ressource/Admin/Zeitraum) und Pagination.
Passt Verwaltung an die Podcast-Integration im Monitor an (Commit 5127e0a):
Backend (src/routers/sources.py):
- Pydantic-Pattern von GlobalSourceCreate + GlobalSourceUpdate um
podcast_feed erweitert
- Health-Check Feed-Validierung greift jetzt auch fuer podcast_feed
(Podcast-Feeds sind technisch RSS/Atom)
Frontend:
- src/static/js/sources.js: TYPE_LABELS um podcast_feed ("Podcast-Feed")
ergaenzt
- src/static/dashboard.html: Neue <option value=podcast_feed> in Filter-
und Anlage-Dropdown
Ohne diese Anpassung waere das Anlegen von Podcast-Quellen ueber das
Verwaltungsportal nicht moeglich (422 Unprocessable Entity vom
Pydantic-Validator).
- Backend liefert usage_by_source im current-Endpoint
- Monatliche Tabelle zeigt Quelle-Badge (Monitor/Globe)
- Source-Split unter den Kosten-KPIs sichtbar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- network_access Spalte in UserResponse Model
- PUT /api/users/{id}/network-access Toggle-Endpoint
- Dashboard: Netzwerk-Spalte mit An/Aus-Button in User-Tabelle
Ursache: UserResponse hatte kein globe_access Feld, response_model
filterte es aus der API-Antwort. Checkbox zeigte immer false.
UI: Checkbox durch An/Aus Toggle-Button ersetzt (gruen/grau).
Nach Klick wird Liste neu geladen und zeigt korrekten DB-State.
Tabellen-Reload nach erfolgreichem Toggle entfernt.
Checkbox bleibt im angeklickten Zustand, DB wird korrekt aktualisiert.
Nur bei API-Fehler wird die Tabelle neu geladen.
Neue Spalte "Globe" in der Nutzertabelle mit Toggle-Checkbox.
API-Endpoint PUT /api/users/{id}/globe-access.
Steuert das globe_access Feld in der geteilten DB.
- 3 Options (russische-staatspropaganda, russische-opposition, syrien-nahost)
in den Select-Tag verschoben (waren außerhalb und nicht auswählbar)
- IntelSight -> AegisSight in E-Mail-Templates und JWT Issuer/Audience
- MAGIC_LINK_BASE_URL: osint.intelsight.de -> monitor.aegis-sight.de
- SMTP Default: noreply@aegis-sight.de
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Duplikat-Check in search-fix auf source_id+type umgestellt
- Stale-Check im Streaming-Endpoint überspringt web_sources
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Neuer Endpoint /health/run-stream mit Server-Sent Events
- Frontend zeigt Fortschrittsbalken: 4/12 + Quellenname + Prozent
- Status-Icons pro Quelle (Fehler/Warnung/OK)
- Phase Vorschläge wird separat angezeigt
- Ergebnis-Zusammenfassung verschwindet nach 5 Sekunden
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sonnet: max 3 Lösungen, echte Umlaute erzwingen
- Auto-Reject: deactivate_source wird abgelehnt wenn fix_url akzeptiert
- Titel-Limit von 80 auf 120 Zeichen erhöht
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Button erscheint neben Annehmen/Ablehnen bei deactivate_source-
Vorschlägen mit source_id. Startet Sonnet WebSearch-Recherche
nach Alternativen bevor man deaktiviert.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`...` innerhalb von ${} Expressions ist kein valides JS und
verhinderte Variablen-Substitution + crashte das Script in manchen
Browsern, wodurch alle Quellen-Tabs nicht funktionierten.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- formatDate() in source-health.js zu formatDateTime() umbenannt,
damit app.js formatDate() nicht überschrieben wird
- searchFix(): data-Attribute statt inline onclick-String (XSS-sicher)
- searchFix(btn): expliziter Parameter statt implizites event.target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sourceSubTabs vom Haupt-Navigation-Handler ausgeschlossen -
verhindert dass Source-Sub-Tabs die Hauptnavigation stören
und Inhalte unterhalb anderer Tabs angezeigt werden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>