diff --git a/geoip_dashboard.py b/geoip_dashboard.py
new file mode 100644
index 0000000..f1c0366
--- /dev/null
+++ b/geoip_dashboard.py
@@ -0,0 +1,1375 @@
+#!/usr/bin/env python3
+"""
+GeoIP Dashboard v2.2.0 - WebSocket Real-Time Dashboard
+
+ÄNDERUNG: Keine SQLite mehr für Echtzeit-Daten!
+- Alle Agent/Shop-Daten im Memory
+- DB nur für: Passwort, Tokens, Sessions
+- Kein Locking, keine Timeouts
+
+v2.2.0: In-Memory Storage für maximale Performance
+"""
+
+import os
+import sys
+import json
+import secrets
+import hashlib
+import asyncio
+import subprocess
+from datetime import datetime, timedelta, timezone
+from typing import Optional, Dict, Any, List, Set
+from dataclasses import dataclass, field
+from collections import deque
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException, Form
+from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
+from starlette.middleware.sessions import SessionMiddleware
+import uvicorn
+
+# =============================================================================
+# VERSION & CONFIG
+# =============================================================================
+VERSION = "2.3.0"
+
+DATA_DIR = "/var/lib/geoip-dashboard"
+SSL_DIR = "/var/lib/geoip-dashboard/ssl"
+SSL_CERT = "/var/lib/geoip-dashboard/ssl/server.crt"
+SSL_KEY = "/var/lib/geoip-dashboard/ssl/server.key"
+CONFIG_FILE = "/var/lib/geoip-dashboard/config.json"
+TOKENS_FILE = "/var/lib/geoip-dashboard/tokens.json"
+
+AGENT_TIMEOUT = 120
+HISTORY_MAX_POINTS = 1000 # Max Datenpunkte pro Shop
+
+SECRET_KEY = os.environ.get("DASHBOARD_SECRET", secrets.token_hex(32))
+
+
+# =============================================================================
+# UTILITY
+# =============================================================================
+def utc_now() -> datetime:
+ return datetime.now(timezone.utc)
+
+def utc_now_str() -> str:
+ return utc_now().strftime("%Y-%m-%d %H:%M:%S")
+
+def utc_now_iso() -> str:
+ return utc_now().strftime('%Y-%m-%dT%H:%M:%SZ')
+
+
+# =============================================================================
+# IN-MEMORY DATA STORE
+# =============================================================================
+@dataclass
+class AgentData:
+ id: str
+ hostname: str
+ version: str = ""
+ os_info: Dict = field(default_factory=dict)
+ first_seen: str = ""
+ last_seen: str = ""
+ approved: bool = False
+ token: str = ""
+ status: str = "pending"
+ load_1m: float = 0.0
+ load_5m: float = 0.0
+ memory_percent: float = 0.0
+ uptime_seconds: int = 0
+ shops_total: int = 0
+ shops_active: int = 0
+
+
+@dataclass
+class ShopData:
+ domain: str
+ agent_id: str
+ agent_hostname: str = ""
+ status: str = "inactive"
+ mode: str = ""
+ geo_region: str = ""
+ rate_limit: int = 0
+ ban_duration: int = 0
+ link11: bool = False
+ link11_ip: str = ""
+ activated: str = ""
+ runtime_minutes: float = 0.0
+ # Stats
+ log_entries: int = 0
+ total_bans: int = 0
+ active_bans: int = 0
+ banned_bots: List[str] = field(default_factory=list)
+ req_per_min: float = 0.0
+ unique_ips: int = 0
+ unique_bots: int = 0
+ top_bots: Dict[str, int] = field(default_factory=dict)
+ top_ips: Dict[str, int] = field(default_factory=dict)
+ # History für Graph - jetzt pro Bot
+ history: deque = field(default_factory=lambda: deque(maxlen=HISTORY_MAX_POINTS))
+ bot_history: Dict[str, deque] = field(default_factory=dict) # bot_name -> deque of {timestamp, count}
+
+
+class DataStore:
+ """In-Memory Datenspeicher - Thread-safe durch asyncio."""
+
+ def __init__(self):
+ self.agents: Dict[str, AgentData] = {}
+ self.shops: Dict[str, ShopData] = {}
+ self.sessions: Dict[str, Dict] = {} # token -> {username, expires}
+ self._password_hash: Optional[str] = None
+ self._tokens: Dict[str, str] = {} # agent_id -> token
+ self._load_persistent_data()
+
+ def _load_persistent_data(self):
+ """Lädt persistente Daten (Passwort, Tokens)."""
+ os.makedirs(DATA_DIR, exist_ok=True)
+
+ # Config laden (Passwort)
+ if os.path.exists(CONFIG_FILE):
+ try:
+ with open(CONFIG_FILE, 'r') as f:
+ config = json.load(f)
+ self._password_hash = config.get('password_hash')
+ except:
+ pass
+
+ # Tokens laden
+ if os.path.exists(TOKENS_FILE):
+ try:
+ with open(TOKENS_FILE, 'r') as f:
+ self._tokens = json.load(f)
+ except:
+ pass
+
+ def _save_config(self):
+ """Speichert Config."""
+ with open(CONFIG_FILE, 'w') as f:
+ json.dump({'password_hash': self._password_hash}, f)
+
+ def _save_tokens(self):
+ """Speichert Tokens."""
+ with open(TOKENS_FILE, 'w') as f:
+ json.dump(self._tokens, f)
+
+ # === Password ===
+ def get_password_hash(self) -> Optional[str]:
+ return self._password_hash
+
+ def set_password(self, password: str):
+ self._password_hash = hashlib.sha256(password.encode()).hexdigest()
+ self._save_config()
+
+ def verify_password(self, password: str) -> bool:
+ if not self._password_hash:
+ return False
+ return hashlib.sha256(password.encode()).hexdigest() == self._password_hash
+
+ # === Sessions ===
+ def create_session(self, username: str) -> str:
+ token = secrets.token_hex(32)
+ expires = (utc_now() + timedelta(hours=24)).isoformat()
+ self.sessions[token] = {'username': username, 'expires': expires}
+ return token
+
+ def verify_session(self, token: str) -> Optional[str]:
+ if not token or token not in self.sessions:
+ return None
+ session = self.sessions[token]
+ expires = datetime.fromisoformat(session['expires'])
+ if utc_now() > expires:
+ del self.sessions[token]
+ return None
+ return session['username']
+
+ def delete_session(self, token: str):
+ self.sessions.pop(token, None)
+
+ # === Agent Tokens ===
+ def get_agent_token(self, agent_id: str) -> Optional[str]:
+ return self._tokens.get(agent_id)
+
+ def set_agent_token(self, agent_id: str, token: str):
+ self._tokens[agent_id] = token
+ self._save_tokens()
+
+ # === Agents ===
+ def get_or_create_agent(self, agent_id: str, hostname: str) -> AgentData:
+ if agent_id not in self.agents:
+ self.agents[agent_id] = AgentData(
+ id=agent_id,
+ hostname=hostname,
+ first_seen=utc_now_str()
+ )
+ # Prüfe ob Token existiert
+ if agent_id in self._tokens:
+ self.agents[agent_id].approved = True
+ self.agents[agent_id].token = self._tokens[agent_id]
+ self.agents[agent_id].status = 'online'
+ return self.agents[agent_id]
+
+ def get_agent(self, agent_id: str) -> Optional[AgentData]:
+ return self.agents.get(agent_id)
+
+ def get_all_agents(self) -> List[Dict]:
+ result = []
+ for agent in self.agents.values():
+ # Status prüfen
+ status = agent.status
+ if status == 'online' and agent.last_seen:
+ try:
+ last = datetime.strptime(agent.last_seen, "%Y-%m-%d %H:%M:%S")
+ if (utc_now().replace(tzinfo=None) - last).total_seconds() > AGENT_TIMEOUT:
+ status = 'offline'
+ except:
+ pass
+
+ result.append({
+ 'id': agent.id,
+ 'hostname': agent.hostname,
+ 'version': agent.version,
+ 'status': status,
+ 'approved': agent.approved,
+ 'first_seen': agent.first_seen,
+ 'last_seen': agent.last_seen,
+ 'load_1m': agent.load_1m,
+ 'memory_percent': agent.memory_percent,
+ 'shops_total': agent.shops_total,
+ 'shops_active': agent.shops_active
+ })
+ return result
+
+ # === Shops ===
+ def update_shop(self, agent_id: str, agent_hostname: str, shop_data: Dict) -> ShopData:
+ domain = shop_data.get('domain')
+
+ if domain not in self.shops:
+ self.shops[domain] = ShopData(domain=domain, agent_id=agent_id)
+
+ shop = self.shops[domain]
+ shop.agent_id = agent_id
+ shop.agent_hostname = agent_hostname
+ shop.status = shop_data.get('status', 'inactive')
+ shop.mode = shop_data.get('mode', '')
+ shop.geo_region = shop_data.get('geo_region', '')
+ shop.rate_limit = shop_data.get('rate_limit', 0)
+ shop.ban_duration = shop_data.get('ban_duration', 0)
+ shop.link11 = bool(shop_data.get('link11'))
+ shop.link11_ip = shop_data.get('link11_ip', '')
+ shop.activated = shop_data.get('activated', '')
+ shop.runtime_minutes = shop_data.get('runtime_minutes', 0)
+
+ # Stats
+ stats = shop_data.get('stats', {})
+ if stats:
+ shop.log_entries = stats.get('log_entries', 0)
+ shop.total_bans = stats.get('total_bans', 0)
+ shop.active_bans = stats.get('active_bans', 0)
+ shop.banned_bots = stats.get('banned_bots', [])
+ shop.req_per_min = stats.get('req_per_min', 0)
+ shop.unique_ips = stats.get('unique_ips', 0)
+ shop.unique_bots = stats.get('unique_bots', 0)
+ shop.top_bots = stats.get('top_bots', {})
+ shop.top_ips = stats.get('top_ips', {})
+
+ # History für Graph
+ shop.history.append({
+ 'timestamp': utc_now_str(),
+ 'req_per_min': shop.req_per_min,
+ 'active_bans': shop.active_bans
+ })
+
+ return shop
+
+ def update_shop_stats(self, domain: str, stats: Dict):
+ if domain not in self.shops:
+ return
+
+ shop = self.shops[domain]
+ shop.log_entries = stats.get('log_entries', shop.log_entries)
+ shop.total_bans = stats.get('total_bans', shop.total_bans)
+ shop.active_bans = stats.get('active_bans', shop.active_bans)
+ shop.banned_bots = stats.get('banned_bots', shop.banned_bots)
+ shop.req_per_min = stats.get('req_per_min', shop.req_per_min)
+ shop.unique_ips = stats.get('unique_ips', shop.unique_ips)
+ shop.unique_bots = stats.get('unique_bots', shop.unique_bots)
+ shop.top_bots = stats.get('top_bots', shop.top_bots)
+ shop.top_ips = stats.get('top_ips', shop.top_ips)
+
+ timestamp = utc_now_str()
+
+ # Gesamt-History
+ shop.history.append({
+ 'timestamp': timestamp,
+ 'req_per_min': shop.req_per_min,
+ 'active_bans': shop.active_bans
+ })
+
+ # Bot-History aktualisieren
+ top_bots = stats.get('top_bots', {})
+ for bot_name, count in top_bots.items():
+ if bot_name not in shop.bot_history:
+ shop.bot_history[bot_name] = deque(maxlen=HISTORY_MAX_POINTS)
+ shop.bot_history[bot_name].append({
+ 'timestamp': timestamp,
+ 'count': count
+ })
+
+ def get_shop(self, domain: str) -> Optional[ShopData]:
+ return self.shops.get(domain)
+
+ def get_all_shops(self) -> List[Dict]:
+ result = []
+ for shop in self.shops.values():
+ result.append({
+ 'domain': shop.domain,
+ 'agent_id': shop.agent_id,
+ 'agent_hostname': shop.agent_hostname,
+ 'status': shop.status,
+ 'mode': shop.mode,
+ 'geo_region': shop.geo_region,
+ 'rate_limit': shop.rate_limit,
+ 'ban_duration': shop.ban_duration,
+ 'link11': shop.link11,
+ 'link11_ip': shop.link11_ip,
+ 'activated': shop.activated,
+ 'runtime_minutes': shop.runtime_minutes,
+ 'stats': {
+ 'log_entries': shop.log_entries,
+ 'total_bans': shop.total_bans,
+ 'active_bans': shop.active_bans,
+ 'banned_bots': shop.banned_bots,
+ 'req_per_min': shop.req_per_min,
+ 'unique_ips': shop.unique_ips,
+ 'unique_bots': shop.unique_bots,
+ 'top_bots': shop.top_bots,
+ 'top_ips': shop.top_ips
+ }
+ })
+ return result
+
+ def get_shop_history(self, domain: str) -> Dict:
+ shop = self.shops.get(domain)
+ if not shop:
+ return {'history': [], 'bot_history': {}}
+
+ # Bot-History in JSON-serialisierbares Format
+ bot_history = {}
+ for bot_name, history in shop.bot_history.items():
+ bot_history[bot_name] = list(history)
+
+ return {
+ 'history': list(shop.history),
+ 'bot_history': bot_history
+ }
+
+ def get_top_shops(self, limit: int = 10, sort_by: str = 'req_per_min') -> List[Dict]:
+ """Gibt Top Shops sortiert nach req_per_min oder active_bans zurück."""
+ shops_list = []
+ for shop in self.shops.values():
+ shops_list.append({
+ 'domain': shop.domain,
+ 'agent_hostname': shop.agent_hostname,
+ 'status': shop.status,
+ 'req_per_min': shop.req_per_min,
+ 'active_bans': shop.active_bans,
+ 'link11': shop.link11
+ })
+
+ # Sortieren
+ if sort_by == 'active_bans':
+ shops_list.sort(key=lambda x: x['active_bans'], reverse=True)
+ else:
+ shops_list.sort(key=lambda x: x['req_per_min'], reverse=True)
+
+ if limit:
+ return shops_list[:limit]
+ return shops_list
+
+ def get_stats(self) -> Dict:
+ agents_online = sum(1 for a in self.agents.values()
+ if a.approved and a.status == 'online')
+ agents_pending = sum(1 for a in self.agents.values() if not a.approved)
+
+ shops_active = sum(1 for s in self.shops.values() if s.status == 'active')
+ shops_total = len(self.shops)
+ shops_link11 = sum(1 for s in self.shops.values() if s.link11)
+ shops_direct = shops_total - shops_link11
+
+ req_per_min = sum(s.req_per_min for s in self.shops.values())
+ active_bans = sum(s.active_bans for s in self.shops.values())
+
+ return {
+ 'agents_online': agents_online,
+ 'agents_pending': agents_pending,
+ 'shops_active': shops_active,
+ 'shops_total': shops_total,
+ 'shops_link11': shops_link11,
+ 'shops_direct': shops_direct,
+ 'req_per_min': round(req_per_min, 1),
+ 'active_bans': active_bans
+ }
+
+
+# Global Data Store
+store = DataStore()
+
+
+# =============================================================================
+# SSL
+# =============================================================================
+def generate_ssl_certificate():
+ os.makedirs(SSL_DIR, exist_ok=True)
+
+ if os.path.exists(SSL_CERT) and os.path.exists(SSL_KEY):
+ return
+
+ print("🔐 Generiere SSL-Zertifikat...")
+
+ try:
+ subprocess.run([
+ 'openssl', 'req', '-x509', '-nodes',
+ '-days', '3650',
+ '-newkey', 'rsa:2048',
+ '-keyout', SSL_KEY,
+ '-out', SSL_CERT,
+ '-subj', '/CN=geoip-dashboard/O=GeoIP/C=DE'
+ ], check=True, capture_output=True)
+
+ os.chmod(SSL_KEY, 0o600)
+ os.chmod(SSL_CERT, 0o644)
+
+ print(f"✅ SSL-Zertifikat generiert: {SSL_CERT}")
+ except Exception as e:
+ print(f"❌ SSL Fehler: {e}")
+ raise
+
+
+# =============================================================================
+# CONNECTION MANAGER
+# =============================================================================
+class ConnectionManager:
+ def __init__(self):
+ self.agent_connections: Dict[str, WebSocket] = {}
+ self.browser_connections: Set[WebSocket] = set()
+ self.agent_hostnames: Dict[str, str] = {}
+
+ async def connect_agent(self, agent_id: str, hostname: str, websocket: WebSocket):
+ # Alte Verbindung schließen
+ if agent_id in self.agent_connections:
+ try:
+ await self.agent_connections[agent_id].close()
+ except:
+ pass
+
+ self.agent_connections[agent_id] = websocket
+ self.agent_hostnames[agent_id] = hostname
+ print(f"✅ Agent verbunden: {hostname}")
+
+ async def disconnect_agent(self, agent_id: str):
+ self.agent_connections.pop(agent_id, None)
+ hostname = self.agent_hostnames.pop(agent_id, "unknown")
+
+ # Status updaten
+ agent = store.get_agent(agent_id)
+ if agent:
+ agent.status = 'offline'
+ agent.last_seen = utc_now_str()
+
+ print(f"❌ Agent getrennt: {hostname}")
+
+ await self.broadcast_to_browsers({
+ 'type': 'agent.offline',
+ 'data': {'agent_id': agent_id, 'hostname': hostname}
+ })
+
+ async def connect_browser(self, websocket: WebSocket):
+ self.browser_connections.add(websocket)
+ print(f"🌐 Browser verbunden (Total: {len(self.browser_connections)})")
+
+ async def disconnect_browser(self, websocket: WebSocket):
+ self.browser_connections.discard(websocket)
+ print(f"🌐 Browser getrennt (Total: {len(self.browser_connections)})")
+
+ async def send_to_agent(self, agent_id: str, message: Dict):
+ ws = self.agent_connections.get(agent_id)
+ if ws:
+ try:
+ await ws.send_json(message)
+ except Exception as e:
+ print(f"Send to agent error: {e}")
+
+ async def broadcast_to_browsers(self, message: Dict):
+ dead = set()
+ for ws in self.browser_connections:
+ try:
+ await ws.send_json(message)
+ except:
+ dead.add(ws)
+ self.browser_connections -= dead
+
+ def get_agent_for_shop(self, domain: str) -> Optional[str]:
+ shop = store.get_shop(domain)
+ return shop.agent_id if shop else None
+
+ def is_agent_connected(self, agent_id: str) -> bool:
+ return agent_id in self.agent_connections
+
+
+manager = ConnectionManager()
+
+
+# =============================================================================
+# FASTAPI APP
+# =============================================================================
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ generate_ssl_certificate()
+ yield
+
+
+app = FastAPI(title="GeoIP Dashboard", version=VERSION, lifespan=lifespan)
+app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="geoip_session", max_age=86400)
+
+
+# =============================================================================
+# AUTH HELPERS
+# =============================================================================
+async def get_current_user(request: Request) -> Optional[str]:
+ token = request.session.get("token")
+ return store.verify_session(token)
+
+
+# =============================================================================
+# WEBSOCKET: AGENT
+# =============================================================================
+@app.websocket("/ws/agent")
+async def agent_websocket(websocket: WebSocket):
+ await websocket.accept()
+ agent_id = None
+
+ try:
+ async for message in websocket.iter_text():
+ try:
+ data = json.loads(message)
+ event_type = data.get('type')
+ event_data = data.get('data', {})
+
+ if event_type == 'agent.connect':
+ agent_id = event_data.get('agent_id')
+ hostname = event_data.get('hostname')
+ token = event_data.get('token')
+ version = event_data.get('version', '')
+ os_info = event_data.get('os_info', {})
+ shops_summary = event_data.get('shops_summary', {})
+
+ # Agent registrieren
+ agent = store.get_or_create_agent(agent_id, hostname)
+ agent.hostname = hostname
+ agent.version = version
+ agent.os_info = os_info
+ agent.last_seen = utc_now_str()
+ agent.shops_total = shops_summary.get('total', 0)
+ agent.shops_active = shops_summary.get('active', 0)
+
+ # Token prüfen
+ stored_token = store.get_agent_token(agent_id)
+ if stored_token and token == stored_token:
+ agent.approved = True
+ agent.token = stored_token
+ agent.status = 'online'
+
+ await manager.connect_agent(agent_id, hostname, websocket)
+
+ # Browser informieren
+ await manager.broadcast_to_browsers({
+ 'type': 'agent.online' if agent.approved else 'agent.pending',
+ 'data': {
+ 'agent_id': agent_id,
+ 'hostname': hostname,
+ 'version': version,
+ 'status': agent.status,
+ 'approved': agent.approved,
+ 'shops_total': shops_summary.get('total', 0),
+ 'shops_active': shops_summary.get('active', 0)
+ }
+ })
+
+ # Token senden wenn approved
+ if agent.approved:
+ await websocket.send_json({
+ 'type': 'auth.approved',
+ 'data': {'token': agent.token}
+ })
+
+ elif event_type == 'agent.heartbeat':
+ if agent_id:
+ agent = store.get_agent(agent_id)
+ if agent:
+ system = event_data.get('system', {})
+ shops_summary = event_data.get('shops_summary', {})
+
+ agent.last_seen = utc_now_str()
+ agent.load_1m = system.get('load_1m', 0)
+ agent.load_5m = system.get('load_5m', 0)
+ agent.memory_percent = system.get('memory_percent', 0)
+ agent.uptime_seconds = system.get('uptime_seconds', 0)
+ agent.shops_total = shops_summary.get('total', 0)
+ agent.shops_active = shops_summary.get('active', 0)
+
+ await manager.broadcast_to_browsers({
+ 'type': 'agent.update',
+ 'data': {
+ 'agent_id': agent_id,
+ 'hostname': agent.hostname,
+ 'system': system,
+ 'shops_summary': shops_summary
+ }
+ })
+
+ elif event_type == 'shop.full_update':
+ if agent_id:
+ agent = store.get_agent(agent_id)
+ hostname = agent.hostname if agent else ''
+ shops = event_data.get('shops', [])
+
+ for shop_data in shops:
+ store.update_shop(agent_id, hostname, shop_data)
+
+ await manager.broadcast_to_browsers({
+ 'type': 'shop.full_update',
+ 'data': {
+ 'agent_id': agent_id,
+ 'hostname': hostname,
+ 'shops': shops
+ }
+ })
+
+ elif event_type == 'shop.stats':
+ if agent_id:
+ domain = event_data.get('domain')
+ stats = event_data.get('stats', {})
+
+ store.update_shop_stats(domain, stats)
+
+ await manager.broadcast_to_browsers({
+ 'type': 'shop.stats',
+ 'data': {'domain': domain, 'stats': stats}
+ })
+
+ elif event_type == 'log.entry':
+ await manager.broadcast_to_browsers({
+ 'type': 'log.entry',
+ 'data': event_data
+ })
+
+ elif event_type == 'bot.banned':
+ await manager.broadcast_to_browsers({
+ 'type': 'bot.banned',
+ 'data': event_data
+ })
+
+ elif event_type == 'command.result':
+ await manager.broadcast_to_browsers({
+ 'type': 'command.result',
+ 'data': event_data
+ })
+
+ except json.JSONDecodeError:
+ pass
+ except Exception as e:
+ print(f"Agent message error: {e}")
+
+ except WebSocketDisconnect:
+ pass
+ except Exception as e:
+ print(f"Agent WebSocket error: {e}")
+ finally:
+ if agent_id:
+ await manager.disconnect_agent(agent_id)
+
+
+# =============================================================================
+# WEBSOCKET: BROWSER
+# =============================================================================
+@app.websocket("/ws/dashboard")
+async def dashboard_websocket(websocket: WebSocket):
+ await websocket.accept()
+ await manager.connect_browser(websocket)
+
+ try:
+ # Initial state senden
+ await websocket.send_json({
+ 'type': 'initial_state',
+ 'data': {
+ 'agents': store.get_all_agents(),
+ 'shops': store.get_all_shops(),
+ 'stats': store.get_stats()
+ }
+ })
+
+ async for message in websocket.iter_text():
+ try:
+ data = json.loads(message)
+ event_type = data.get('type')
+ event_data = data.get('data', {})
+
+ if event_type == 'log.subscribe':
+ domain = event_data.get('shop')
+ agent_id = manager.get_agent_for_shop(domain)
+ if agent_id:
+ await manager.send_to_agent(agent_id, {
+ 'type': 'log.subscribe',
+ 'data': {'shop': domain}
+ })
+
+ elif event_type == 'log.unsubscribe':
+ domain = event_data.get('shop')
+ agent_id = manager.get_agent_for_shop(domain)
+ if agent_id:
+ await manager.send_to_agent(agent_id, {
+ 'type': 'log.unsubscribe',
+ 'data': {'shop': domain}
+ })
+
+ elif event_type == 'get_shop_history':
+ domain = event_data.get('domain')
+ data = store.get_shop_history(domain)
+ await websocket.send_json({
+ 'type': 'shop_history',
+ 'data': {'domain': domain, **data}
+ })
+
+ elif event_type == 'get_top_shops':
+ sort_by = event_data.get('sort_by', 'req_per_min')
+ limit = event_data.get('limit', 10)
+ shops = store.get_top_shops(limit=limit, sort_by=sort_by)
+ await websocket.send_json({
+ 'type': 'top_shops',
+ 'data': {'shops': shops, 'sort_by': sort_by}
+ })
+
+ elif event_type == 'get_all_shops_sorted':
+ sort_by = event_data.get('sort_by', 'req_per_min')
+ shops = store.get_top_shops(limit=None, sort_by=sort_by)
+ await websocket.send_json({
+ 'type': 'all_shops_sorted',
+ 'data': {'shops': shops, 'sort_by': sort_by}
+ })
+
+ elif event_type == 'refresh':
+ await websocket.send_json({
+ 'type': 'refresh',
+ 'data': {
+ 'agents': store.get_all_agents(),
+ 'shops': store.get_all_shops(),
+ 'stats': store.get_stats()
+ }
+ })
+
+ except Exception as e:
+ print(f"Browser message error: {e}")
+
+ except WebSocketDisconnect:
+ pass
+ except Exception as e:
+ print(f"Browser WebSocket error: {e}")
+ finally:
+ await manager.disconnect_browser(websocket)
+
+
+# =============================================================================
+# HTTP ENDPOINTS
+# =============================================================================
+@app.get("/", response_class=HTMLResponse)
+async def root(request: Request):
+ user = await get_current_user(request)
+ if not store.get_password_hash():
+ return get_setup_html()
+ if not user:
+ return get_login_html()
+ return get_dashboard_html()
+
+
+@app.post("/setup")
+async def setup(request: Request, password: str = Form(...), confirm: str = Form(...)):
+ if store.get_password_hash():
+ raise HTTPException(400, "Passwort bereits gesetzt")
+ if password != confirm:
+ return HTMLResponse(get_setup_html("Passwörter stimmen nicht überein"))
+ if len(password) < 8:
+ return HTMLResponse(get_setup_html("Passwort muss mindestens 8 Zeichen haben"))
+ store.set_password(password)
+ token = store.create_session("admin")
+ response = RedirectResponse(url="/", status_code=303)
+ request.session["token"] = token
+ return response
+
+
+@app.post("/login")
+async def login(request: Request, password: str = Form(...)):
+ if not store.verify_password(password):
+ return HTMLResponse(get_login_html("Falsches Passwort"))
+ token = store.create_session("admin")
+ response = RedirectResponse(url="/", status_code=303)
+ request.session["token"] = token
+ return response
+
+
+@app.get("/logout")
+async def logout(request: Request):
+ token = request.session.get("token")
+ if token:
+ store.delete_session(token)
+ request.session.clear()
+ return RedirectResponse(url="/")
+
+
+@app.post("/api/agents/{agent_id}/approve")
+async def approve_agent(agent_id: str, request: Request):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ agent = store.get_agent(agent_id)
+ if not agent:
+ raise HTTPException(404, "Agent nicht gefunden")
+
+ # Token generieren
+ token = secrets.token_hex(32)
+ agent.approved = True
+ agent.token = token
+ agent.status = 'online'
+ store.set_agent_token(agent_id, token)
+
+ # Token an Agent senden
+ if manager.is_agent_connected(agent_id):
+ await manager.send_to_agent(agent_id, {
+ 'type': 'auth.approved',
+ 'data': {'token': token}
+ })
+
+ await manager.broadcast_to_browsers({
+ 'type': 'agent.approved',
+ 'data': {'agent_id': agent_id}
+ })
+
+ return {"success": True}
+
+
+@app.post("/api/shops/activate")
+async def activate_shop(
+ request: Request,
+ domain: str = Form(...),
+ mode: str = Form(...),
+ geo_region: str = Form("dach"),
+ rate_limit: int = Form(30),
+ ban_duration: int = Form(300)
+):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ agent_id = manager.get_agent_for_shop(domain)
+ if not agent_id or not manager.is_agent_connected(agent_id):
+ return JSONResponse({"success": False, "error": "Agent nicht verbunden"})
+
+ command_id = secrets.token_hex(8)
+ await manager.send_to_agent(agent_id, {
+ 'type': 'command.activate',
+ 'data': {
+ 'command_id': command_id,
+ 'shop': domain,
+ 'mode': mode,
+ 'geo_region': geo_region if mode == 'geoip' else None,
+ 'rate_limit': rate_limit if mode == 'bot' else None,
+ 'ban_duration': ban_duration if mode == 'bot' else None
+ }
+ })
+
+ return {"success": True, "command_id": command_id}
+
+
+@app.post("/api/shops/deactivate")
+async def deactivate_shop(request: Request, domain: str = Form(...)):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ agent_id = manager.get_agent_for_shop(domain)
+ if not agent_id or not manager.is_agent_connected(agent_id):
+ return JSONResponse({"success": False, "error": "Agent nicht verbunden"})
+
+ command_id = secrets.token_hex(8)
+ await manager.send_to_agent(agent_id, {
+ 'type': 'command.deactivate',
+ 'data': {'command_id': command_id, 'shop': domain}
+ })
+
+ return {"success": True, "command_id": command_id}
+
+
+@app.post("/api/shops/bulk-activate")
+async def bulk_activate(
+ request: Request,
+ mode: str = Form(...),
+ geo_region: str = Form("dach"),
+ rate_limit: int = Form(30),
+ ban_duration: int = Form(300),
+ filter_type: str = Form("all")
+):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ activated = 0
+ shops = store.get_all_shops()
+
+ for shop in shops:
+ if shop['status'] == 'active':
+ continue
+
+ if filter_type == 'direct' and shop['link11']:
+ continue
+ if filter_type == 'link11' and not shop['link11']:
+ continue
+
+ agent_id = shop.get('agent_id')
+ if not agent_id or not manager.is_agent_connected(agent_id):
+ continue
+
+ command_id = secrets.token_hex(8)
+ await manager.send_to_agent(agent_id, {
+ 'type': 'command.activate',
+ 'data': {
+ 'command_id': command_id,
+ 'shop': shop['domain'],
+ 'mode': mode,
+ 'geo_region': geo_region if mode == 'geoip' else None,
+ 'rate_limit': rate_limit if mode == 'bot' else None,
+ 'ban_duration': ban_duration if mode == 'bot' else None
+ }
+ })
+ activated += 1
+
+ # Kleine Pause um nicht zu überlasten
+ if activated % 5 == 0:
+ await asyncio.sleep(0.1)
+
+ return {"success": True, "activated": activated}
+
+
+@app.post("/api/shops/bulk-deactivate")
+async def bulk_deactivate(request: Request, filter_type: str = Form("all")):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ deactivated = 0
+ shops = store.get_all_shops()
+
+ for shop in shops:
+ if shop['status'] != 'active':
+ continue
+
+ if filter_type == 'direct' and shop['link11']:
+ continue
+ if filter_type == 'link11' and not shop['link11']:
+ continue
+
+ agent_id = shop.get('agent_id')
+ if not agent_id or not manager.is_agent_connected(agent_id):
+ continue
+
+ command_id = secrets.token_hex(8)
+ await manager.send_to_agent(agent_id, {
+ 'type': 'command.deactivate',
+ 'data': {'command_id': command_id, 'shop': shop['domain']}
+ })
+ deactivated += 1
+
+ if deactivated % 5 == 0:
+ await asyncio.sleep(0.1)
+
+ return {"success": True, "deactivated": deactivated}
+
+
+@app.post("/api/change-password")
+async def change_password(
+ request: Request,
+ current: str = Form(...),
+ new_pw: str = Form(...),
+ confirm: str = Form(...)
+):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ if not store.verify_password(current):
+ return {"success": False, "error": "Aktuelles Passwort falsch"}
+ if new_pw != confirm:
+ return {"success": False, "error": "Neue Passwörter stimmen nicht überein"}
+ if len(new_pw) < 8:
+ return {"success": False, "error": "Mindestens 8 Zeichen"}
+
+ store.set_password(new_pw)
+ return {"success": True}
+
+
+@app.get("/api/shop/{domain}/history")
+async def get_shop_history_api(domain: str, request: Request):
+ user = await get_current_user(request)
+ if not user:
+ raise HTTPException(401)
+
+ data = store.get_shop_history(domain)
+ return {"domain": domain, **data}
+
+
+# =============================================================================
+# HTML TEMPLATES
+# =============================================================================
+def get_setup_html(error: str = None) -> str:
+ error_html = f'
{error}
' if error else ''
+ return f'''
+
+
+
+
+ GeoIP Dashboard - Setup
+
+
+
+
+
🔐 Setup
+
Erstelle ein Admin-Passwort
+ {error_html}
+
+
+
+'''
+
+
+def get_login_html(error: str = None) -> str:
+ error_html = f'{error}
' if error else ''
+ return f'''
+
+
+
+
+ GeoIP Dashboard - Login
+
+
+
+
+
🌍 GeoIP Dashboard
+ {error_html}
+
+
+
+'''
+
+
+def get_dashboard_html() -> str:
+ return '''
+
+
+
+
+ GeoIP Dashboard v2.3
+
+
+
+
+ 🌍 GeoIP Dashboard v2.3
+
+
+
+
+
+
+ ⚡ Massenaktionen:
+
+
+
+
+
+
| Status | Hostname | Shops | Load | Memory | Zuletzt | Aktionen |
|---|
+
+
+
+
| Status | Domain | Server | Modus | Req/min | Bans | Laufzeit | Aktionen |
|---|
+
+
+
+
| Status | Domain | Server | Modus | Req/min | Bans | Laufzeit | Aktionen |
|---|
+
+
+
+
+
+
+
+ 📊 Alle Shops
| # | Domain | Server | Status | Req/min | Bans | Typ |
|---|
+ -
📈 Bot-Aktivität über Zeit
+
+
+
+'''
+
+
+def create_systemd_service():
+ service = """[Unit]
+Description=GeoIP Dashboard v2.3 (WebSocket)
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/python3 /opt/geoip-dashboard/geoip_dashboard.py
+Restart=always
+RestartSec=10
+User=root
+Environment=PYTHONUNBUFFERED=1
+
+[Install]
+WantedBy=multi-user.target
+"""
+ with open("/etc/systemd/system/geoip-dashboard.service", 'w') as f:
+ f.write(service)
+ print("✅ Service erstellt")
+ print(" systemctl daemon-reload && systemctl enable --now geoip-dashboard")
+
+
+def main():
+ import argparse
+ parser = argparse.ArgumentParser(description=f"GeoIP Dashboard v{VERSION}")
+ parser.add_argument("--host", default="0.0.0.0")
+ parser.add_argument("--port", type=int, default=8000)
+ parser.add_argument("--no-ssl", action="store_true")
+ parser.add_argument("--install-service", action="store_true")
+ args = parser.parse_args()
+
+ if args.install_service:
+ create_systemd_service()
+ return
+
+ os.makedirs(DATA_DIR, exist_ok=True)
+
+ print("=" * 60)
+ print(f"🌍 GeoIP Dashboard v{VERSION} (In-Memory)")
+ print("=" * 60)
+ print(f"Host: {args.host}:{args.port}")
+ print(f"SSL: {'Nein' if args.no_ssl else 'Ja'}")
+ print("=" * 60)
+
+ ssl_config = {}
+ if not args.no_ssl:
+ generate_ssl_certificate()
+ ssl_config = {"ssl_certfile": SSL_CERT, "ssl_keyfile": SSL_KEY}
+
+ uvicorn.run(app, host=args.host, port=args.port, **ssl_config, log_level="info")
+
+
+if __name__ == "__main__":
+ main()