Vorläufig fertiger server

Dieser Commit ist enthalten in:
2025-06-19 13:17:24 +02:00
Ursprung c30d974d57
Commit f82131b5f9
19 geänderte Dateien mit 1595 neuen und 583 gelöschten Zeilen

Datei anzeigen

@@ -409,24 +409,12 @@
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.audit_log' %}active{% endif %}" href="{{ url_for('admin.audit_log') }}">
<i class="bi bi-journal-text"></i>
<span>Audit-Log</span>
</a>
</li>
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.system_status', 'monitoring.alerts', 'monitoring.analytics'] %}has-active-child{% endif %}">
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.system_status', 'monitoring.alerts', 'admin.audit_log', 'admin.license_monitor', 'admin.license_analytics', 'admin.license_anomalies'] %}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>
@@ -434,53 +422,35 @@
</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 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 class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.backups' %}active{% endif %}" href="{{ url_for('admin.backups') }}">
<i class="bi bi-cloud-download"></i>
<span>Backups</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.blocked_ips' %}active{% endif %}" href="{{ url_for('admin.blocked_ips') }}">
<i class="bi bi-shield-lock"></i>
<span>Sicherheit</span>
</a>
</li>
<li class="nav-item {% if request.endpoint in ['admin.license_monitor', 'admin.license_analytics', 'admin.license_anomalies', 'admin.license_config'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('admin.license_monitor') }}">
<i class="bi bi-speedometer2"></i>
<span>Lizenzserver</span>
<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') }}">
<i class="bi bi-tools"></i>
<span>Administration</span>
</a>
<ul class="sidebar-submenu">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_analytics' %}active{% endif %}" href="{{ url_for('admin.license_analytics') }}">
<i class="bi bi-bar-chart"></i>
<span>Analytics</span>
<a class="nav-link {% if request.endpoint == 'admin.audit_log' %}active{% endif %}" href="{{ url_for('admin.audit_log') }}">
<i class="bi bi-journal-text"></i>
<span>Audit-Log</span>
</a>
</li>
<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-exclamation-triangle"></i>
<span>Anomalien</span>
<a class="nav-link {% if request.endpoint == 'admin.backups' %}active{% endif %}" href="{{ url_for('admin.backups') }}">
<i class="bi bi-cloud-download"></i>
<span>Backups</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_config' %}active{% endif %}" href="{{ url_for('admin.license_config') }}">
<i class="bi bi-gear"></i>
<span>Konfiguration</span>
<a class="nav-link {% if request.endpoint == 'admin.blocked_ips' %}active{% endif %}" href="{{ url_for('admin.blocked_ips') }}">
<i class="bi bi-slash-circle"></i>
<span>Gesperrte IPs</span>
</a>
</li>
</ul>

Datei anzeigen

@@ -94,15 +94,13 @@
</a>
</div>
<div class="col-md-4">
<a href="{{ url_for('sessions.sessions') }}" class="text-decoration-none">
<div class="card stat-card h-100">
<div class="card-body text-center">
<div class="card-icon text-success{% if stats.active_sessions > 0 %} pulse-effect{% endif %}">🟢</div>
<div class="card-value text-success">{{ stats.active_sessions }}</div>
<div class="card-label text-muted">Aktive Sessions</div>
</div>
<div class="card stat-card h-100">
<div class="card-body text-center">
<div class="card-icon text-success{% if stats.active_sessions > 0 %} pulse-effect{% endif %}">🟢</div>
<div class="card-value text-success">{{ stats.active_sessions }}</div>
<div class="card-label text-muted">Aktive Sessions</div>
</div>
</a>
</div>
</div>
</div>

