diff --git a/CLAUDE.md b/CLAUDE.md index 46dd724..4df5b74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,8 @@ src/: token_usage.py: "Token-Verbrauch pro Org/Monat, Credits-Stellwerte je Lizenz (PUT /budget). Laufzeitfelder (credits_used-Reset, credits_carried, credits_period_start, budget_warning_sent) verwaltet der MONITOR, das Portal setzt nur Stellwerte" pricing.py: "Preis-Empfehlung aus Ist-Kosten (GET /cost-basis) und pflegbare Preistabelle billing_tariff (GET/PUT /tariff). Die Tabelle ist die Quelle der Buchung im Monitor, der Rechner kann sie speichern" statistik.py: "Betriebsstatistik (GET /api/statistik?months=N). Kosten/Marge je Monat und Org, Betrieb, Fehler-Ranking, MAU/DAU aus user_activity_days, Wachstum. Aggregation als pure Funktionen (unit-testbar)" - audit.py: "Audit-Log-Abfrage, Filter" + audit.py: "Audit-Log-Abfrage, Filter (action/resource_type/admin_id akzeptieren kommagetrennte Mehrfachwerte)" + x_scraper.py: "Recherche-Zugänge: twscrape-X-Konten (eigener Store, X_ACCOUNTS_DB_PATH) + GET /telegram-status (liest system_status, gemeldet vom Monitor)" email_utils/: sender.py: "Async SMTP Versand" @@ -70,12 +71,14 @@ src/: a11y.js: "Barrierefreiheits-Panel (Kontrast, Focus, Schrift, Animationen). IDENTISCHE Datei wie im Monitor-Repo, bei Aenderungen dort mitziehen" statistik.js: "Statistik-Tab, selbst-injizierend wie rechner.js. Inline-SVG-Charts (keine Bibliothek), Serienfarben mit dem dataviz-Validator gegen beide Themes geprüft" sources.js: "Vereinte Quellenliste (Grund- + Kundenquellen, Herkunfts-Filter, Health-Ausklapp mit Deaktivieren, Bulk-Promote, Modal, Discovery, PDF-Upload)" - aufgaben.js: "Aufgaben-Posteingang (Health-Vorschläge + Klassifikations-Review + Verlauf, Badge via GET /tasks/summary)" + aufgaben.js: "Aufgaben-Posteingang (Health-Vorschläge + Klassifikations-Review + Verlauf, Badge via GET /tasks/summary, Sammel-Bearbeitung via PUT /suggestions/bulk)" + x-scraper.js: "Recherche-Zugänge (X-Konten-Pool + Telegram-Session-Status-Karte)" audit.js: "Audit-Log Tab" migrations/: einmal_migrationen: "Backfill-Skripte (DE-Übersetzungen, Umlaute, HTML-Strip etc.)" 2026-07-25_user_activity.py: "Tabelle user_activity_days anlegen (MAU/DAU). Nötig für die Portal-Staging-DB, auf Live legt sie der Monitor an." + 2026-07-25_system_status.py: "Tabelle system_status anlegen (Key-Value, u.a. Telegram-Session-Status). Nötig für die Portal-Staging-DB, auf Live legt sie der Monitor an. Reihenfolge: Monitor VOR Portal promoten." 2026-07-25_billing_tariff.py: "Preistabelle billing_tariff anlegen und mit den beschlossenen Sätzen befüllen (idempotent, überschreibt nichts). Nötig für die Portal-Staging-DB, auf Live legt sie der Monitor an." 2026-07-25_credits_period.py: "Credits-Perioden-Spalten in licenses (idempotent, identisch zur Monitor-Migration). Nötig für die Portal-Staging-DB, auf Live legt sie normalerweise der Monitor an. Reihenfolge: Monitor VOR Portal promoten." ``` diff --git a/migrations/2026-07-25_system_status.py b/migrations/2026-07-25_system_status.py new file mode 100644 index 0000000..2e96c40 --- /dev/null +++ b/migrations/2026-07-25_system_status.py @@ -0,0 +1,46 @@ +"""Migration 2026-07-25: Tabelle system_status anlegen (Telegram-Session-Status). + +Der Monitor legt die Tabelle beim Start selbst an und meldet dort u.a. den +Status seiner Telegram-Session (Key telegram_session). Die +Portal-Staging-Umgebung nutzt eine EIGENE Datenbank ohne laufenden Monitor, +deshalb dieses Skript. Idempotent (CREATE TABLE IF NOT EXISTS), es werden +keine Daten eingefügt. + +Ausführung: + DB_PATH=/home/claude-dev/AegisSight-Monitor-staging/data/osint.db python3 migrations/2026-07-25_system_status.py + DB_PATH=/home/claude-dev/osint-data/osint.db python3 migrations/2026-07-25_system_status.py +""" +import os +import sqlite3 +import sys + + +def main(db_path: str) -> int: + if not os.path.exists(db_path): + print(f"FEHLER: DB nicht gefunden: {db_path}", file=sys.stderr) + return 1 + + conn = sqlite3.connect(db_path, timeout=60) + conn.execute("PRAGMA busy_timeout = 60000") + conn.execute("PRAGMA journal_mode = WAL") + + print(f"Migration auf {db_path}") + + conn.execute(""" + CREATE TABLE IF NOT EXISTS system_status ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + """) + print(" + system_status angelegt (oder vorhanden)") + + conn.commit() + conn.close() + print("Migration abgeschlossen.") + return 0 + + +if __name__ == "__main__": + db_path = os.environ.get("DB_PATH", "/home/claude-dev/osint-data/osint.db") + sys.exit(main(db_path)) diff --git a/src/routers/x_scraper.py b/src/routers/x_scraper.py index a38887a..c0495ec 100644 --- a/src/routers/x_scraper.py +++ b/src/routers/x_scraper.py @@ -76,6 +76,38 @@ class XScraperActive(BaseModel): active: bool +@router.get("/telegram-status") +async def telegram_status( + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Status der Telegram-Session, gemeldet vom Monitor (system_status). + + Der Monitor prüft beim Start und täglich um 04:30 und schreibt das + Ergebnis in die geteilte DB. Das Portal liest nur an. Fehlt der + Eintrag (z.B. Portal-Staging mit eigener DB-Kopie), meldet der + Endpoint available=false statt eines Fehlers. + """ + import json as _json + + cur = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='system_status'" + ) + if not await cur.fetchone(): + return {"available": False} + cur = await db.execute( + "SELECT value, updated_at FROM system_status WHERE key = 'telegram_session'" + ) + row = await cur.fetchone() + if not row: + return {"available": False} + try: + data = _json.loads(row["value"]) if row["value"] else {} + except Exception: + data = {} + return {"available": True, "updated_at": row["updated_at"], **data} + + @router.get("/accounts") async def list_accounts(admin: dict = Depends(get_current_admin)): """Alle X-Scraper-Konten auflisten (ohne Passwoerter/Cookies).""" diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 21aa7d4..1a5dfde 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -287,7 +287,7 @@ @@ -355,8 +355,12 @@
- +
+
+

Telegram-Zugang

+
Lade…
+
@@ -972,7 +976,7 @@ - +
diff --git a/src/static/js/x-scraper.js b/src/static/js/x-scraper.js index 77a77d3..cde246a 100644 --- a/src/static/js/x-scraper.js +++ b/src/static/js/x-scraper.js @@ -1,10 +1,11 @@ -/* X-Recherche-Konten: Verwaltung des twscrape-Account-Pools */ +/* Recherche-Zugänge: twscrape-Account-Pool (X) + Telegram-Session-Status */ "use strict"; let xScraperCache = []; async function loadXScraperAccounts() { setupXScraperForms(); + loadTelegramStatus(); const tbody = document.getElementById("xScraperTable"); tbody.innerHTML = 'Lade...'; try { @@ -15,6 +16,32 @@ async function loadXScraperAccounts() { } } +// Telegram-Session-Status (vom Monitor in die geteilte DB gemeldet) +async function loadTelegramStatus() { + const box = document.getElementById("telegramStatusBox"); + if (!box) return; + try { + const st = await API.get("/api/x-scraper/telegram-status"); + if (!st.available) { + box.innerHTML = 'Noch keine Statusmeldung vom Monitor vorhanden. ' + + 'Der Monitor meldet den Status beim Start und täglich um 04:30 Uhr.'; + return; + } + const stand = `Stand ${formatDateTime(st.updated_at)}`; + if (st.ok) { + box.innerHTML = `Session gültig ` + + `Verbunden${st.account ? " als " + esc(st.account) : ""}. ` + + `Die Telegram-Anmeldung läuft nicht periodisch ab, hier ist normalerweise nichts zu tun.${stand}`; + } else { + box.innerHTML = `Problem ` + + `${esc(st.error || "Session nicht autorisiert")}. ` + + `Die Telegram-Recherche ist pausiert, bis auf dem Server neu eingeloggt wurde.${stand}`; + } + } catch (err) { + box.innerHTML = 'Status konnte nicht geladen werden. ' + esc(err.message || "") + ''; + } +} + function renderXScraperAccounts(list) { const tbody = document.getElementById("xScraperTable"); const cnt = document.getElementById("xScraperCount"); diff --git a/tests/test_api_smoke.py b/tests/test_api_smoke.py index c7ee8d3..784640f 100644 --- a/tests/test_api_smoke.py +++ b/tests/test_api_smoke.py @@ -67,6 +67,8 @@ AUTH_PROTECTED = [ ("POST", "/api/sources/1/classification/reclassify"), ("POST", "/api/sources/external-reputation/sync"), ("POST", "/api/sources/global/upload-pdf"), + ("GET", "/api/x-scraper/accounts"), + ("GET", "/api/x-scraper/telegram-status"), ("GET", "/api/statistik"), ("GET", "/api/token-usage/overview"), ("GET", "/api/token-usage/1"),