feat(zugaenge): Telegram-Session-Status im Reiter Recherche-Zugaenge

Der Reiter X-Recherche-Konten heisst jetzt Recherche-Zugaenge und zeigt
oben eine Telegram-Karte: Session gueltig/Problem, verbunden als, Stand.
Die Daten meldet der Monitor (Commit 7dae63e) in die geteilte Tabelle
system_status, das Portal liest nur an (GET /api/x-scraper/telegram-status,
tolerant wenn Tabelle/Eintrag fehlt). Migration fuer die
Portal-Staging-DB liegt bei. Promote-Reihenfolge: Monitor VOR Portal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-25 19:07:01 +00:00
Ursprung 14546f7c0a
Commit 16250cd8f5
6 geänderte Dateien mit 120 neuen und 6 gelöschten Zeilen

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");