Commits vergleichen
2 Commits
e31536f8f9
...
e52202b087
| Autor | SHA1 | Datum | |
|---|---|---|---|
|
|
e52202b087 | ||
|
|
670a6617a7 |
166
CLAUDE.md
Normale Datei
166
CLAUDE.md
Normale Datei
@@ -0,0 +1,166 @@
|
|||||||
|
# AegisSight-Monitor-Verwaltung
|
||||||
|
|
||||||
|
> Admin-Portal für Mandanten, Lizenzen, Nutzer, Grundquellen und Token-Verbrauch des AegisSight-Monitors
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
projekt: AegisSight-Monitor-Verwaltung
|
||||||
|
url: https://monitor-verwaltung.aegis-sight.de
|
||||||
|
server: ssh monitor (46.225.141.13, User: claude-dev)
|
||||||
|
pfad: /home/claude-dev/AegisSight-Monitor-Verwaltung
|
||||||
|
quellcode: /home/claude-dev/AegisSight-Monitor-Verwaltung/src/
|
||||||
|
datenbank: /mnt/gitea/osint-data/osint.db (SQLite WAL, geteilt mit AegisSight-Monitor)
|
||||||
|
gitea: https://gitea-undso.aegis-sight.de/AegisSight/AegisSight-Monitor-Verwaltung
|
||||||
|
service: verwaltungsportal.service (systemd, Port 8892, Nginx Reverse Proxy)
|
||||||
|
venv: /home/claude-dev/.venvs/verwaltung/ (Python 3.12)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technologie-Stack
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backend:
|
||||||
|
framework: FastAPI + Uvicorn
|
||||||
|
datenbank: SQLite WAL (aiosqlite, async) - geteilt mit AegisSight-Monitor
|
||||||
|
auth: Passwort-Login (bcrypt, JWT HS256, 8 Stunden)
|
||||||
|
brute_force_schutz: 5 Fehlversuche pro 15 Minuten Block, Aufräumen nach 24 Stunden
|
||||||
|
audit: jede Mutation via log_action -> portal_audit Tabelle
|
||||||
|
email: aiosmtplib (smtp.ionos.de:587 TLS) - für Magic-Link-Einladungen Richtung Monitor
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
typ: Vanilla JS (kein Framework, kein Build-Step)
|
||||||
|
design: AegisSight Dark Theme (gemeinsame Optik wie Monitor)
|
||||||
|
fonts: Poppins (Titel), Inter (Body)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
src/:
|
||||||
|
main.py: "FastAPI App, Login + Brute-Force-Logik, Lifespan, statische Routen"
|
||||||
|
config.py: "Konfiguration (DB-Pfad, JWT, SMTP, Source-Discovery-Konstanten)"
|
||||||
|
auth.py: "Passwort-Hash (bcrypt), JWT erstellen/verifizieren, get_current_admin Dependency"
|
||||||
|
database.py: "DB-Connection-Pool, Schema-Helper"
|
||||||
|
models.py: "Pydantic Request/Response-Schemas"
|
||||||
|
audit.py: "log_action, get_client_ip, row_to_dict, /api/audit Router"
|
||||||
|
|
||||||
|
routers/:
|
||||||
|
organizations.py: "CRUD Mandanten (organizations + Org-Settings + Token-Budget)"
|
||||||
|
licenses.py: "CRUD Lizenzen (Org-Lizenzen, Ablauf, Nutzer-Limit, Module)"
|
||||||
|
users.py: "CRUD User pro Org, Magic-Link-Einladung an info@aegis-sight.de"
|
||||||
|
sources.py: "Grundquellen, Tenant-Quellen-Übersicht, Discovery, Health-Check, KI-Vorschläge"
|
||||||
|
dashboard.py: "Aggregat-Endpoints für Übersichts-Tab"
|
||||||
|
token_usage.py: "Token-Verbrauch pro Org/Monat, Budget-Steuerung"
|
||||||
|
audit.py: "Audit-Log-Abfrage, Filter"
|
||||||
|
|
||||||
|
email_utils/:
|
||||||
|
sender.py: "Async SMTP Versand"
|
||||||
|
templates.py: "HTML-Templates (Magic-Link für neue Nutzer)"
|
||||||
|
|
||||||
|
static/:
|
||||||
|
index.html: "Login (Passwort)"
|
||||||
|
dashboard.html: "Hauptdashboard mit Tabs (Dashboard, Orgs, Lizenzen, Quellen, Audit)"
|
||||||
|
favicon.svg: "AegisSight Logo"
|
||||||
|
css/: "Stylesheets (Dark Theme)"
|
||||||
|
js/:
|
||||||
|
app.js: "Hauptlogik, Login, Tab-Switching, Dashboard-Render"
|
||||||
|
sources.js: "Grundquellen + Kundenquellen Management"
|
||||||
|
source-health.js: "Quellen-Health & KI-Vorschläge"
|
||||||
|
audit.js: "Audit-Log Tab"
|
||||||
|
|
||||||
|
migrations/:
|
||||||
|
einmal_migrationen: "Backfill-Skripte (DE-Übersetzungen, Umlaute, HTML-Strip etc.)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datenbank-Tabellen (relevant fürs Portal)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
kern: "organizations, licenses, users, magic_links, portal_admins"
|
||||||
|
quellen: "sources (geteilt mit Monitor), source_health_checks, source_suggestions"
|
||||||
|
verbrauch: "token_usage_monthly"
|
||||||
|
audit: "portal_audit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwandte Projekte
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
monitor:
|
||||||
|
pfad: /home/claude-dev/AegisSight-Monitor
|
||||||
|
url: https://monitor.aegis-sight.de
|
||||||
|
service: aegis-monitor.service (Port 8891)
|
||||||
|
geteilte_db: ja
|
||||||
|
geteilte_module:
|
||||||
|
- source_rules: "Domain-Erkennung, RSS-Discovery, Claude-Feed-Bewertung"
|
||||||
|
- services/source_health: "Health-Check-Logik"
|
||||||
|
- services/source_suggester: "KI-Quellenvorschläge"
|
||||||
|
- agents/claude_client: "Shared Claude CLI Client"
|
||||||
|
hinweis: "Verwaltung importiert diese Module; sys.path-Hacks sollen schrittweise durch eigene Kopien in src/shared/ ersetzt werden"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regeln
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
regeln:
|
||||||
|
- "Jede Änderung MUSS sofort committed und nach Gitea gepusht werden"
|
||||||
|
- "Echte Umlaute (ü, ä, ö, ß), niemals Umschreibungen (ue, ae, oe, ss) - gilt auch in Code-Kommentaren, Logs, UI-Texten"
|
||||||
|
- "Keine Passwörter oder Secrets in den Code committen (.env nicht im Repo)"
|
||||||
|
- "Service nach Backend-Änderungen: sudo systemctl restart verwaltungsportal"
|
||||||
|
- "Frontend-Änderungen (HTML/JS/CSS) brauchen keinen Neustart"
|
||||||
|
- "Backup-Dateien (.bak) nicht committen, vor Push löschen"
|
||||||
|
- "Code-Fixes immer über develop -> Staging -> Promote, niemals direkt auf main"
|
||||||
|
- "Direkte Live-DB-Patches nur nach Vorab-Ankündigung"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changelog-Workflow
|
||||||
|
|
||||||
|
Bei JEDER Änderung an dieser Anwendung müssen zwei Dinge passieren:
|
||||||
|
|
||||||
|
1. **TaskMate Wissensdatenbank** (Kategorie: "Changelog Verwaltung", category_id=34)
|
||||||
|
2. **Git Commit + Push zu Gitea**
|
||||||
|
|
||||||
|
Siehe AegisSight-Monitor/CLAUDE.md für vollständiges Beispiel des TaskMate-Aufrufs.
|
||||||
|
|
||||||
|
## Staging-Umgebung
|
||||||
|
|
||||||
|
Wird im Rahmen des Aufräum-Plans (Phase 0) aufgesetzt. Geplante Eckdaten:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
staging:
|
||||||
|
url: https://staging.monitor-verwaltung.aegis-sight.de
|
||||||
|
server: 46.225.141.13 (gleicher Host wie Live)
|
||||||
|
pfad: /home/claude-dev/AegisSight-Monitor-Verwaltung-staging
|
||||||
|
branch: develop
|
||||||
|
port: 18892 (Live: 8892)
|
||||||
|
service: aegis-verwaltung-staging.service
|
||||||
|
venv: /home/claude-dev/AegisSight-Monitor-Verwaltung-staging/venv (eigenes venv)
|
||||||
|
zugriff: Magic-Link-Login an info@aegis-sight.de (Cookie 30 Tage, vorgelagerter Auth-Service)
|
||||||
|
|
||||||
|
datenbank:
|
||||||
|
plan: eigene SQLite-Kopie der Live-DB in ~/AegisSight-Monitor-Verwaltung-staging/data/osint.db
|
||||||
|
drift: gewollt - Änderungen in Staging beeinflussen Live nicht
|
||||||
|
abstimmung: gemeinsame DB mit Monitor-Staging möglich, wird beim Aufbau entschieden
|
||||||
|
|
||||||
|
auth_service:
|
||||||
|
pfad: /opt/aegis-verwaltung-staging-auth
|
||||||
|
service: aegis-verwaltung-staging-auth.service
|
||||||
|
port: 127.0.0.1:8098 (Monitor-Staging-Auth liegt schon auf 8095)
|
||||||
|
cookie_domain: staging.monitor-verwaltung.aegis-sight.de
|
||||||
|
cookie_name: aegis_verwaltung_staging_auth
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow develop -> Staging -> Live (Plan)
|
||||||
|
|
||||||
|
1. **Änderung in develop machen**:
|
||||||
|
```bash
|
||||||
|
cd ~/AegisSight-Monitor-Verwaltung
|
||||||
|
git checkout develop
|
||||||
|
# Änderung
|
||||||
|
git add . && git commit -m '...' && git push origin develop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Auto-Deploy** (geplant, Phase 0f): Gitea-Webhook -> aegis-staging-deploy.service -> pullt develop ins Staging-Verzeichnis -> restartet aegis-verwaltung-staging
|
||||||
|
|
||||||
|
3. **Auf https://staging.monitor-verwaltung.aegis-sight.de prüfen**
|
||||||
|
|
||||||
|
4. **Promote zu Live** über https://deploy.aegis-sight.de (Phase 0g)
|
||||||
|
-> Gitea-PR develop->main automerge -> Live-Listener pullt main -> systemctl restart verwaltungsportal
|
||||||
@@ -23,19 +23,23 @@ from datetime import datetime
|
|||||||
# Pfade fuer Imports (Live-Repo bevorzugt, Staging-Fallback)
|
# Pfade fuer Imports (Live-Repo bevorzugt, Staging-Fallback)
|
||||||
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor/src")
|
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor/src")
|
||||||
try:
|
try:
|
||||||
from agents.translator import translate_articles
|
from agents.translator import translate_articles_batch, DEFAULT_BATCH_SIZE
|
||||||
from agents.claude_client import UsageAccumulator
|
from agents.claude_client import UsageAccumulator
|
||||||
from services.post_refresh_qc import normalize_german_umlauts
|
from services.post_refresh_qc import normalize_german_umlauts
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor-staging/src")
|
sys.path.insert(0, "/home/claude-dev/AegisSight-Monitor-staging/src")
|
||||||
from agents.translator import translate_articles
|
from agents.translator import translate_articles_batch, DEFAULT_BATCH_SIZE
|
||||||
from agents.claude_client import UsageAccumulator
|
from agents.claude_client import UsageAccumulator
|
||||||
from services.post_refresh_qc import normalize_german_umlauts
|
from services.post_refresh_qc import normalize_german_umlauts
|
||||||
|
|
||||||
|
|
||||||
async def main_async(db_path: str, dry_run: bool, limit: int | None) -> int:
|
async def main_async(db_path: str, dry_run: bool, limit: int | None) -> int:
|
||||||
db = sqlite3.connect(db_path)
|
db = sqlite3.connect(db_path, timeout=60)
|
||||||
db.row_factory = sqlite3.Row
|
db.row_factory = sqlite3.Row
|
||||||
|
# Live-Service haelt regelmaessig den Write-Lock. Statt sofort zu crashen
|
||||||
|
# warten wir bis zu 60 Sekunden auf den Lock.
|
||||||
|
db.execute("PRAGMA busy_timeout = 60000")
|
||||||
|
db.execute("PRAGMA journal_mode = WAL")
|
||||||
|
|
||||||
sql = """SELECT id, incident_id, headline, content_original, language
|
sql = """SELECT id, incident_id, headline, content_original, language
|
||||||
FROM articles
|
FROM articles
|
||||||
@@ -60,42 +64,76 @@ async def main_async(db_path: str, dry_run: bool, limit: int | None) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
usage = UsageAccumulator()
|
usage = UsageAccumulator()
|
||||||
translations = await translate_articles(rows, output_lang="de",
|
total = len(rows)
|
||||||
usage_accumulator=usage)
|
batch_size = DEFAULT_BATCH_SIZE
|
||||||
print(f"Uebersetzt: {len(translations)} von {len(rows)}")
|
PARALLEL_WORKERS = 4
|
||||||
|
|
||||||
updated = 0
|
updated = 0
|
||||||
for t in translations:
|
translated = 0
|
||||||
hd = t.get("headline_de")
|
sample_translations = []
|
||||||
cd = t.get("content_de")
|
completed_count = 0
|
||||||
if hd:
|
print(f"Starte parallele Verarbeitung: Batches a {batch_size}, {PARALLEL_WORKERS} Worker parallel...", flush=True)
|
||||||
hd, _ = normalize_german_umlauts(hd)
|
|
||||||
if cd:
|
# Batches vorbereiten
|
||||||
cd, _ = normalize_german_umlauts(cd)
|
batches = [rows[i:i + batch_size] for i in range(0, total, batch_size)]
|
||||||
if hd or cd:
|
semaphore = asyncio.Semaphore(PARALLEL_WORKERS)
|
||||||
db.execute(
|
|
||||||
"UPDATE articles SET headline_de = COALESCE(?, headline_de), "
|
async def process_batch(batch):
|
||||||
"content_de = COALESCE(?, content_de) WHERE id = ?",
|
async with semaphore:
|
||||||
(hd, cd, t["id"]),
|
return await translate_articles_batch(batch)
|
||||||
|
|
||||||
|
# Tasks erstellen und in beliebiger Reihenfolge bearbeiten
|
||||||
|
tasks = [asyncio.create_task(process_batch(b)) for b in batches]
|
||||||
|
n_batches = len(batches)
|
||||||
|
|
||||||
|
for completed_task in asyncio.as_completed(tasks):
|
||||||
|
try:
|
||||||
|
translations, batch_usage = await completed_task
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Batch-Fehler: {e}", flush=True)
|
||||||
|
continue
|
||||||
|
usage.add(batch_usage)
|
||||||
|
translated += len(translations)
|
||||||
|
for t in translations:
|
||||||
|
hd = t.get("headline_de")
|
||||||
|
cd = t.get("content_de")
|
||||||
|
if hd:
|
||||||
|
hd, _ = normalize_german_umlauts(hd)
|
||||||
|
if cd:
|
||||||
|
cd, _ = normalize_german_umlauts(cd)
|
||||||
|
if hd or cd:
|
||||||
|
db.execute(
|
||||||
|
"UPDATE articles SET headline_de = COALESCE(?, headline_de), "
|
||||||
|
"content_de = COALESCE(?, content_de) WHERE id = ?",
|
||||||
|
(hd, cd, t["id"]),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
if len(sample_translations) < 3:
|
||||||
|
sample_translations.append(t["id"])
|
||||||
|
db.commit() # Per-Batch commit -> bei Abbruch kein Datenverlust
|
||||||
|
|
||||||
|
completed_count += 1
|
||||||
|
if completed_count % 20 == 0 or completed_count == n_batches:
|
||||||
|
print(
|
||||||
|
f"[{completed_count}/{n_batches} Batches | Updates={updated}/{total} | Cost=${usage.total_cost_usd:.2f}]",
|
||||||
|
flush=True,
|
||||||
)
|
)
|
||||||
updated += 1
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"=== Stats ===")
|
print(f"=== Stats ===")
|
||||||
print(f" Updates: {updated}")
|
print(f" Total betrachtet: {total}")
|
||||||
print(f" Calls: {usage.call_count}")
|
print(f" Translator OK: {translated}")
|
||||||
print(f" Input-Tokens: {usage.input_tokens:,}")
|
print(f" DB-Updates: {updated}")
|
||||||
print(f" Output-Tokens: {usage.output_tokens:,}")
|
print(f" Calls: {usage.call_count}")
|
||||||
print(f" Cost gesamt: ${usage.total_cost_usd:.4f}")
|
print(f" Input-Tokens: {usage.input_tokens:,}")
|
||||||
|
print(f" Output-Tokens: {usage.output_tokens:,}")
|
||||||
|
print(f" Cost gesamt: ${usage.total_cost_usd:.4f}")
|
||||||
print()
|
print()
|
||||||
print("=== Stichprobe (3 frische Uebersetzungen) ===")
|
print("=== Stichprobe (3 frische Uebersetzungen) ===")
|
||||||
sample_ids = [t["id"] for t in translations[:3]]
|
if sample_translations:
|
||||||
if sample_ids:
|
placeholders = ",".join("?" * len(sample_translations))
|
||||||
placeholders = ",".join("?" * len(sample_ids))
|
|
||||||
for r in db.execute(
|
for r in db.execute(
|
||||||
f"SELECT id, headline, headline_de FROM articles WHERE id IN ({placeholders})",
|
f"SELECT id, headline, headline_de FROM articles WHERE id IN ({placeholders})",
|
||||||
sample_ids,
|
sample_translations,
|
||||||
):
|
):
|
||||||
d = dict(r)
|
d = dict(r)
|
||||||
print(f" [{d['id']}]")
|
print(f" [{d['id']}]")
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren