Navbar erweitert - Zwischenstand
Dieser Commit ist enthalten in:
@@ -71,7 +71,8 @@
|
||||
"Bash(fi)",
|
||||
"Bash(done)",
|
||||
"Bash(docker compose:*)",
|
||||
"Bash(true)"
|
||||
"Bash(true)",
|
||||
"Bash(git checkout:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
15
lizenzserver/services/auth/config.py
Normale Datei
15
lizenzserver/services/auth/config.py
Normale Datei
@@ -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'),
|
||||
}
|
||||
4
v2/.env
4
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
259
v2_adminpanel/routes/monitoring_routes.py
Normale Datei
259
v2_adminpanel/routes/monitoring_routes.py
Normale Datei
@@ -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
|
||||
@@ -415,11 +415,37 @@
|
||||
<span>Audit-Log</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'sessions.sessions' %}active{% endif %}" href="{{ url_for('sessions.sessions') }}">
|
||||
<i class="bi bi-people"></i>
|
||||
<span>Sitzungen</span>
|
||||
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.system_status', 'monitoring.alerts', 'monitoring.analytics'] %}has-active-child{% endif %}">
|
||||
<a class="nav-link has-submenu" href="{{ url_for('monitoring.live_dashboard') }}">
|
||||
<i class="bi bi-activity"></i>
|
||||
<span>Monitoring</span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'monitoring.live_dashboard' %}active{% endif %}" href="{{ url_for('monitoring.live_dashboard') }}">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
<span>Live Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'monitoring.system_status' %}active{% endif %}" href="{{ url_for('monitoring.system_status') }}">
|
||||
<i class="bi bi-pc-display"></i>
|
||||
<span>System Status</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'monitoring.alerts' %}active{% endif %}" href="{{ url_for('monitoring.alerts') }}">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<span>Alerts</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'monitoring.analytics' %}active{% endif %}" href="{{ url_for('monitoring.analytics') }}">
|
||||
<i class="bi bi-bar-chart-line"></i>
|
||||
<span>Analytics</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.backups' %}active{% endif %}" href="{{ url_for('admin.backups') }}">
|
||||
|
||||
322
v2_adminpanel/templates/monitoring/alerts.html
Normale Datei
322
v2_adminpanel/templates/monitoring/alerts.html
Normale Datei
@@ -0,0 +1,322 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Alerts{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.alert-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 5px solid #dee2e6;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.alert-critical {
|
||||
border-left-color: #dc3545;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.alert-high {
|
||||
border-left-color: #fd7e14;
|
||||
background-color: #ffe5d1;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-left-color: #ffc107;
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-left-color: #17a2b8;
|
||||
background-color: #d1ecf1;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.severity-critical {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.severity-high {
|
||||
background-color: #fd7e14;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.severity-medium {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.severity-low {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-timestamp {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.alert-stats {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-pills {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-pill.active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-exclamation-triangle"></i> Alerts</h2>
|
||||
<p class="text-muted">Aktive Warnungen und Anomalien</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-primary" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Statistics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-danger">{{ alerts|selectattr('severity', 'equalto', 'critical')|list|length }}</div>
|
||||
<div class="text-muted">Kritisch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-warning">{{ alerts|selectattr('severity', 'equalto', 'high')|list|length }}</div>
|
||||
<div class="text-muted">Hoch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-info">{{ alerts|selectattr('severity', 'equalto', 'medium')|list|length }}</div>
|
||||
<div class="text-muted">Mittel</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-success">{{ alerts|selectattr('severity', 'equalto', 'low')|list|length }}</div>
|
||||
<div class="text-muted">Niedrig</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Pills -->
|
||||
<div class="filter-pills">
|
||||
<span class="badge bg-secondary filter-pill active me-2" onclick="filterAlerts('all')">
|
||||
Alle ({{ alerts|length }})
|
||||
</span>
|
||||
<span class="badge bg-danger filter-pill me-2" onclick="filterAlerts('critical')">
|
||||
Kritisch
|
||||
</span>
|
||||
<span class="badge bg-warning filter-pill me-2" onclick="filterAlerts('high')">
|
||||
Hoch
|
||||
</span>
|
||||
<span class="badge bg-info filter-pill me-2" onclick="filterAlerts('medium')">
|
||||
Mittel
|
||||
</span>
|
||||
<span class="badge bg-success filter-pill me-2" onclick="filterAlerts('low')">
|
||||
Niedrig
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Alerts List -->
|
||||
<div id="alerts-container">
|
||||
{% for alert in alerts %}
|
||||
<div class="alert-card alert-{{ alert.severity }}" data-severity="{{ alert.severity }}">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h5 class="mb-0 me-3">
|
||||
{% if alert.anomaly_type == 'multiple_ips' %}
|
||||
<i class="bi bi-geo-alt-fill"></i> Mehrere IP-Adressen erkannt
|
||||
{% elif alert.anomaly_type == 'rapid_hardware_change' %}
|
||||
<i class="bi bi-laptop"></i> Schneller Hardware-Wechsel
|
||||
{% elif alert.anomaly_type == 'suspicious_pattern' %}
|
||||
<i class="bi bi-shield-exclamation"></i> Verdächtiges Muster
|
||||
{% else %}
|
||||
<i class="bi bi-exclamation-circle"></i> {{ alert.anomaly_type }}
|
||||
{% endif %}
|
||||
</h5>
|
||||
<span class="severity-badge severity-{{ alert.severity }}">
|
||||
{{ alert.severity }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if alert.company_name %}
|
||||
<div class="mb-2">
|
||||
<strong>Kunde:</strong> {{ alert.company_name }}
|
||||
{% if alert.license_key %}
|
||||
<span class="text-muted">({{ alert.license_key[:8] }}...)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="alert-timestamp">
|
||||
<i class="bi bi-clock"></i> {{ alert.detected_at|default(alert.startsAt) }}
|
||||
</div>
|
||||
|
||||
{% if alert.details %}
|
||||
<div class="alert-details">
|
||||
<strong>Details:</strong><br>
|
||||
{{ alert.details }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="alert-actions">
|
||||
{% if not alert.resolved %}
|
||||
<button class="btn btn-sm btn-success w-100 mb-2" onclick="resolveAlert('{{ alert.id }}')">
|
||||
<i class="bi bi-check-circle"></i> Als gelöst markieren
|
||||
</button>
|
||||
<button class="btn btn-sm btn-warning w-100 mb-2" onclick="investigateAlert('{{ alert.id }}')">
|
||||
<i class="bi bi-search"></i> Untersuchen
|
||||
</button>
|
||||
{% if alert.severity in ['critical', 'high'] %}
|
||||
<button class="btn btn-sm btn-danger w-100" onclick="blockLicense('{{ alert.license_id }}')">
|
||||
<i class="bi bi-shield-lock"></i> Lizenz blockieren
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-success text-center">
|
||||
<i class="bi bi-check-circle-fill"></i> Gelöst
|
||||
{% if alert.resolved_at %}
|
||||
<div class="small">{{ alert.resolved_at }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield-check" style="font-size: 4rem; color: #28a745;"></i>
|
||||
<h4 class="mt-3">Keine aktiven Alerts</h4>
|
||||
<p class="text-muted">Alle Systeme laufen normal</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Filter alerts by severity
|
||||
function filterAlerts(severity) {
|
||||
// Update active pill
|
||||
document.querySelectorAll('.filter-pill').forEach(pill => {
|
||||
pill.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Filter alert cards
|
||||
document.querySelectorAll('.alert-card').forEach(card => {
|
||||
if (severity === 'all' || card.dataset.severity === severity) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve alert
|
||||
async function resolveAlert(alertId) {
|
||||
if (!confirm('Möchten Sie diesen Alert als gelöst markieren?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/alerts/${alertId}/resolve`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Alert wurde als gelöst markiert');
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Markieren des Alerts');
|
||||
}
|
||||
}
|
||||
|
||||
// Investigate alert
|
||||
function investigateAlert(alertId) {
|
||||
// In production, this would open a detailed investigation view
|
||||
alert('Detaillierte Untersuchung wird geöffnet...');
|
||||
}
|
||||
|
||||
// Block license
|
||||
async function blockLicense(licenseId) {
|
||||
if (!confirm('WARNUNG: Möchten Sie diese Lizenz wirklich blockieren? Der Kunde kann die Software nicht mehr nutzen!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/licenses/${licenseId}/block`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Lizenz wurde blockiert');
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Blockieren der Lizenz');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh alerts every 60 seconds
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, 60000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
358
v2_adminpanel/templates/monitoring/analytics.html
Normale Datei
358
v2_adminpanel/templates/monitoring/analytics.html
Normale Datei
@@ -0,0 +1,358 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Analytics{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.analytics-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.date-range-selector {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.grafana-info {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-bar-chart-line"></i> Analytics</h2>
|
||||
<p class="text-muted">Detaillierte Analyse und Berichte</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grafana Integration Info -->
|
||||
<div class="grafana-info">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h5><i class="bi bi-graph-up"></i> Erweiterte Analytics verfügbar</h5>
|
||||
<p class="mb-0">Für detaillierte Dashboards und erweiterte Analysen nutzen Sie unser Grafana Dashboard.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="http://localhost:3000/d/license-server-overview" target="_blank" class="btn btn-primary">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Grafana öffnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Selector -->
|
||||
<div class="date-range-selector">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<label>Zeitraum auswählen:</label>
|
||||
<select class="form-select" id="date-range" onchange="updateAnalytics()">
|
||||
<option value="today">Heute</option>
|
||||
<option value="week" selected>Letzte 7 Tage</option>
|
||||
<option value="month">Letzte 30 Tage</option>
|
||||
<option value="quarter">Letztes Quartal</option>
|
||||
<option value="year">Letztes Jahr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-outline-primary" onclick="refreshAnalytics()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics Overview -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">1,234</div>
|
||||
<div class="stat-label">Aktive Lizenzen</div>
|
||||
<div class="trend-up">
|
||||
<i class="bi bi-arrow-up"></i> +12% vs. Vormonat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">45.2K</div>
|
||||
<div class="stat-label">Validierungen</div>
|
||||
<div class="trend-up">
|
||||
<i class="bi bi-arrow-up"></i> +8% vs. Vormonat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">€12.5K</div>
|
||||
<div class="stat-label">MRR</div>
|
||||
<div class="trend-up">
|
||||
<i class="bi bi-arrow-up"></i> +15% vs. Vormonat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">2.3%</div>
|
||||
<div class="stat-label">Churn Rate</div>
|
||||
<div class="trend-down">
|
||||
<i class="bi bi-arrow-down"></i> -0.5% vs. Vormonat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row 1 -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="analytics-card">
|
||||
<h5>Lizenz-Nutzung über Zeit</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="usageChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="analytics-card">
|
||||
<h5>Validierungen nach Stunde</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="validationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row 2 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="analytics-card">
|
||||
<h5>Lizenztyp-Verteilung</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="licenseTypeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="analytics-card">
|
||||
<h5>Geografische Verteilung</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="geoChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="analytics-card">
|
||||
<h5>Top 10 Kunden</h5>
|
||||
<div style="height: 300px; overflow-y: auto;">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kunde</th>
|
||||
<th>Lizenzen</th>
|
||||
<th>Nutzung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ACME Corp</td>
|
||||
<td>45</td>
|
||||
<td><span class="badge bg-success">Hoch</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TechStart GmbH</td>
|
||||
<td>32</td>
|
||||
<td><span class="badge bg-success">Hoch</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Global Solutions</td>
|
||||
<td>28</td>
|
||||
<td><span class="badge bg-warning">Mittel</span></td>
|
||||
</tr>
|
||||
<!-- More rows... -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Options -->
|
||||
<div class="analytics-card mt-4">
|
||||
<h5>Berichte exportieren</h5>
|
||||
<div class="export-buttons">
|
||||
<button class="btn btn-outline-primary me-2" onclick="exportReport('pdf')">
|
||||
<i class="bi bi-file-pdf"></i> PDF Export
|
||||
</button>
|
||||
<button class="btn btn-outline-success me-2" onclick="exportReport('excel')">
|
||||
<i class="bi bi-file-excel"></i> Excel Export
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="exportReport('csv')">
|
||||
<i class="bi bi-file-text"></i> CSV Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script>
|
||||
let usageChart, validationChart, licenseTypeChart, geoChart;
|
||||
|
||||
function initCharts() {
|
||||
// Usage over time
|
||||
const usageCtx = document.getElementById('usageChart').getContext('2d');
|
||||
usageChart = new Chart(usageCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
|
||||
datasets: [{
|
||||
label: 'Aktive Lizenzen',
|
||||
data: [1180, 1205, 1195, 1220, 1234, 1150, 1100],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Validations by hour
|
||||
const validationCtx = document.getElementById('validationChart').getContext('2d');
|
||||
validationChart = new Chart(validationCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['00', '04', '08', '12', '16', '20'],
|
||||
datasets: [{
|
||||
label: 'Validierungen',
|
||||
data: [120, 80, 450, 680, 520, 340],
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// License type distribution
|
||||
const typeCtx = document.getElementById('licenseTypeChart').getContext('2d');
|
||||
licenseTypeChart = new Chart(typeCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Professional', 'Enterprise', 'Starter', 'Trial'],
|
||||
datasets: [{
|
||||
data: [45, 30, 20, 5],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.5)',
|
||||
'rgba(54, 162, 235, 0.5)',
|
||||
'rgba(255, 205, 86, 0.5)',
|
||||
'rgba(75, 192, 192, 0.5)'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Geographic distribution
|
||||
const geoCtx = document.getElementById('geoChart').getContext('2d');
|
||||
geoChart = new Chart(geoCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['DE', 'AT', 'CH', 'US', 'UK'],
|
||||
datasets: [{
|
||||
label: 'Aktive Nutzer',
|
||||
data: [580, 230, 180, 120, 90],
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.5)',
|
||||
borderColor: 'rgba(153, 102, 255, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAnalytics() {
|
||||
const range = document.getElementById('date-range').value;
|
||||
// In production, this would fetch new data based on the selected range
|
||||
console.log('Updating analytics for range:', range);
|
||||
}
|
||||
|
||||
function refreshAnalytics() {
|
||||
// Refresh all data
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function exportReport(format) {
|
||||
alert(`Exporting report as ${format.toUpperCase()}...`);
|
||||
// In production, this would trigger actual export
|
||||
}
|
||||
|
||||
// Initialize charts on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initCharts();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
386
v2_adminpanel/templates/monitoring/live_dashboard.html
Normale Datei
386
v2_adminpanel/templates/monitoring/live_dashboard.html
Normale Datei
@@ -0,0 +1,386 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Live Dashboard{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.stats-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #28a745;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.activity-indicator {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.activity-active {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.activity-recent {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
.activity-inactive {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.geo-info {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
background: #f8f9fa;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.85rem;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-activity"></i> Live Dashboard</h2>
|
||||
<p class="text-muted">Echtzeit-Übersicht der aktiven Kunden-Sessions</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span class="text-muted">Auto-Refresh: <span id="refresh-countdown">30</span>s</span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" onclick="refreshData()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Jetzt aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<i class="bi bi-people-fill text-primary" style="font-size: 2rem;"></i>
|
||||
<div class="stats-number text-primary">{{ stats.active_licenses|default(0) }}</div>
|
||||
<div class="stats-label">Aktive Kunden</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<i class="bi bi-laptop text-success" style="font-size: 2rem;"></i>
|
||||
<div class="stats-number text-success">{{ stats.active_devices|default(0) }}</div>
|
||||
<div class="stats-label">Aktive Geräte</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stats-card">
|
||||
<i class="bi bi-speedometer2 text-info" style="font-size: 2rem;"></i>
|
||||
<div class="stats-number text-info" id="validations-per-minute">0</div>
|
||||
<div class="stats-label">Validierungen/Min</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Timeline Chart -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Aktivität (letzte 60 Minuten)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="activityChart" height="80"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Sessions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="active-sessions-container">
|
||||
{% for session in active_sessions %}
|
||||
<div class="session-card">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-1">
|
||||
<span class="activity-indicator activity-active"></span>
|
||||
{{ session.company_name }}
|
||||
</h6>
|
||||
<small class="text-muted">{{ session.contact_person }}</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-1">
|
||||
<i class="bi bi-key"></i> {{ session.license_key[:8] }}...
|
||||
</div>
|
||||
<div class="device-info">
|
||||
<i class="bi bi-laptop"></i> {{ session.active_devices }} Gerät(e)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="geo-info">
|
||||
<i class="bi bi-geo-alt"></i> {{ session.ip_address }}
|
||||
<div><small>Hardware: {{ session.hardware_id[:12] }}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-clock"></i>
|
||||
<span class="last-activity" data-timestamp="{{ session.last_activity }}">
|
||||
vor wenigen Sekunden
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||
<p>Keine aktiven Sessions in den letzten 5 Minuten</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden validation timeline data for chart -->
|
||||
<script id="validation-timeline-data" type="application/json">
|
||||
{{ validation_timeline|tojson }}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
|
||||
<script>
|
||||
let activityChart;
|
||||
let refreshInterval;
|
||||
let refreshCountdown = 30;
|
||||
|
||||
// Initialize activity chart
|
||||
function initActivityChart() {
|
||||
const ctx = document.getElementById('activityChart').getContext('2d');
|
||||
const timelineData = JSON.parse(document.getElementById('validation-timeline-data').textContent);
|
||||
|
||||
// Prepare data for chart
|
||||
const labels = timelineData.map(item => {
|
||||
const date = new Date(item.minute);
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}).reverse();
|
||||
|
||||
const data = timelineData.map(item => item.validations).reverse();
|
||||
|
||||
activityChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Validierungen',
|
||||
data: data,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.1,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update last activity times
|
||||
function updateActivityTimes() {
|
||||
document.querySelectorAll('.last-activity').forEach(el => {
|
||||
const timestamp = new Date(el.dataset.timestamp);
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now - timestamp) / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
el.textContent = 'vor wenigen Sekunden';
|
||||
} else if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
el.textContent = `vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
||||
} else {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
el.textContent = `vor ${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh data via AJAX
|
||||
async function refreshData() {
|
||||
try {
|
||||
// Get live stats
|
||||
const statsResponse = await fetch('/monitoring/api/live-stats');
|
||||
const stats = await statsResponse.json();
|
||||
|
||||
// Update stats cards
|
||||
document.querySelector('.stats-number.text-primary').textContent = stats.active_licenses || 0;
|
||||
document.querySelector('.stats-number.text-success').textContent = stats.active_devices || 0;
|
||||
document.getElementById('validations-per-minute').textContent = stats.validations_last_minute || 0;
|
||||
|
||||
// Get active sessions
|
||||
const sessionsResponse = await fetch('/monitoring/api/active-sessions');
|
||||
const sessions = await sessionsResponse.json();
|
||||
|
||||
// Update sessions display
|
||||
updateSessionsDisplay(sessions);
|
||||
|
||||
// Reset countdown
|
||||
refreshCountdown = 30;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update sessions display
|
||||
function updateSessionsDisplay(sessions) {
|
||||
const container = document.getElementById('active-sessions-container');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||
<p>Keine aktiven Sessions in den letzten 5 Minuten</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionsHtml = sessions.map(session => {
|
||||
const secondsAgo = Math.floor(session.seconds_ago);
|
||||
let activityClass = 'activity-active';
|
||||
if (secondsAgo > 120) activityClass = 'activity-recent';
|
||||
if (secondsAgo > 240) activityClass = 'activity-inactive';
|
||||
|
||||
return `
|
||||
<div class="session-card">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-1">
|
||||
<span class="activity-indicator ${activityClass}"></span>
|
||||
${session.company_name}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-1">
|
||||
<i class="bi bi-key"></i> ${session.license_key.substring(0, 8)}...
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="geo-info">
|
||||
<i class="bi bi-geo-alt"></i> ${session.ip_address}
|
||||
<div><small>Hardware: ${session.hardware_id.substring(0, 12)}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-clock"></i>
|
||||
vor ${formatSecondsAgo(secondsAgo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = sessionsHtml;
|
||||
}
|
||||
|
||||
// Format seconds ago
|
||||
function formatSecondsAgo(seconds) {
|
||||
if (seconds < 60) return 'wenigen Sekunden';
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
return `${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
||||
}
|
||||
|
||||
// Countdown timer
|
||||
function updateCountdown() {
|
||||
refreshCountdown--;
|
||||
document.getElementById('refresh-countdown').textContent = refreshCountdown;
|
||||
|
||||
if (refreshCountdown <= 0) {
|
||||
refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initActivityChart();
|
||||
updateActivityTimes();
|
||||
|
||||
// Set up auto-refresh
|
||||
refreshInterval = setInterval(() => {
|
||||
updateCountdown();
|
||||
updateActivityTimes();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Clean up on page leave
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
257
v2_adminpanel/templates/monitoring/system_status.html
Normale Datei
257
v2_adminpanel/templates/monitoring/system_status.html
Normale Datei
@@ -0,0 +1,257 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}System Status{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 5px solid #dee2e6;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.service-card.healthy {
|
||||
border-left-color: #28a745;
|
||||
}
|
||||
|
||||
.service-card.unhealthy {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.service-card.down {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.875rem;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-healthy {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-unhealthy {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-down {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.response-time {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.grafana-embed {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-pc-display"></i> System Status</h2>
|
||||
<p class="text-muted">Übersicht über die Gesundheit aller Services</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-primary" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Status Grid -->
|
||||
<div class="row">
|
||||
{% for service in services %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="service-card {{ service.status }}">
|
||||
<div class="text-center">
|
||||
{% if service.name == 'License Server' %}
|
||||
<i class="bi bi-shield-check service-icon text-primary"></i>
|
||||
{% elif service.name == 'PostgreSQL' %}
|
||||
<i class="bi bi-database service-icon text-info"></i>
|
||||
{% elif service.name == 'Redis' %}
|
||||
<i class="bi bi-memory service-icon text-danger"></i>
|
||||
{% elif service.name == 'Auth Service' %}
|
||||
<i class="bi bi-key service-icon text-warning"></i>
|
||||
{% elif service.name == 'Analytics Service' %}
|
||||
<i class="bi bi-graph-up service-icon text-success"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-server service-icon text-secondary"></i>
|
||||
{% endif %}
|
||||
|
||||
<h5>{{ service.name }}</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
{% if service.status == 'healthy' %}
|
||||
<span class="status-badge status-healthy">
|
||||
<i class="bi bi-check-circle"></i> Healthy
|
||||
</span>
|
||||
{% elif service.status == 'unhealthy' %}
|
||||
<span class="status-badge status-unhealthy">
|
||||
<i class="bi bi-exclamation-circle"></i> Unhealthy
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-down">
|
||||
<i class="bi bi-x-circle"></i> Down
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if service.response_time %}
|
||||
<div class="response-time">
|
||||
<i class="bi bi-speedometer2"></i> {{ "%.1f"|format(service.response_time) }}ms
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- System Metrics -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-cpu"></i> System Metriken</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="cpu-usage">-</div>
|
||||
<div class="metric-label">CPU Auslastung</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="memory-usage">-</div>
|
||||
<div class="metric-label">RAM Auslastung</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="disk-usage">-</div>
|
||||
<div class="metric-label">Festplatte</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="uptime">-</div>
|
||||
<div class="metric-label">Uptime</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grafana Dashboard Embed (if available) -->
|
||||
{% if prometheus_data %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Performance Dashboard</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Für detaillierte Metriken besuchen Sie das
|
||||
<a href="http://localhost:3000" target="_blank" class="alert-link">Grafana Dashboard</a>
|
||||
</div>
|
||||
<!-- Optionally embed Grafana dashboard here -->
|
||||
<!-- <iframe src="http://localhost:3000/d/license-server-overview?orgId=1&theme=light" class="grafana-embed"></iframe> -->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-lightning"></i> Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-primary w-100 mb-2" onclick="restartService('license-server')">
|
||||
<i class="bi bi-arrow-clockwise"></i> License Server neustarten
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-warning w-100 mb-2" onclick="clearCache()">
|
||||
<i class="bi bi-trash"></i> Cache leeren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Mock functions for demonstration
|
||||
function restartService(service) {
|
||||
if (confirm(`Möchten Sie ${service} wirklich neustarten?`)) {
|
||||
alert('Service-Neustart wurde initiiert. Dies kann einige Minuten dauern.');
|
||||
}
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
if (confirm('Möchten Sie den Cache wirklich leeren?')) {
|
||||
alert('Cache wurde geleert.');
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch system metrics (mock data for now)
|
||||
function updateSystemMetrics() {
|
||||
// In production, these would come from actual monitoring endpoints
|
||||
document.getElementById('cpu-usage').textContent = Math.floor(Math.random() * 40 + 20) + '%';
|
||||
document.getElementById('memory-usage').textContent = Math.floor(Math.random() * 30 + 40) + '%';
|
||||
document.getElementById('disk-usage').textContent = Math.floor(Math.random() * 20 + 60) + '%';
|
||||
document.getElementById('uptime').textContent = '14 Tage';
|
||||
}
|
||||
|
||||
// Update metrics on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSystemMetrics();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(updateSystemMetrics, 30000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -73,35 +73,15 @@ http {
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# Auth Service API (internal only)
|
||||
location /api/v1/auth/ {
|
||||
proxy_pass http://auth-service:5001/api/v1/auth/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
|
||||
# Analytics Service API (internal only)
|
||||
location /api/v1/analytics/ {
|
||||
proxy_pass http://analytics-service:5003/api/v1/analytics/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
|
||||
# Admin API Service (internal only)
|
||||
location /api/v1/admin/ {
|
||||
proxy_pass http://admin-api-service:5004/api/v1/admin/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
# Auth Service API (internal only) - temporarily disabled
|
||||
# location /api/v1/auth/ {
|
||||
# proxy_pass http://auth-service:5001/api/v1/auth/;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header Authorization $http_authorization;
|
||||
# }
|
||||
}
|
||||
|
||||
# API Server (für später)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren