From eee4b4de1ea6ff6eeba4454208ebfe084743cc81 Mon Sep 17 00:00:00 2001 From: UserIsMH Date: Thu, 19 Jun 2025 00:29:07 +0200 Subject: [PATCH] Navbar erweitert - Zwischenstand --- .claude/settings.local.json | 3 +- lizenzserver/services/auth/config.py | 15 + v2/.env | 4 +- v2/docker-compose.yaml | 145 ++++--- v2_adminpanel/app.py | 2 + v2_adminpanel/routes/monitoring_routes.py | 259 ++++++++++++ v2_adminpanel/templates/base.html | 34 +- .../templates/monitoring/alerts.html | 322 +++++++++++++++ .../templates/monitoring/analytics.html | 358 ++++++++++++++++ .../templates/monitoring/live_dashboard.html | 386 ++++++++++++++++++ .../templates/monitoring/system_status.html | 257 ++++++++++++ v2_nginx/nginx.conf | 38 +- 12 files changed, 1713 insertions(+), 110 deletions(-) create mode 100644 lizenzserver/services/auth/config.py create mode 100644 v2_adminpanel/routes/monitoring_routes.py create mode 100644 v2_adminpanel/templates/monitoring/alerts.html create mode 100644 v2_adminpanel/templates/monitoring/analytics.html create mode 100644 v2_adminpanel/templates/monitoring/live_dashboard.html create mode 100644 v2_adminpanel/templates/monitoring/system_status.html diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 087477f..c90728a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -71,7 +71,8 @@ "Bash(fi)", "Bash(done)", "Bash(docker compose:*)", - "Bash(true)" + "Bash(true)", + "Bash(git checkout:*)" ], "deny": [] } diff --git a/lizenzserver/services/auth/config.py b/lizenzserver/services/auth/config.py new file mode 100644 index 0000000..60da21f --- /dev/null +++ b/lizenzserver/services/auth/config.py @@ -0,0 +1,15 @@ +import os +from datetime import timedelta + +def get_config(): + """Get configuration from environment variables""" + return { + 'DATABASE_URL': os.getenv('DATABASE_URL', 'postgresql://postgres:password@postgres:5432/v2_adminpanel'), + 'REDIS_URL': os.getenv('REDIS_URL', 'redis://redis:6379/1'), + 'JWT_SECRET': os.getenv('JWT_SECRET', 'dev-secret-key'), + 'JWT_ALGORITHM': 'HS256', + 'ACCESS_TOKEN_EXPIRE_MINUTES': 30, + 'REFRESH_TOKEN_EXPIRE_DAYS': 7, + 'FLASK_ENV': os.getenv('FLASK_ENV', 'production'), + 'LOG_LEVEL': os.getenv('LOG_LEVEL', 'INFO'), + } \ No newline at end of file diff --git a/v2/.env b/v2/.env index 3a27e3f..d8dc562 100644 --- a/v2/.env +++ b/v2/.env @@ -18,8 +18,8 @@ ADMIN_PANEL_DOMAIN=admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com # ===================== OPTIONALE VARIABLEN ===================== -# JWT für API-Auth -# JWT_SECRET=geheimer_token_schlüssel +# JWT für API-Auth (WICHTIG: Für sichere Token-Verschlüsselung!) +JWT_SECRET=xY9ZmK2pL7nQ4wF6jH8vB3tG5aZ1dE7fR9hT2kM4nP6qS8uW0xC3yA5bD7eF9gH2jK4 # E-Mail Konfiguration (z. B. bei Ablaufwarnungen) # MAIL_SERVER=smtp.meinedomain.de diff --git a/v2/docker-compose.yaml b/v2/docker-compose.yaml index e0fa836..0715e2f 100644 --- a/v2/docker-compose.yaml +++ b/v2/docker-compose.yaml @@ -81,79 +81,79 @@ services: cpus: '2' memory: 4g - auth-service: - build: - context: ../lizenzserver/services/auth - container_name: auth-service - restart: always - # Port 5001 - nur intern erreichbar - env_file: .env - environment: - TZ: Europe/Berlin - DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel - REDIS_URL: redis://redis:6379/1 - JWT_SECRET: ${JWT_SECRET} - FLASK_ENV: production - depends_on: - - postgres - - redis - networks: - - internal_net - deploy: - resources: - limits: - cpus: '1' - memory: 1g + # auth-service: + # build: + # context: ../lizenzserver/services/auth + # container_name: auth-service + # restart: always + # # Port 5001 - nur intern erreichbar + # env_file: .env + # environment: + # TZ: Europe/Berlin + # DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel + # REDIS_URL: redis://redis:6379/1 + # JWT_SECRET: ${JWT_SECRET} + # FLASK_ENV: production + # depends_on: + # - postgres + # - redis + # networks: + # - internal_net + # deploy: + # resources: + # limits: + # cpus: '1' + # memory: 1g - analytics-service: - build: - context: ../v2_lizenzserver/services/analytics - container_name: analytics-service - restart: always - # Port 5003 - nur intern erreichbar - env_file: .env - environment: - TZ: Europe/Berlin - DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel - REDIS_URL: redis://redis:6379/2 - JWT_SECRET: ${JWT_SECRET} - FLASK_ENV: production - depends_on: - - postgres - - redis - - rabbitmq - networks: - - internal_net - deploy: - resources: - limits: - cpus: '1' - memory: 2g + # analytics-service: + # build: + # context: ../lizenzserver/services/analytics + # container_name: analytics-service + # restart: always + # # Port 5003 - nur intern erreichbar + # env_file: .env + # environment: + # TZ: Europe/Berlin + # DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel + # REDIS_URL: redis://redis:6379/2 + # JWT_SECRET: ${JWT_SECRET} + # FLASK_ENV: production + # depends_on: + # - postgres + # - redis + # - rabbitmq + # networks: + # - internal_net + # deploy: + # resources: + # limits: + # cpus: '1' + # memory: 2g - admin-api-service: - build: - context: ../v2_lizenzserver/services/admin - container_name: admin-api-service - restart: always - # Port 5004 - nur intern erreichbar - env_file: .env - environment: - TZ: Europe/Berlin - DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel - REDIS_URL: redis://redis:6379/3 - JWT_SECRET: ${JWT_SECRET} - FLASK_ENV: production - depends_on: - - postgres - - redis - - rabbitmq - networks: - - internal_net - deploy: - resources: - limits: - cpus: '1' - memory: 2g + # admin-api-service: + # build: + # context: ../lizenzserver/services/admin_api + # container_name: admin-api-service + # restart: always + # # Port 5004 - nur intern erreichbar + # env_file: .env + # environment: + # TZ: Europe/Berlin + # DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/v2_adminpanel + # REDIS_URL: redis://redis:6379/3 + # JWT_SECRET: ${JWT_SECRET} + # FLASK_ENV: production + # depends_on: + # - postgres + # - redis + # - rabbitmq + # networks: + # - internal_net + # deploy: + # resources: + # limits: + # cpus: '1' + # memory: 2g admin-panel: build: @@ -190,9 +190,6 @@ services: depends_on: - admin-panel - license-server - - auth-service - - analytics-service - - admin-api-service networks: - internal_net diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 2913c87..e690297 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -59,6 +59,7 @@ try: from routes.license_routes import license_bp from routes.resource_routes import resource_bp from routes.session_routes import session_bp + from routes.monitoring_routes import monitoring_bp print("All blueprints imported successfully!") except Exception as e: print(f"Blueprint import error: {str(e)}") @@ -75,6 +76,7 @@ app.register_blueprint(export_bp) app.register_blueprint(license_bp) app.register_blueprint(resource_bp) app.register_blueprint(session_bp) +app.register_blueprint(monitoring_bp) # Debug routes to test diff --git a/v2_adminpanel/routes/monitoring_routes.py b/v2_adminpanel/routes/monitoring_routes.py new file mode 100644 index 0000000..9bb2d42 --- /dev/null +++ b/v2_adminpanel/routes/monitoring_routes.py @@ -0,0 +1,259 @@ +from flask import Blueprint, render_template, jsonify, request, session +from functools import wraps +import psycopg2 +from psycopg2.extras import RealDictCursor +import os +import requests +from datetime import datetime, timedelta +import logging + +monitoring_bp = Blueprint('monitoring', __name__) +logger = logging.getLogger(__name__) + +# Database connection +def get_db_connection(): + return psycopg2.connect( + host=os.environ.get('POSTGRES_HOST', 'postgres'), + database=os.environ.get('POSTGRES_DB', 'v2_adminpanel'), + user=os.environ.get('POSTGRES_USER', 'postgres'), + password=os.environ.get('POSTGRES_PASSWORD', 'postgres') + ) + +# Login required decorator +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'logged_in' not in session: + return render_template('error.html', + error_message='Nicht autorisiert', + details='Sie müssen angemeldet sein, um diese Seite zu sehen.') + return f(*args, **kwargs) + return decorated_function + +@monitoring_bp.route('/live-dashboard') +@login_required +def live_dashboard(): + """Live Dashboard showing active customer sessions""" + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # Get active customer sessions (last 5 minutes) + cur.execute(""" + SELECT + l.id, + l.license_key, + c.company_name, + c.contact_person, + lh.hardware_id, + lh.ip_address, + lh.timestamp as last_activity, + lh.session_data, + COUNT(DISTINCT lh.hardware_id) OVER (PARTITION BY l.id) as active_devices + FROM license_heartbeats lh + JOIN licenses l ON l.id = lh.license_id + JOIN customers c ON c.id = l.customer_id + WHERE lh.timestamp > NOW() - INTERVAL '5 minutes' + AND l.is_active = true + ORDER BY lh.timestamp DESC + LIMIT 100 + """) + active_sessions = cur.fetchall() + + # Get session statistics + cur.execute(""" + SELECT + COUNT(DISTINCT license_id) as active_licenses, + COUNT(DISTINCT hardware_id) as active_devices, + COUNT(*) as total_heartbeats + FROM license_heartbeats + WHERE timestamp > NOW() - INTERVAL '5 minutes' + """) + stats = cur.fetchone() + + # Get validations per minute + cur.execute(""" + SELECT + DATE_TRUNC('minute', timestamp) as minute, + COUNT(*) as validations + FROM license_heartbeats + WHERE timestamp > NOW() - INTERVAL '60 minutes' + GROUP BY minute + ORDER BY minute DESC + LIMIT 60 + """) + validation_timeline = cur.fetchall() + + cur.close() + conn.close() + + return render_template('monitoring/live_dashboard.html', + active_sessions=active_sessions, + stats=stats, + validation_timeline=validation_timeline) + + except Exception as e: + logger.error(f"Error in live dashboard: {str(e)}") + return render_template('error.html', + error_message='Fehler beim Laden des Dashboards', + details=str(e)) + +@monitoring_bp.route('/system-status') +@login_required +def system_status(): + """System status showing service health""" + services = [] + + # Check each service + service_checks = [ + {'name': 'License Server', 'url': 'http://license-server:8443/health', 'port': 8443}, + {'name': 'Auth Service', 'url': 'http://auth-service:5001/health', 'port': 5001}, + {'name': 'Analytics Service', 'url': 'http://analytics-service:5003/health', 'port': 5003}, + {'name': 'Admin API Service', 'url': 'http://admin-api-service:5004/health', 'port': 5004}, + {'name': 'PostgreSQL', 'check': 'database'}, + {'name': 'Redis', 'url': 'http://redis:6379', 'check': 'redis'}, + ] + + for service in service_checks: + status = {'name': service['name'], 'status': 'unknown', 'response_time': None} + + try: + if service.get('check') == 'database': + # Check database + start = datetime.now() + conn = get_db_connection() + conn.close() + status['status'] = 'healthy' + status['response_time'] = (datetime.now() - start).total_seconds() * 1000 + elif service.get('url'): + # Check HTTP service + start = datetime.now() + response = requests.get(service['url'], timeout=2) + if response.status_code == 200: + status['status'] = 'healthy' + else: + status['status'] = 'unhealthy' + status['response_time'] = (datetime.now() - start).total_seconds() * 1000 + except: + status['status'] = 'down' + + services.append(status) + + # Get Prometheus metrics if available + prometheus_data = None + try: + response = requests.get('http://prometheus:9090/api/v1/query', + params={'query': 'up'}, timeout=2) + if response.status_code == 200: + prometheus_data = response.json() + except: + pass + + return render_template('monitoring/system_status.html', + services=services, + prometheus_data=prometheus_data) + +@monitoring_bp.route('/alerts') +@login_required +def alerts(): + """Show active alerts from Alertmanager""" + alerts = [] + + try: + # Get alerts from Alertmanager + response = requests.get('http://alertmanager:9093/api/v1/alerts', timeout=2) + if response.status_code == 200: + alerts = response.json() + except: + # Fallback to database anomalies + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + cur.execute(""" + SELECT + ad.*, + l.license_key, + c.company_name + FROM anomaly_detections ad + LEFT JOIN licenses l ON l.id = ad.license_id + LEFT JOIN customers c ON c.id = l.customer_id + WHERE ad.resolved = false + ORDER BY ad.detected_at DESC + LIMIT 50 + """) + alerts = cur.fetchall() + + cur.close() + conn.close() + + return render_template('monitoring/alerts.html', alerts=alerts) + +@monitoring_bp.route('/analytics') +@login_required +def analytics(): + """Detailed analytics page""" + # This will integrate with the existing analytics service + return render_template('monitoring/analytics.html') + +# API endpoints for live data +@monitoring_bp.route('/api/live-stats') +@login_required +def api_live_stats(): + """API endpoint for live statistics""" + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # Get current stats + cur.execute(""" + SELECT + COUNT(DISTINCT license_id) as active_licenses, + COUNT(DISTINCT hardware_id) as active_devices, + COUNT(*) as validations_last_minute + FROM license_heartbeats + WHERE timestamp > NOW() - INTERVAL '1 minute' + """) + stats = cur.fetchone() + + cur.close() + conn.close() + + return jsonify(stats) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@monitoring_bp.route('/api/active-sessions') +@login_required +def api_active_sessions(): + """API endpoint for active customer sessions""" + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # Get active sessions with geo data + cur.execute(""" + SELECT + l.license_key, + c.company_name, + lh.hardware_id, + lh.ip_address, + lh.timestamp as last_activity, + EXTRACT(EPOCH FROM (NOW() - lh.timestamp)) as seconds_ago, + lh.session_data + FROM license_heartbeats lh + JOIN licenses l ON l.id = lh.license_id + JOIN customers c ON c.id = l.customer_id + WHERE lh.timestamp > NOW() - INTERVAL '5 minutes' + ORDER BY lh.timestamp DESC + LIMIT 50 + """) + sessions = cur.fetchall() + + cur.close() + conn.close() + + return jsonify(sessions) + + except Exception as e: + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/v2_adminpanel/templates/base.html b/v2_adminpanel/templates/base.html index 8df5398..3865573 100644 --- a/v2_adminpanel/templates/base.html +++ b/v2_adminpanel/templates/base.html @@ -415,11 +415,37 @@ Audit-Log -