Datei anzeigen

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Fehler{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">
<i class="bi bi-exclamation-triangle-fill"></i>
{{ error_message|default('Ein Fehler ist aufgetreten') }}
</h4>
</div>
<div class="card-body">
{% if details %}
<div class="alert alert-danger">
<h6>Details:</h6>
<pre class="mb-0">{{ details }}</pre>
</div>
{% endif %}
<div class="mt-3">
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Zurück zum Dashboard
</a>
<button onclick="window.history.back();" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Datei anzeigen

@@ -1,319 +0,0 @@
{% extends "base.html" %}
{% block title %}Lizenzserver Monitor{% endblock %}
{% block extra_css %}
<style>
.stat-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: var(--status-active);
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.validation-timeline {
height: 300px;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-speedometer2"></i> Lizenzserver Live Monitor</h1>
<div>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="toggleAutoRefresh()">
<i class="bi bi-arrow-clockwise"></i> Auto-Refresh: <span id="refresh-status">AN</span>
</button>
</div>
</div>
<!-- Live Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="active-licenses">
{{ live_stats[0] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="total-validations">
{{ live_stats[1] if live_stats else 0 }}
</div>
<div class="stat-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-devices">
{{ live_stats[2] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-ips">
{{ live_stats[3] if live_stats else 0 }}
</div>
<div class="stat-label">Unique IPs</div>
</div>
</div>
</div>
<div class="row">
<!-- Validation Timeline -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge badge-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let autoRefresh = true;
let refreshInterval;
let validationChart;
// Initialize validation chart
const ctx = document.getElementById('validationChart').getContext('2d');
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Validierungen',
data: [],
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Update chart with validation rates
{% if validation_rates %}
const rates = {{ validation_rates|tojson }};
validationChart.data.labels = rates.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse();
validationChart.data.datasets[0].data = rates.map(r => r[1]).reverse();
validationChart.update();
{% endif %}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update statistics
document.getElementById('active-licenses').textContent = data.active_licenses;
document.getElementById('total-validations').textContent = data.validations_per_minute;
document.getElementById('unique-devices').textContent = data.active_devices;
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Toggle auto-refresh
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
document.getElementById('refresh-status').textContent = autoRefresh ? 'AN' : 'AUS';
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
} else {
clearInterval(refreshInterval);
}
}
// Start auto-refresh
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
}
// Initial fetch
fetchLiveStats();
</script>
{% endblock %}

Datei anzeigen

@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% block title %}Analytics{% endblock %}
{% block title %}Analytics & Lizenzserver Status{% endblock %}
{% block extra_css %}
<style>
/* Analytics Styles */
.analytics-card {
background: white;
border-radius: 10px;
@@ -69,14 +70,72 @@
padding: 30px;
color: #6c757d;
}
/* License Monitor Styles */
.stat-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: var(--status-active);
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.validation-timeline {
height: 300px;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
</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 class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-bar-chart-line"></i> Analytics & Lizenzserver Status</h1>
<div>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="toggleAutoRefresh()">
<i class="bi bi-arrow-clockwise"></i> Auto-Refresh: <span id="refresh-status">AN</span>
</button>
</div>
</div>
@@ -101,40 +160,145 @@
</div>
</div>
<!-- Key Metrics Overview -->
<!-- Live Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="active-licenses">-</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="active-licenses">
{{ live_stats[0] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="total-validations">-</div>
<div class="stat-label">Validierungen</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="total-validations">
{{ live_stats[1] if live_stats else 0 }}
</div>
<div class="stat-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="active-devices">-</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-devices">
{{ live_stats[2] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-ips">
{{ live_stats[3] if live_stats else 0 }}
</div>
<div class="stat-label">Unique IPs</div>
</div>
</div>
</div>
<!-- Analytics Notice -->
<div class="analytics-card">
<div class="no-data">
<i class="bi bi-info-circle" style="font-size: 3rem; color: #6c757d;"></i>
<h5 class="mt-3">Analytics-Daten werden gesammelt</h5>
<p>Die detaillierten Analysen stehen zur Verfügung, sobald genügend Daten vorhanden sind.</p>
<p>Nutzen Sie das <a href="{{ url_for('monitoring.live_dashboard') }}">Live Dashboard</a> für Echtzeit-Statistiken.</p>
<div class="row">
<!-- Validation Timeline -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge badge-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Export Options -->
<div class="analytics-card mt-4">
<div class="analytics-card">
<h5>Berichte exportieren</h5>
<div class="export-buttons">
<button class="btn btn-outline-primary me-2" onclick="exportReport('pdf')">
@@ -151,7 +315,45 @@
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let autoRefresh = true;
let refreshInterval;
let validationChart;
// Initialize validation chart
const ctx = document.getElementById('validationChart').getContext('2d');
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Validierungen',
data: [],
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Update chart with validation rates
{% if validation_rates %}
const rates = {{ validation_rates|tojson }};
validationChart.data.labels = rates.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse();
validationChart.data.datasets[0].data = rates.map(r => r[1]).reverse();
validationChart.update();
{% endif %}
function loadAnalyticsData() {
// Load basic statistics from database
fetch('/monitoring/api/live-stats')
@@ -159,33 +361,92 @@
.then(data => {
document.getElementById('active-licenses').textContent = data.active_licenses || '0';
document.getElementById('total-validations').textContent = data.validations_last_minute || '0';
document.getElementById('active-devices').textContent = data.active_devices || '0';
document.getElementById('unique-devices').textContent = data.active_devices || '0';
})
.catch(error => {
console.error('Error loading analytics:', error);
document.getElementById('active-licenses').textContent = '0';
document.getElementById('total-validations').textContent = '0';
document.getElementById('active-devices').textContent = '0';
document.getElementById('unique-devices').textContent = '0';
});
}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update statistics
document.getElementById('active-licenses').textContent = data.active_licenses;
document.getElementById('total-validations').textContent = data.validations_per_minute;
document.getElementById('unique-devices').textContent = data.active_devices;
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Toggle auto-refresh
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
document.getElementById('refresh-status').textContent = autoRefresh ? 'AN' : 'AUS';
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
} else {
clearInterval(refreshInterval);
}
}
function updateAnalytics() {
const range = document.getElementById('date-range').value;
console.log('Updating analytics for range:', range);
loadAnalyticsData();
fetchLiveStats();
}
function refreshAnalytics() {
loadAnalyticsData();
fetchLiveStats();
}
function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`);
}
// Start auto-refresh
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
}
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadAnalyticsData();
fetchLiveStats();
// Refresh every 30 seconds
setInterval(loadAnalyticsData, 30000);
});

Datei anzeigen

@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% block title %}Live Dashboard{% endblock %}
{% block title %}Live Dashboard & Analytics{% endblock %}
{% block extra_css %}
<style>
/* Combined styles from both dashboards */
.stats-card {
background: white;
border-radius: 10px;
@@ -11,6 +12,7 @@
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
height: 100%;
}
.stats-card:hover {
@@ -83,17 +85,69 @@
display: inline-block;
margin: 2px;
}
/* Analytics specific styles */
.chart-container {
position: relative;
height: 300px;
margin-top: 20px;
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
.nav-tabs .nav-link {
color: #495057;
border: none;
border-bottom: 3px solid transparent;
padding: 0.5rem 1rem;
}
.nav-tabs .nav-link.active {
color: var(--bs-primary);
background: none;
border-bottom-color: var(--bs-primary);
}
.tab-content {
padding-top: 1.5rem;
}
</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>
<h2><i class="bi bi-activity"></i> Live Dashboard & Analytics</h2>
<p class="text-muted">Echtzeit-Übersicht und Analyse der Lizenznutzung</p>
</div>
<div class="col-auto">
<span class="text-muted">Auto-Refresh: <span id="refresh-countdown">30</span>s</span>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<span class="text-muted ms-3">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>
@@ -102,86 +156,261 @@
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="col-md-3">
<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 class="stats-number text-primary" id="active-licenses">{{ live_stats[0] if live_stats else 0 }}</div>
<div class="stats-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<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>
<i class="bi bi-shield-check text-success" style="font-size: 2rem;"></i>
<div class="stats-number text-success" id="total-validations">{{ live_stats[1] if live_stats else 0 }}</div>
<div class="stats-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<i class="bi bi-laptop text-info" style="font-size: 2rem;"></i>
<div class="stats-number text-info" id="unique-devices">{{ live_stats[2] if live_stats else 0 }}</div>
<div class="stats-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<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>
<i class="bi bi-globe text-warning" style="font-size: 2rem;"></i>
<div class="stats-number text-warning" id="unique-ips">{{ live_stats[3] if live_stats else 0 }}</div>
<div class="stats-label">Unique IPs</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>
<!-- Tabbed Interface -->
<ul class="nav nav-tabs" id="dashboardTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">
<i class="bi bi-speedometer2"></i> Übersicht
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="sessions-tab" data-bs-toggle="tab" data-bs-target="#sessions" type="button" role="tab">
<i class="bi bi-people"></i> Aktive Sessions
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="analytics-tab" data-bs-toggle="tab" data-bs-target="#analytics" type="button" role="tab">
<i class="bi bi-bar-chart-line"></i> Analytics
</button>
</li>
</ul>
<!-- Active Sessions -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions</h5>
<div class="tab-content" id="dashboardTabContent">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row">
<!-- Activity Timeline Chart -->
<div class="col-md-8 mb-4">
<div class="card">
<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>
</div>
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge bg-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</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)
<!-- Active Sessions Tab -->
<div class="tab-pane fade" id="sessions" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions (letzte 5 Minuten)</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>
<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>
{% 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>
<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>
{% endfor %}
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
</div>
<!-- Analytics Tab -->
<div class="tab-pane fade" id="analytics" role="tabpanel">
<div class="row">
<div class="col-md-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute (30 Min)</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
</div>
<!-- Export Options -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Berichte exportieren</h5>
</div>
<div class="card-body">
<div class="row">
<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">
<label>&nbsp;</label>
<div>
<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>
</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>
@@ -190,6 +419,9 @@
<script id="validation-timeline-data" type="application/json">
{{ validation_timeline|tojson }}
</script>
<script id="validation-rates-data" type="application/json">
{{ validation_rates|tojson }}
</script>
{% endblock %}
{% block extra_js %}
@@ -197,6 +429,7 @@
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
<script>
let activityChart;
let validationChart;
let refreshInterval;
let refreshCountdown = 30;
@@ -243,6 +476,35 @@
});
}
// Initialize validation chart
function initValidationChart() {
const ctx = document.getElementById('validationChart').getContext('2d');
const ratesData = JSON.parse(document.getElementById('validation-rates-data').textContent);
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: ratesData.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse(),
datasets: [{
label: 'Validierungen',
data: ratesData.map(r => r[1]).reverse(),
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Update last activity times
function updateActivityTimes() {
document.querySelectorAll('.last-activity').forEach(el => {
@@ -270,9 +532,9 @@
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;
document.getElementById('active-licenses').textContent = stats.active_licenses || 0;
document.getElementById('unique-devices').textContent = stats.active_devices || 0;
document.getElementById('total-validations').textContent = stats.validations_last_minute || 0;
// Get active sessions
const sessionsResponse = await fetch('/monitoring/api/active-sessions');
@@ -281,6 +543,9 @@
// Update sessions display
updateSessionsDisplay(sessions);
// Fetch live stats for validation stream
fetchLiveStats();
// Reset countdown
refreshCountdown = 30;
@@ -343,6 +608,40 @@
container.innerHTML = sessionsHtml;
}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Format seconds ago
function formatSecondsAgo(seconds) {
if (seconds < 60) return 'wenigen Sekunden';
@@ -354,6 +653,16 @@
return `${hours} Stunde${hours > 1 ? 'n' : ''}`;
}
function updateAnalytics() {
const range = document.getElementById('date-range').value;
console.log('Updating analytics for range:', range);
refreshData();
}
function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`);
}
// Countdown timer
function updateCountdown() {
refreshCountdown--;
@@ -367,6 +676,7 @@
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initActivityChart();
initValidationChart();
updateActivityTimes();
// Set up auto-refresh

Datei anzeigen

@@ -182,23 +182,6 @@
</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">