Monitoring Anpassung
Dieser Commit ist enthalten in:
@@ -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"])
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|
||||||
# Ensure current month partition exists
|
# Check if tables exist before querying
|
||||||
ensure_partition_exists(conn, 'license_heartbeats', datetime.now())
|
has_heartbeats = check_table_exists(conn, 'license_heartbeats')
|
||||||
|
has_anomalies = check_table_exists(conn, 'anomaly_detections')
|
||||||
|
|
||||||
# Get active customer sessions (last 5 minutes)
|
if has_anomalies:
|
||||||
cur.execute("""
|
# Get active alerts count
|
||||||
SELECT
|
cur.execute("""
|
||||||
l.id,
|
SELECT COUNT(*) as count
|
||||||
l.license_key,
|
FROM anomaly_detections
|
||||||
c.name as company_name,
|
WHERE resolved = false
|
||||||
lh.hardware_id,
|
AND detected_at > NOW() - INTERVAL '24 hours'
|
||||||
lh.ip_address,
|
""")
|
||||||
lh.timestamp as last_activity,
|
active_alerts = cur.fetchone()['count'] or 0
|
||||||
lh.session_data,
|
|
||||||
COUNT(DISTINCT lh.hardware_id) OVER (PARTITION BY l.id) as active_devices
|
# Determine system status based on alerts
|
||||||
FROM license_heartbeats lh
|
if active_alerts == 0:
|
||||||
JOIN licenses l ON l.id = lh.license_id
|
system_status = 'normal'
|
||||||
JOIN customers c ON c.id = l.customer_id
|
status_color = 'success'
|
||||||
WHERE lh.timestamp > NOW() - INTERVAL '5 minutes'
|
elif active_alerts < 5:
|
||||||
AND l.is_active = true
|
system_status = 'warning'
|
||||||
ORDER BY lh.timestamp DESC
|
status_color = 'warning'
|
||||||
LIMIT 100
|
else:
|
||||||
""")
|
system_status = 'critical'
|
||||||
active_sessions = cur.fetchall()
|
status_color = 'danger'
|
||||||
|
|
||||||
# Get session statistics
|
if has_heartbeats:
|
||||||
cur.execute("""
|
# Ensure current month partition exists
|
||||||
SELECT
|
ensure_partition_exists(conn, 'license_heartbeats', datetime.now())
|
||||||
COUNT(DISTINCT license_id) as active_licenses,
|
|
||||||
COUNT(DISTINCT hardware_id) as active_devices,
|
# Executive summary metrics
|
||||||
COUNT(*) as total_heartbeats
|
cur.execute("""
|
||||||
FROM license_heartbeats
|
SELECT
|
||||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
COUNT(DISTINCT license_id) as active_licenses,
|
||||||
""")
|
COUNT(*) as total_validations,
|
||||||
stats = cur.fetchone()
|
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||||
|
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
|
||||||
|
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||||
|
""")
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
live_metrics = result
|
||||||
|
|
||||||
|
# Get 24h trend data for metrics
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('hour', timestamp) as hour,
|
||||||
|
COUNT(DISTINCT license_id) as licenses,
|
||||||
|
COUNT(*) as validations
|
||||||
|
FROM license_heartbeats
|
||||||
|
WHERE timestamp > NOW() - INTERVAL '24 hours'
|
||||||
|
GROUP BY hour
|
||||||
|
ORDER BY hour
|
||||||
|
""")
|
||||||
|
trend_data = cur.fetchall()
|
||||||
|
|
||||||
|
# Activity stream - just validations if no anomalies table
|
||||||
|
if has_anomalies:
|
||||||
|
cur.execute("""
|
||||||
|
WITH combined_events AS (
|
||||||
|
-- Normal validations
|
||||||
|
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'
|
||||||
|
|
||||||
|
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
|
||||||
|
LIMIT 10
|
||||||
|
""")
|
||||||
|
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()
|
||||||
|
|
||||||
# Get validations per minute (for both charts)
|
if has_anomalies:
|
||||||
cur.execute("""
|
# Anomaly distribution
|
||||||
SELECT
|
cur.execute("""
|
||||||
DATE_TRUNC('minute', timestamp) as minute,
|
SELECT
|
||||||
COUNT(*) as validations
|
anomaly_type,
|
||||||
FROM license_heartbeats
|
COUNT(*) as count,
|
||||||
WHERE timestamp > NOW() - INTERVAL '60 minutes'
|
MAX(severity) as max_severity
|
||||||
GROUP BY minute
|
FROM anomaly_detections
|
||||||
ORDER BY minute DESC
|
WHERE detected_at > NOW() - INTERVAL '24 hours'
|
||||||
LIMIT 60
|
GROUP BY anomaly_type
|
||||||
""")
|
ORDER BY count DESC
|
||||||
validation_timeline = cur.fetchall()
|
""")
|
||||||
|
anomaly_distribution = cur.fetchall()
|
||||||
# Get live statistics for analytics cards
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
COUNT(DISTINCT license_id) as active_licenses,
|
|
||||||
COUNT(*) as total_validations,
|
|
||||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
|
||||||
COUNT(DISTINCT ip_address) as unique_ips
|
|
||||||
FROM license_heartbeats
|
|
||||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
|
||||||
""")
|
|
||||||
live_stats_data = cur.fetchone()
|
|
||||||
live_stats = [
|
|
||||||
live_stats_data['active_licenses'] or 0,
|
|
||||||
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)
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('minute', timestamp) as minute,
|
|
||||||
COUNT(*) as count
|
|
||||||
FROM license_heartbeats
|
|
||||||
WHERE timestamp > NOW() - INTERVAL '30 minutes'
|
|
||||||
GROUP BY minute
|
|
||||||
ORDER BY minute DESC
|
|
||||||
LIMIT 30
|
|
||||||
""")
|
|
||||||
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/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,25 +304,26 @@ 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()
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
if check_table_exists(conn, 'anomaly_detections'):
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
cur.execute("""
|
||||||
ad.*,
|
SELECT
|
||||||
l.license_key,
|
ad.*,
|
||||||
c.name as company_name
|
l.license_key,
|
||||||
FROM anomaly_detections ad
|
c.name as company_name
|
||||||
LEFT JOIN licenses l ON l.id = ad.license_id
|
FROM anomaly_detections ad
|
||||||
LEFT JOIN customers c ON c.id = l.customer_id
|
LEFT JOIN licenses l ON l.id = ad.license_id
|
||||||
WHERE ad.resolved = false
|
LEFT JOIN customers c ON c.id = l.customer_id
|
||||||
ORDER BY ad.detected_at DESC
|
WHERE ad.resolved = false
|
||||||
LIMIT 50
|
ORDER BY ad.detected_at DESC
|
||||||
""")
|
LIMIT 50
|
||||||
alerts = cur.fetchall()
|
""")
|
||||||
|
alerts = cur.fetchall()
|
||||||
cur.close()
|
|
||||||
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return render_template('monitoring/alerts.html', alerts=alerts)
|
return render_template('monitoring/alerts.html', alerts=alerts)
|
||||||
@@ -226,146 +336,94 @@ def analytics():
|
|||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
|
||||||
# Get live statistics for the top cards
|
# Initialize default values
|
||||||
cur.execute("""
|
live_stats = [0, 0, 0, 0]
|
||||||
SELECT
|
validation_rates = []
|
||||||
COUNT(DISTINCT license_id) as active_licenses,
|
|
||||||
COUNT(*) as total_validations,
|
|
||||||
COUNT(DISTINCT hardware_id) as unique_devices,
|
|
||||||
COUNT(DISTINCT ip_address) as unique_ips
|
|
||||||
FROM license_heartbeats
|
|
||||||
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
|
||||||
""")
|
|
||||||
live_stats = cur.fetchone()
|
|
||||||
live_stats = [
|
|
||||||
live_stats['active_licenses'] or 0,
|
|
||||||
live_stats['total_validations'] or 0,
|
|
||||||
live_stats['unique_devices'] or 0,
|
|
||||||
live_stats['unique_ips'] or 0
|
|
||||||
]
|
|
||||||
|
|
||||||
# Get validation rates for chart
|
if check_table_exists(conn, 'license_heartbeats'):
|
||||||
cur.execute("""
|
# Get live statistics for the top cards
|
||||||
SELECT
|
cur.execute("""
|
||||||
DATE_TRUNC('minute', timestamp) as minute,
|
SELECT
|
||||||
COUNT(*) as count
|
COUNT(DISTINCT license_id) as active_licenses,
|
||||||
FROM license_heartbeats
|
COUNT(*) as total_validations,
|
||||||
WHERE timestamp > NOW() - INTERVAL '30 minutes'
|
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||||
GROUP BY minute
|
COUNT(DISTINCT ip_address) as unique_ips
|
||||||
ORDER BY minute DESC
|
FROM license_heartbeats
|
||||||
LIMIT 30
|
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||||
""")
|
""")
|
||||||
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
|
live_stats_data = cur.fetchone()
|
||||||
|
live_stats = [
|
||||||
# Get recent anomalies
|
live_stats_data['active_licenses'] or 0,
|
||||||
cur.execute("""
|
live_stats_data['total_validations'] or 0,
|
||||||
SELECT
|
live_stats_data['unique_devices'] or 0,
|
||||||
ad.*,
|
live_stats_data['unique_ips'] or 0
|
||||||
l.license_key,
|
]
|
||||||
c.name as customer_name
|
|
||||||
FROM anomaly_detections ad
|
# Get validation rates for the chart (last 30 minutes, aggregated by minute)
|
||||||
LEFT JOIN licenses l ON l.id = ad.license_id
|
cur.execute("""
|
||||||
LEFT JOIN customers c ON c.id = l.customer_id
|
SELECT
|
||||||
WHERE ad.detected_at > NOW() - INTERVAL '24 hours'
|
DATE_TRUNC('minute', timestamp) as minute,
|
||||||
ORDER BY ad.detected_at DESC
|
COUNT(*) as count
|
||||||
LIMIT 10
|
FROM license_heartbeats
|
||||||
""")
|
WHERE timestamp > NOW() - INTERVAL '30 minutes'
|
||||||
recent_anomalies = cur.fetchall()
|
GROUP BY minute
|
||||||
|
ORDER BY minute DESC
|
||||||
# Get top active licenses
|
LIMIT 30
|
||||||
cur.execute("""
|
""")
|
||||||
SELECT
|
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
|
||||||
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')
|
|
||||||
@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')
|
@monitoring_bp.route('/analytics/stream')
|
||||||
@login_required
|
@login_required
|
||||||
def api_active_sessions():
|
def analytics_stream():
|
||||||
"""API endpoint for active customer sessions"""
|
"""Server-sent event stream for live analytics updates"""
|
||||||
try:
|
def generate():
|
||||||
conn = get_db_connection()
|
while True:
|
||||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
# Get active sessions with geo data
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
data = {'active_licenses': 0, 'total_validations': 0,
|
||||||
l.license_key,
|
'unique_devices': 0, 'unique_ips': 0}
|
||||||
c.name as company_name,
|
|
||||||
lh.hardware_id,
|
if check_table_exists(conn, 'license_heartbeats'):
|
||||||
lh.ip_address,
|
cur.execute("""
|
||||||
lh.timestamp as last_activity,
|
SELECT
|
||||||
EXTRACT(EPOCH FROM (NOW() - lh.timestamp)) as seconds_ago,
|
COUNT(DISTINCT license_id) as active_licenses,
|
||||||
lh.session_data
|
COUNT(*) as total_validations,
|
||||||
FROM license_heartbeats lh
|
COUNT(DISTINCT hardware_id) as unique_devices,
|
||||||
JOIN licenses l ON l.id = lh.license_id
|
COUNT(DISTINCT ip_address) as unique_ips
|
||||||
JOIN customers c ON c.id = l.customer_id
|
FROM license_heartbeats
|
||||||
WHERE lh.timestamp > NOW() - INTERVAL '5 minutes'
|
WHERE timestamp > NOW() - INTERVAL '5 minutes'
|
||||||
ORDER BY lh.timestamp DESC
|
""")
|
||||||
LIMIT 50
|
result = cur.fetchone()
|
||||||
""")
|
if result:
|
||||||
sessions = cur.fetchall()
|
data = dict(result)
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return jsonify(sessions)
|
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"
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(5) # Update every 5 seconds
|
||||||
|
|
||||||
|
from flask import Response
|
||||||
|
return Response(generate(), mimetype="text/event-stream")
|
||||||
@@ -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') }}">
|
||||||
|
|||||||
609
v2_adminpanel/templates/monitoring/unified_monitoring.html
Normale Datei
609
v2_adminpanel/templates/monitoring/unified_monitoring.html
Normale Datei
@@ -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 %}
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren