feat(x): X (Twitter) als Bezugsquelle pro Lage

X-Accounts werden analog zu Telegram als Quelle (source_type=x_account)
konfiguriert und pro Lage ueber include_x zugeschaltet. Der Scraper
(feeds/x_parser.py, twscrape) liest Account-Timelines, optional ueber
einen HTTP-Proxy mit Fallback auf direkten Abruf ueber die Server-IP.

- DB-Migration include_x, Pydantic-Modelle, incidents-Router
- Orchestrator-X-Pipeline plus Haiku-Account-Vorselektion
- sources-Router /x/validate, x_account-Typ in Stats und Frontend
- Lage-Einstellungen: X-Toggle neben international und Telegram
- twscrape als Abhaengigkeit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-05-22 06:52:19 +00:00
Ursprung f1200743e6
Commit 9c50439785
12 geänderte Dateien mit 547 neuen und 10 gelöschten Zeilen

Datei anzeigen

@@ -21,7 +21,7 @@ router = APIRouter(prefix="/api/incidents", tags=["incidents"])
INCIDENT_UPDATE_COLUMNS = {
"title", "description", "type", "status", "refresh_mode",
"refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "visibility",
"refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "include_x", "visibility",
}
@@ -89,7 +89,7 @@ async def list_incidents(
query = (
"SELECT id, title, description, type, status, refresh_mode, refresh_interval, "
"refresh_start_time, retention_days, visibility, "
"international_sources, include_telegram, created_by, created_at, updated_at, "
"international_sources, include_telegram, include_x, created_by, created_at, updated_at, "
"CASE WHEN summary IS NOT NULL AND summary != '' THEN 1 ELSE 0 END AS has_summary "
"FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
)
@@ -120,9 +120,9 @@ async def create_incident(
now = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')
cursor = await db.execute(
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
refresh_start_time, retention_days, international_sources, include_telegram, visibility,
refresh_start_time, retention_days, international_sources, include_telegram, include_x, visibility,
tenant_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
data.title,
data.description,
@@ -133,6 +133,7 @@ async def create_incident(
data.retention_days,
1 if data.international_sources else 0,
1 if data.include_telegram else 0,
1 if data.include_x else 0,
data.visibility,
tenant_id,
current_user["id"],
@@ -385,7 +386,7 @@ async def update_incident(
for field, value in data.model_dump(exclude_none=True).items():
if field not in INCIDENT_UPDATE_COLUMNS:
continue
if field in ("international_sources", "include_telegram"):
if field in ("international_sources", "include_telegram", "include_x"):
updates[field] = 1 if value else 0
else:
updates[field] = value