Promote develop → main (2026-07-25 22:22 UTC) #16

Zusammengeführt
IntelSight_Admin hat 41 Commits von develop nach main 2026-07-26 00:22:10 +02:00 zusammengeführt
6 geänderte Dateien mit 120 neuen und 6 gelöschten Zeilen
Nur Änderungen aus Commit 16250cd8f5 werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -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."
```

Datei anzeigen

@@ -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))

Datei anzeigen

@@ -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)."""

Datei anzeigen

@@ -287,7 +287,7 @@
<div class="nav-tabs" id="sourceSubTabs">
<button class="nav-tab active" data-subtab="global-sources">Quellenliste</button>
<button class="nav-tab" data-subtab="aufgaben">Aufgaben <span class="sources-tab-badge" id="tasksPendingBadge">0</span></button>
<button class="nav-tab" data-subtab="x-scraper">X-Recherche-Konten</button>
<button class="nav-tab" data-subtab="x-scraper">Recherche-Zugänge</button>
</div>
<!-- Vereinte Quellenliste (Grund- + Kundenquellen) -->
@@ -355,8 +355,12 @@
<div id="aufgabenVerlauf"></div>
</div>
<!-- X-Recherche-Konten (Sub-Tab) -->
<!-- Recherche-Zugänge (Sub-Tab): X-Konten + Telegram-Session-Status -->
<div class="section" id="sub-x-scraper">
<div class="card" style="margin-bottom:16px;">
<div class="card-header"><h2>Telegram-Zugang</h2></div>
<div class="card-body" id="telegramStatusBox">Lade…</div>
</div>
<div class="action-bar">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span class="text-secondary" id="xScraperCount"></span>
@@ -972,7 +976,7 @@
<script src="/static/js/app.js?v=20260725v"></script>
<script src="/static/js/sources.js?v=20260725f"></script>
<script src="/static/js/aufgaben.js?v=20260725d"></script>
<script src="/static/js/x-scraper.js?v=20260725a"></script>
<script src="/static/js/x-scraper.js?v=20260725b"></script>
<script src="/static/js/audit.js?v=20260725a"></script>
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
</body>

Datei anzeigen

@@ -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 = '<tr><td colspan="6" class="text-muted">Lade...</td></tr>';
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 = '<span class="text-muted">Noch keine Statusmeldung vom Monitor vorhanden. '
+ 'Der Monitor meldet den Status beim Start und täglich um 04:30 Uhr.</span>';
return;
}
const stand = `<span class="text-secondary" style="margin-left:10px;font-size:12px;">Stand ${formatDateTime(st.updated_at)}</span>`;
if (st.ok) {
box.innerHTML = `<span class="badge badge-active">Session gültig</span> `
+ `<span style="margin-left:6px;">Verbunden${st.account ? " als " + esc(st.account) : ""}. `
+ `Die Telegram-Anmeldung läuft nicht periodisch ab, hier ist normalerweise nichts zu tun.</span>${stand}`;
} else {
box.innerHTML = `<span class="badge badge-inactive">Problem</span> `
+ `<span style="margin-left:6px;">${esc(st.error || "Session nicht autorisiert")}. `
+ `Die Telegram-Recherche ist pausiert, bis auf dem Server neu eingeloggt wurde.</span>${stand}`;
}
} catch (err) {
box.innerHTML = '<span class="text-muted">Status konnte nicht geladen werden. ' + esc(err.message || "") + '</span>';
}
}
function renderXScraperAccounts(list) {
const tbody = document.getElementById("xScraperTable");
const cnt = document.getElementById("xScraperCount");

Datei anzeigen

@@ -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"),