Navbar erweitert - Zwischenstand

Dieser Commit ist enthalten in:
2025-06-19 00:29:07 +02:00
Ursprung 7017549fcd
Commit eee4b4de1e
12 geänderte Dateien mit 1713 neuen und 110 gelöschten Zeilen

Datei anzeigen

@@ -71,7 +71,8 @@
"Bash(fi)",
"Bash(done)",
"Bash(docker compose:*)",
"Bash(true)"
"Bash(true)",
"Bash(git checkout:*)"
],
"deny": []
}

Datei anzeigen

@@ -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'),
}

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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') }}">

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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)