Monitoring Anpassung

Dieser Commit ist enthalten in:
2025-06-21 19:47:49 +02:00
Ursprung 3d02c7a111
Commit fdf74c11ec
4 geänderte Dateien mit 962 neuen und 364 gelöschten Zeilen

Datei anzeigen

@@ -881,69 +881,8 @@ def license_analytics():
@admin_bp.route("/lizenzserver/anomalies") @admin_bp.route("/lizenzserver/anomalies")
@login_required @login_required
def license_anomalies(): def license_anomalies():
"""Anomaly detection and management""" """Redirect to unified monitoring page"""
try: return redirect(url_for('monitoring.unified_monitoring'))
conn = get_connection()
cur = conn.cursor()
# Filter parameters
severity = request.args.get('severity', 'all')
resolved = request.args.get('resolved', 'false')
# Build query
query = """
SELECT ad.*, l.license_key, c.name as customer_name, c.email
FROM anomaly_detections ad
LEFT JOIN licenses l ON ad.license_id = l.id
LEFT JOIN customers c ON l.customer_id = c.id
WHERE 1=1
"""
params = []
if severity != 'all':
query += " AND ad.severity = %s"
params.append(severity)
if resolved == 'false':
query += " AND ad.resolved = false"
elif resolved == 'true':
query += " AND ad.resolved = true"
query += " ORDER BY ad.detected_at DESC LIMIT 100"
cur.execute(query, params)
anomalies = cur.fetchall()
# Get anomaly statistics
cur.execute("""
SELECT anomaly_type, severity, COUNT(*) as count
FROM anomaly_detections
WHERE resolved = false
GROUP BY anomaly_type, severity
ORDER BY count DESC
""")
anomaly_stats = cur.fetchall()
return render_template('license_anomalies.html',
anomalies=anomalies,
anomaly_stats=anomaly_stats,
severity=severity,
resolved=resolved
)
except Exception as e:
flash(f'Fehler beim Laden der Anomalie-Daten: {str(e)}', 'error')
return render_template('license_anomalies.html',
anomalies=[],
anomaly_stats=[],
severity='all',
resolved='false'
)
finally:
if 'cur' in locals():
cur.close()
if 'conn' in locals():
conn.close()
@admin_bp.route("/lizenzserver/anomaly/<anomaly_id>/resolve", methods=["POST"]) @admin_bp.route("/lizenzserver/anomaly/<anomaly_id>/resolve", methods=["POST"])

Datei anzeigen

@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, jsonify, request, session from flask import Blueprint, render_template, jsonify, request, session, redirect, url_for
from functools import wraps from functools import wraps
import psycopg2 import psycopg2
from psycopg2.extras import RealDictCursor from psycopg2.extras import RealDictCursor
@@ -8,10 +8,10 @@ from datetime import datetime, timedelta
import logging import logging
from utils.partition_helper import ensure_partition_exists, check_table_exists from utils.partition_helper import ensure_partition_exists, check_table_exists
monitoring_bp = Blueprint('monitoring', __name__) # Configure logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Database connection # Create a function to get database connection
def get_db_connection(): def get_db_connection():
return psycopg2.connect( return psycopg2.connect(
host=os.environ.get('POSTGRES_HOST', 'postgres'), host=os.environ.get('POSTGRES_HOST', 'postgres'),
@@ -20,168 +20,277 @@ def get_db_connection():
password=os.environ.get('POSTGRES_PASSWORD', 'postgres') password=os.environ.get('POSTGRES_PASSWORD', 'postgres')
) )
# Login required decorator
def login_required(f): def login_required(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if 'logged_in' not in session: if 'user_id' not in session:
return render_template('error.html', return redirect(url_for('auth.login'))
error_message='Nicht autorisiert',
details='Sie müssen angemeldet sein, um diese Seite zu sehen.')
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
@monitoring_bp.route('/live-dashboard') # Create Blueprint
monitoring_bp = Blueprint('monitoring', __name__)
@monitoring_bp.route('/monitoring')
@login_required @login_required
def live_dashboard(): def unified_monitoring():
"""Live Dashboard showing active customer sessions and analytics""" """Unified monitoring dashboard combining live activity and anomaly detection"""
try: try:
conn = get_db_connection() conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
# Check if license_heartbeats table exists # Initialize default values
if not check_table_exists(conn, 'license_heartbeats'): system_status = 'normal'
logger.warning("license_heartbeats table does not exist") status_color = 'success'
# Return empty data active_alerts = 0
return render_template('monitoring/live_dashboard.html', live_metrics = {
active_sessions=[], 'active_licenses': 0,
stats={'active_licenses': 0, 'active_devices': 0, 'total_heartbeats': 0}, 'total_validations': 0,
validation_timeline=[], 'unique_devices': 0,
live_stats=[0, 0, 0, 0], 'unique_ips': 0,
validation_rates=[], 'avg_response_time': 0
recent_anomalies=[], }
top_licenses=[]) trend_data = []
activity_stream = []
geo_data = []
top_licenses = []
anomaly_distribution = []
performance_data = []
# Check if tables exist before querying
has_heartbeats = check_table_exists(conn, 'license_heartbeats')
has_anomalies = check_table_exists(conn, 'anomaly_detections')
if has_anomalies:
# Get active alerts count
cur.execute("""
SELECT COUNT(*) as count
FROM anomaly_detections
WHERE resolved = false
AND detected_at > NOW() - INTERVAL '24 hours'
""")
active_alerts = cur.fetchone()['count'] or 0
# Determine system status based on alerts
if active_alerts == 0:
system_status = 'normal'
status_color = 'success'
elif active_alerts < 5:
system_status = 'warning'
status_color = 'warning'
else:
system_status = 'critical'
status_color = 'danger'
if has_heartbeats:
# Ensure current month partition exists # Ensure current month partition exists
ensure_partition_exists(conn, 'license_heartbeats', datetime.now()) ensure_partition_exists(conn, 'license_heartbeats', datetime.now())
# Get active customer sessions (last 5 minutes) # Executive summary metrics
cur.execute("""
SELECT
l.id,
l.license_key,
c.name as company_name,
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 (for both charts)
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()
# Get live statistics for analytics cards
cur.execute(""" cur.execute("""
SELECT SELECT
COUNT(DISTINCT license_id) as active_licenses, COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations, COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices, COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips COUNT(DISTINCT ip_address) as unique_ips,
AVG(CASE WHEN response_time IS NOT NULL THEN response_time ELSE 0 END) as avg_response_time
FROM license_heartbeats FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes' WHERE timestamp > NOW() - INTERVAL '5 minutes'
""") """)
live_stats_data = cur.fetchone() result = cur.fetchone()
live_stats = [ if result:
live_stats_data['active_licenses'] or 0, live_metrics = result
live_stats_data['total_validations'] or 0,
live_stats_data['unique_devices'] or 0,
live_stats_data['unique_ips'] or 0
]
# Get validation rates for analytics chart (last 30 minutes) # Get 24h trend data for metrics
cur.execute(""" cur.execute("""
SELECT SELECT
DATE_TRUNC('minute', timestamp) as minute, DATE_TRUNC('hour', timestamp) as hour,
COUNT(*) as count COUNT(DISTINCT license_id) as licenses,
COUNT(*) as validations
FROM license_heartbeats FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes' WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY minute GROUP BY hour
ORDER BY minute DESC ORDER BY hour
LIMIT 30
""") """)
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()] trend_data = cur.fetchall()
# Get recent anomalies # Activity stream - just validations if no anomalies table
cur.execute(""" if has_anomalies:
SELECT
ad.*,
l.license_key,
c.name as customer_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.detected_at > NOW() - INTERVAL '24 hours'
ORDER BY ad.detected_at DESC
LIMIT 10
""")
recent_anomalies = cur.fetchall()
# Get top active licenses
cur.execute(""" cur.execute("""
WITH combined_events AS (
-- Normal validations
SELECT SELECT
lh.timestamp,
'validation' as event_type,
'normal' as severity,
l.license_key, l.license_key,
c.name as customer_name, c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count, lh.ip_address,
COUNT(*) as validation_count, lh.hardware_id,
MAX(lh.timestamp) as last_seen NULL as anomaly_type,
NULL as description
FROM license_heartbeats lh FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > NOW() - INTERVAL '15 minutes' WHERE lh.timestamp > NOW() - INTERVAL '1 hour'
GROUP BY l.license_key, c.name
UNION ALL
-- Anomalies
SELECT
ad.detected_at as timestamp,
'anomaly' as event_type,
ad.severity,
l.license_key,
c.name as customer_name,
ad.ip_address,
ad.hardware_id,
ad.anomaly_type,
ad.description
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.detected_at > NOW() - INTERVAL '1 hour'
)
SELECT * FROM combined_events
ORDER BY timestamp DESC
LIMIT 100
""")
else:
# Just show validations
cur.execute("""
SELECT
lh.timestamp,
'validation' as event_type,
'normal' as severity,
l.license_key,
c.name as customer_name,
lh.ip_address,
lh.hardware_id,
NULL as anomaly_type,
NULL as description
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 '1 hour'
ORDER BY lh.timestamp DESC
LIMIT 100
""")
activity_stream = cur.fetchall()
# Geographic distribution
cur.execute("""
SELECT
ip_address,
COUNT(*) as request_count,
COUNT(DISTINCT license_id) as license_count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '1 hour'
GROUP BY ip_address
ORDER BY request_count DESC
LIMIT 20
""")
geo_data = cur.fetchall()
# Top active licenses
if has_anomalies:
cur.execute("""
SELECT
l.id,
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(lh.*) as validation_count,
MAX(lh.timestamp) as last_seen,
COUNT(DISTINCT ad.id) as anomaly_count
FROM licenses l
JOIN customers c ON c.id = l.customer_id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '1 hour'
LEFT JOIN anomaly_detections ad ON l.id = ad.license_id
AND ad.detected_at > NOW() - INTERVAL '24 hours'
WHERE lh.license_id IS NOT NULL
GROUP BY l.id, l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
else:
cur.execute("""
SELECT
l.id,
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(lh.*) as validation_count,
MAX(lh.timestamp) as last_seen,
0 as anomaly_count
FROM licenses l
JOIN customers c ON c.id = l.customer_id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '1 hour'
WHERE lh.license_id IS NOT NULL
GROUP BY l.id, l.license_key, c.name
ORDER BY validation_count DESC ORDER BY validation_count DESC
LIMIT 10 LIMIT 10
""") """)
top_licenses = cur.fetchall() top_licenses = cur.fetchall()
# Performance metrics
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
AVG(response_time) as avg_response_time,
MAX(response_time) as max_response_time,
COUNT(*) as request_count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes'
AND response_time IS NOT NULL
GROUP BY minute
ORDER BY minute DESC
""")
performance_data = cur.fetchall()
if has_anomalies:
# Anomaly distribution
cur.execute("""
SELECT
anomaly_type,
COUNT(*) as count,
MAX(severity) as max_severity
FROM anomaly_detections
WHERE detected_at > NOW() - INTERVAL '24 hours'
GROUP BY anomaly_type
ORDER BY count DESC
""")
anomaly_distribution = cur.fetchall()
cur.close() cur.close()
conn.close() conn.close()
return render_template('monitoring/live_dashboard.html', return render_template('monitoring/unified_monitoring.html',
active_sessions=active_sessions, system_status=system_status,
stats=stats, status_color=status_color,
validation_timeline=validation_timeline, active_alerts=active_alerts,
live_stats=live_stats, live_metrics=live_metrics,
validation_rates=validation_rates, trend_data=trend_data,
recent_anomalies=recent_anomalies, activity_stream=activity_stream,
top_licenses=top_licenses) geo_data=geo_data,
top_licenses=top_licenses,
anomaly_distribution=anomaly_distribution,
performance_data=performance_data)
except Exception as e: except Exception as e:
logger.error(f"Error in live dashboard: {str(e)}") logger.error(f"Error in unified monitoring: {str(e)}")
return render_template('error.html', return render_template('error.html',
error_message='Fehler beim Laden des Dashboards', error_message='Fehler beim Laden des Monitorings',
details=str(e)) details=str(e))
@monitoring_bp.route('/live-dashboard')
@login_required
def live_dashboard():
"""Redirect to unified monitoring dashboard"""
return redirect(url_for('monitoring.unified_monitoring'))
@monitoring_bp.route('/alerts') @monitoring_bp.route('/alerts')
@login_required @login_required
@@ -195,8 +304,9 @@ def alerts():
if response.status_code == 200: if response.status_code == 200:
alerts = response.json() alerts = response.json()
except: except:
# Fallback to database anomalies # Fallback to database anomalies if table exists
conn = get_db_connection() conn = get_db_connection()
if check_table_exists(conn, 'anomaly_detections'):
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute(""" cur.execute("""
@@ -226,6 +336,11 @@ def analytics():
conn = get_db_connection() conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
# Initialize default values
live_stats = [0, 0, 0, 0]
validation_rates = []
if check_table_exists(conn, 'license_heartbeats'):
# Get live statistics for the top cards # Get live statistics for the top cards
cur.execute(""" cur.execute("""
SELECT SELECT
@@ -236,15 +351,15 @@ def analytics():
FROM license_heartbeats FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes' WHERE timestamp > NOW() - INTERVAL '5 minutes'
""") """)
live_stats = cur.fetchone() live_stats_data = cur.fetchone()
live_stats = [ live_stats = [
live_stats['active_licenses'] or 0, live_stats_data['active_licenses'] or 0,
live_stats['total_validations'] or 0, live_stats_data['total_validations'] or 0,
live_stats['unique_devices'] or 0, live_stats_data['unique_devices'] or 0,
live_stats['unique_ips'] or 0 live_stats_data['unique_ips'] or 0
] ]
# Get validation rates for chart # Get validation rates for the chart (last 30 minutes, aggregated by minute)
cur.execute(""" cur.execute("""
SELECT SELECT
DATE_TRUNC('minute', timestamp) as minute, DATE_TRUNC('minute', timestamp) as minute,
@@ -257,115 +372,58 @@ def analytics():
""") """)
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()] validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
# Get recent anomalies
cur.execute("""
SELECT
ad.*,
l.license_key,
c.name as customer_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.detected_at > NOW() - INTERVAL '24 hours'
ORDER BY ad.detected_at DESC
LIMIT 10
""")
recent_anomalies = cur.fetchall()
# Get top active licenses
cur.execute("""
SELECT
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as validation_count,
MAX(lh.timestamp) as last_seen
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 '15 minutes'
GROUP BY l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
top_licenses = cur.fetchall()
cur.close() cur.close()
conn.close() conn.close()
return render_template('monitoring/analytics.html', return render_template('monitoring/analytics.html',
live_stats=live_stats, live_stats=live_stats,
validation_rates=validation_rates, validation_rates=validation_rates)
recent_anomalies=recent_anomalies,
top_licenses=top_licenses)
except Exception as e: except Exception as e:
logger.error(f"Error in analytics: {str(e)}") logger.error(f"Error in analytics: {str(e)}")
return render_template('monitoring/analytics.html', return render_template('error.html',
live_stats=[0, 0, 0, 0], error_message='Fehler beim Laden der Analytics',
validation_rates=[], details=str(e))
recent_anomalies=[],
top_licenses=[])
# API endpoints for live data
@monitoring_bp.route('/api/live-stats') @monitoring_bp.route('/analytics/stream')
@login_required @login_required
def api_live_stats(): def analytics_stream():
"""API endpoint for live statistics""" """Server-sent event stream for live analytics updates"""
def generate():
while True:
try: try:
conn = get_db_connection() conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
# Get current stats data = {'active_licenses': 0, 'total_validations': 0,
'unique_devices': 0, 'unique_ips': 0}
if check_table_exists(conn, 'license_heartbeats'):
cur.execute(""" cur.execute("""
SELECT SELECT
COUNT(DISTINCT license_id) as active_licenses, COUNT(DISTINCT license_id) as active_licenses,
COUNT(DISTINCT hardware_id) as active_devices, COUNT(*) as total_validations,
COUNT(*) as validations_last_minute COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '1 minute' WHERE timestamp > NOW() - INTERVAL '5 minutes'
""") """)
stats = cur.fetchone() result = cur.fetchone()
if result:
data = dict(result)
cur.close() cur.close()
conn.close() conn.close()
return jsonify(stats) yield f"data: {jsonify(data).get_data(as_text=True)}\n\n"
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 logger.error(f"Error in analytics stream: {str(e)}")
yield f"data: {jsonify({'error': str(e)}).get_data(as_text=True)}\n\n"
@monitoring_bp.route('/api/active-sessions') import time
@login_required time.sleep(5) # Update every 5 seconds
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 from flask import Response
cur.execute(""" return Response(generate(), mimetype="text/event-stream")
SELECT
l.license_key,
c.name as 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,19 +415,11 @@
</li> </li>
</ul> </ul>
</li> </li>
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.alerts', 'admin.audit_log', 'admin.license_monitor', 'admin.license_analytics', 'admin.license_anomalies'] %}has-active-child{% endif %}"> <li class="nav-item {% if request.endpoint in ['monitoring.unified_monitoring', 'monitoring.live_dashboard', 'monitoring.alerts'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('monitoring.live_dashboard') }}"> <a class="nav-link {% if request.endpoint == 'monitoring.unified_monitoring' %}active{% endif %}" href="{{ url_for('monitoring.unified_monitoring') }}">
<i class="bi bi-activity"></i> <i class="bi bi-activity"></i>
<span>Monitoring</span> <span>Monitoring</span>
</a> </a>
<ul class="sidebar-submenu">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_anomalies' %}active{% endif %}" href="{{ url_for('admin.license_anomalies') }}">
<i class="bi bi-bug"></i>
<span>Lizenz-Anomalien</span>
</a>
</li>
</ul>
</li> </li>
<li class="nav-item {% if request.endpoint in ['admin.audit_log', 'admin.backups', 'admin.blocked_ips', 'admin.license_config'] %}has-active-child{% endif %}"> <li class="nav-item {% if request.endpoint in ['admin.audit_log', 'admin.backups', 'admin.blocked_ips', 'admin.license_config'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('admin.license_config') }}"> <a class="nav-link has-submenu" href="{{ url_for('admin.license_config') }}">

Datei anzeigen

@@ -0,0 +1,609 @@
{% extends "base.html" %}
{% block title %}Monitoring{% endblock %}
{% block extra_css %}
<style>
/* Header Status Bar */
.status-header {
background: #f8f9fa;
border-bottom: 2px solid #dee2e6;
padding: 0.75rem;
position: sticky;
top: 0;
z-index: 100;
margin: -1rem -1rem 1rem -1rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
font-weight: 500;
}
.status-indicator .badge {
margin-left: 0.5rem;
}
/* Metric Cards */
.metric-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
position: relative;
transition: all 0.2s;
}
.metric-card:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.metric-value {
font-size: 2rem;
font-weight: bold;
margin: 0.5rem 0;
}
.metric-label {
color: #6c757d;
font-size: 0.875rem;
text-transform: uppercase;
}
.metric-trend {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 0.875rem;
}
.metric-alert {
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #dc3545;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: bold;
}
/* Activity Stream */
.activity-stream {
max-height: 500px;
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
background: white;
}
.activity-item {
padding: 0.75rem;
border-bottom: 1px solid #f8f9fa;
transition: background 0.2s;
}
.activity-item:hover {
background: #f8f9fa;
}
.activity-item.validation {
border-left: 3px solid #28a745;
}
.activity-item.anomaly-warning {
border-left: 3px solid #ffc107;
background-color: #fff3cd;
}
.activity-item.anomaly-critical {
border-left: 3px solid #dc3545;
background-color: #f8d7da;
}
/* Statistics Panel */
.stats-panel {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
}
.license-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f8f9fa;
}
.license-item:last-child {
border-bottom: none;
}
/* Tabs */
.analysis-tabs .nav-link {
color: #495057;
border: none;
border-bottom: 2px solid transparent;
padding: 0.5rem 1rem;
}
.analysis-tabs .nav-link.active {
color: #0d6efd;
border-bottom-color: #0d6efd;
background: none;
}
/* Charts */
.chart-container {
height: 300px;
position: relative;
}
/* Expandable sections */
.expandable-section {
border: 1px solid #dee2e6;
border-radius: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
}
.expandable-header {
background: #f8f9fa;
padding: 0.75rem;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.expandable-header:hover {
background: #e9ecef;
}
.expandable-content {
padding: 1rem;
display: none;
}
.expandable-content.show {
display: block;
}
/* Auto-refresh indicator */
.refresh-indicator {
font-size: 0.875rem;
color: #6c757d;
}
.refresh-indicator.active {
color: #28a745;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.metric-card {
margin-bottom: 1rem;
}
.activity-stream {
max-height: 300px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid p-3">
<!-- Status Header -->
<div class="status-header">
<div class="d-flex justify-content-between align-items-center">
<div class="status-indicator">
{% if system_status == 'normal' %}
<span class="text-success">🟢 System Normal</span>
{% elif system_status == 'warning' %}
<span class="text-warning">🟡 System Warning</span>
{% else %}
<span class="text-danger">🔴 System Critical</span>
{% endif %}
<span class="badge bg-{{ status_color }}">{{ active_alerts }} Aktive Alerts</span>
</div>
<div class="d-flex align-items-center gap-3">
<span class="text-muted">Letzte Aktualisierung: <span id="last-update">jetzt</span></span>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="auto-refresh" checked>
<label class="form-check-label" for="auto-refresh">Auto-Refresh</label>
</div>
</div>
</div>
</div>
<!-- Executive Summary (Collapsible) -->
<div class="expandable-section" id="executive-summary">
<div class="expandable-header" onclick="toggleSection('executive-summary')">
<h5 class="mb-0">📊 Executive Summary</h5>
<i class="bi bi-chevron-down"></i>
</div>
<div class="expandable-content show">
<div class="row g-3">
<div class="col-md-3">
<div class="metric-card">
<div class="metric-label">Aktive Lizenzen</div>
<div class="metric-value text-primary">{{ live_metrics.active_licenses or 0 }}</div>
<div class="metric-trend text-success">
<i class="bi bi-arrow-up"></i> <span class="trend-value">0%</span>
</div>
{% if live_metrics.active_licenses > 100 %}
<div class="metric-alert">!</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-label">Validierungen (5 Min)</div>
<div class="metric-value text-info">{{ live_metrics.total_validations or 0 }}</div>
<div class="metric-trend text-muted">
<i class="bi bi-dash"></i> <span class="trend-value">0%</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-label">Aktive Geräte</div>
<div class="metric-value text-success">{{ live_metrics.unique_devices or 0 }}</div>
<div class="metric-trend text-success">
<i class="bi bi-arrow-up"></i> <span class="trend-value">0%</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div class="metric-label">Response Zeit</div>
<div class="metric-value text-warning">{{ (live_metrics.avg_response_time or 0)|round(1) }}ms</div>
<div class="metric-trend text-warning">
<i class="bi bi-arrow-up"></i> <span class="trend-value">0%</span>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<canvas id="trend-chart" height="80"></canvas>
</div>
</div>
</div>
</div>
<!-- Main Content Area -->
<div class="row g-3">
<!-- Activity Stream (Left Panel) -->
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">🔄 Activity Stream</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary active" data-filter="all">Alle</button>
<button type="button" class="btn btn-outline-success" data-filter="normal">Normal</button>
<button type="button" class="btn btn-outline-warning" data-filter="warning">Warnungen</button>
<button type="button" class="btn btn-outline-danger" data-filter="critical">Kritisch</button>
</div>
</div>
<div class="activity-stream">
{% for event in activity_stream %}
<div class="activity-item {{ event.event_type }} {% if event.event_type == 'anomaly' %}anomaly-{{ event.severity }}{% endif %}"
data-severity="{{ event.severity or 'normal' }}">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-1">
{% if event.event_type == 'validation' %}
<i class="bi bi-check-circle text-success me-2"></i>
{% else %}
<i class="bi bi-exclamation-triangle text-{{ 'warning' if event.severity == 'warning' else 'danger' }} me-2"></i>
{% endif %}
<strong>{{ event.customer_name or 'Unbekannt' }}</strong>
<span class="text-muted ms-2">{{ event.license_key[:8] }}...</span>
</div>
<div class="small text-muted">
{% if event.event_type == 'anomaly' %}
<span class="badge bg-{{ 'warning' if event.severity == 'warning' else 'danger' }} me-2">
{{ event.anomaly_type }}
</span>
{{ event.description }}
{% else %}
Validierung von {{ event.ip_address }} • Gerät: {{ event.hardware_id[:8] }}...
{% endif %}
</div>
</div>
<div class="text-end">
<small class="text-muted">{{ event.timestamp.strftime('%H:%M:%S') if event.timestamp else '-' }}</small>
{% if event.event_type == 'anomaly' and event.severity == 'critical' %}
<div class="mt-1">
<button class="btn btn-sm btn-danger" onclick="blockIP('{{ event.ip_address }}')">
<i class="bi bi-slash-circle"></i> Block
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% if not activity_stream %}
<div class="text-center text-muted p-5">
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">Keine Aktivitäten in den letzten 60 Minuten</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Statistics Panel (Right Panel) -->
<div class="col-lg-4">
<!-- Top Active Licenses -->
<div class="stats-panel mb-3">
<h6 class="mb-3">🏆 Top Aktive Lizenzen</h6>
{% for license in top_licenses %}
<div class="license-item">
<div>
<div class="fw-bold">{{ license.customer_name }}</div>
<small class="text-muted">{{ license.device_count }} Geräte • {{ license.validation_count }} Validierungen</small>
</div>
{% if license.anomaly_count > 0 %}
<span class="badge bg-warning">{{ license.anomaly_count }} ⚠️</span>
{% else %}
<span class="badge bg-success">OK</span>
{% endif %}
</div>
{% endfor %}
{% if not top_licenses %}
<p class="text-muted text-center">Keine aktiven Lizenzen</p>
{% endif %}
</div>
<!-- Anomaly Distribution -->
<div class="stats-panel mb-3">
<h6 class="mb-3">🎯 Anomalie-Verteilung</h6>
<canvas id="anomaly-chart" height="200"></canvas>
{% if not anomaly_distribution %}
<p class="text-muted text-center">Keine Anomalien erkannt</p>
{% endif %}
</div>
<!-- Geographic Distribution -->
<div class="stats-panel">
<h6 class="mb-3">🌍 Geografische Verteilung</h6>
<div style="max-height: 200px; overflow-y: auto;">
{% for geo in geo_data[:5] %}
<div class="d-flex justify-content-between align-items-center py-1">
<span class="text-truncate">{{ geo.ip_address }}</span>
<span class="badge bg-secondary">{{ geo.request_count }}</span>
</div>
{% endfor %}
</div>
{% if geo_data|length > 5 %}
<small class="text-muted">+{{ geo_data|length - 5 }} weitere IPs</small>
{% endif %}
</div>
</div>
</div>
<!-- Analysis Tabs -->
<div class="card mt-4">
<div class="card-header">
<ul class="nav nav-tabs analysis-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#patterns">🔍 Patterns</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#performance">⚡ Performance</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#forensics">🔬 Forensics</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#predictions">📈 Predictions</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane active" id="patterns">
<p class="text-muted">Ungewöhnliche Verhaltensmuster werden hier angezeigt...</p>
</div>
<div class="tab-pane" id="performance">
<div class="chart-container">
<canvas id="performance-chart"></canvas>
</div>
</div>
<div class="tab-pane" id="forensics">
<p class="text-muted">Detaillierte Analyse spezifischer Lizenzen...</p>
</div>
<div class="tab-pane" id="predictions">
<p class="text-muted">Vorhersagen und Kapazitätsplanung...</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Auto-refresh functionality
let refreshInterval;
const AUTO_REFRESH_INTERVAL = 30000; // 30 seconds
function startAutoRefresh() {
if ($('#auto-refresh').is(':checked')) {
refreshInterval = setInterval(() => {
location.reload();
}, AUTO_REFRESH_INTERVAL);
}
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
}
$('#auto-refresh').change(function() {
if (this.checked) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// Toggle expandable sections
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
const content = section.querySelector('.expandable-content');
const icon = section.querySelector('.bi');
content.classList.toggle('show');
icon.classList.toggle('bi-chevron-down');
icon.classList.toggle('bi-chevron-up');
}
// Filter activity stream
document.querySelectorAll('[data-filter]').forEach(button => {
button.addEventListener('click', function() {
const filter = this.dataset.filter;
document.querySelectorAll('[data-filter]').forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
document.querySelectorAll('.activity-item').forEach(item => {
if (filter === 'all') {
item.style.display = 'block';
} else {
const severity = item.dataset.severity;
item.style.display = severity === filter ? 'block' : 'none';
}
});
});
});
// Block IP function
function blockIP(ip) {
if (confirm(`IP-Adresse ${ip} wirklich blockieren?`)) {
// Implementation for blocking IP
alert(`IP ${ip} wurde blockiert.`);
}
}
// Initialize charts
const trendData = {{ trend_data|tojson }};
const anomalyData = {{ anomaly_distribution|tojson }};
const performanceData = {{ performance_data|tojson }};
// Trend Chart
if (trendData && trendData.length > 0) {
const trendCtx = document.getElementById('trend-chart').getContext('2d');
new Chart(trendCtx, {
type: 'line',
data: {
labels: trendData.map(d => new Date(d.hour).toLocaleTimeString('de-DE', {hour: '2-digit'})),
datasets: [{
label: 'Validierungen',
data: trendData.map(d => d.validations),
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true }
}
}
});
}
// Anomaly Distribution Chart
if (anomalyData && anomalyData.length > 0) {
const anomalyCtx = document.getElementById('anomaly-chart').getContext('2d');
new Chart(anomalyCtx, {
type: 'doughnut',
data: {
labels: anomalyData.map(d => d.anomaly_type),
datasets: [{
data: anomalyData.map(d => d.count),
backgroundColor: ['#ffc107', '#dc3545', '#fd7e14', '#6f42c1']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { boxWidth: 12 }
}
}
}
});
}
// Performance Chart
if (performanceData && performanceData.length > 0) {
const perfCtx = document.getElementById('performance-chart').getContext('2d');
new Chart(perfCtx, {
type: 'line',
data: {
labels: performanceData.map(d => new Date(d.minute).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})),
datasets: [{
label: 'Avg Response Time',
data: performanceData.map(d => d.avg_response_time),
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.4
}, {
label: 'Max Response Time',
data: performanceData.map(d => d.max_response_time),
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Response Time (ms)'
}
}
}
}
});
}
// Update last refresh time
function updateLastRefresh() {
const now = new Date();
document.getElementById('last-update').textContent = now.toLocaleTimeString('de-DE');
}
// Start auto-refresh on load
startAutoRefresh();
updateLastRefresh();
</script>
{% endblock %}