Initial commit
Dieser Commit ist enthalten in:
322
v2_adminpanel/templates/monitoring/alerts.html
Normale Datei
322
v2_adminpanel/templates/monitoring/alerts.html
Normale Datei
@ -0,0 +1,322 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Alerts{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.alert-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-left: 5px solid #dee2e6;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.alert-critical {
|
||||
border-left-color: #dc3545;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.alert-high {
|
||||
border-left-color: #fd7e14;
|
||||
background-color: #ffe5d1;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-left-color: #ffc107;
|
||||
background-color: #fff3cd;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-left-color: #17a2b8;
|
||||
background-color: #d1ecf1;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 12px;
|
||||
border-radius: 15px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.severity-critical {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.severity-high {
|
||||
background-color: #fd7e14;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.severity-medium {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.severity-low {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-timestamp {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.alert-stats {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-pills {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-pill.active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="bi bi-exclamation-triangle"></i> Alerts</h2>
|
||||
<p class="text-muted">Aktive Warnungen und Anomalien</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-primary" onclick="location.reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Statistics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-danger">{{ alerts|selectattr('severity', 'equalto', 'critical')|list|length }}</div>
|
||||
<div class="text-muted">Kritisch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-warning">{{ alerts|selectattr('severity', 'equalto', 'high')|list|length }}</div>
|
||||
<div class="text-muted">Hoch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-info">{{ alerts|selectattr('severity', 'equalto', 'medium')|list|length }}</div>
|
||||
<div class="text-muted">Mittel</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="alert-stats">
|
||||
<div class="stat-number text-success">{{ alerts|selectattr('severity', 'equalto', 'low')|list|length }}</div>
|
||||
<div class="text-muted">Niedrig</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Pills -->
|
||||
<div class="filter-pills">
|
||||
<span class="badge bg-secondary filter-pill active me-2" onclick="filterAlerts('all')">
|
||||
Alle ({{ alerts|length }})
|
||||
</span>
|
||||
<span class="badge bg-danger filter-pill me-2" onclick="filterAlerts('critical')">
|
||||
Kritisch
|
||||
</span>
|
||||
<span class="badge bg-warning filter-pill me-2" onclick="filterAlerts('high')">
|
||||
Hoch
|
||||
</span>
|
||||
<span class="badge bg-info filter-pill me-2" onclick="filterAlerts('medium')">
|
||||
Mittel
|
||||
</span>
|
||||
<span class="badge bg-success filter-pill me-2" onclick="filterAlerts('low')">
|
||||
Niedrig
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Alerts List -->
|
||||
<div id="alerts-container">
|
||||
{% for alert in alerts %}
|
||||
<div class="alert-card alert-{{ alert.severity }}" data-severity="{{ alert.severity }}">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<h5 class="mb-0 me-3">
|
||||
{% if alert.anomaly_type == 'multiple_ips' %}
|
||||
<i class="bi bi-geo-alt-fill"></i> Mehrere IP-Adressen erkannt
|
||||
{% elif alert.anomaly_type == 'rapid_hardware_change' %}
|
||||
<i class="bi bi-laptop"></i> Schneller Hardware-Wechsel
|
||||
{% elif alert.anomaly_type == 'suspicious_pattern' %}
|
||||
<i class="bi bi-shield-exclamation"></i> Verdächtiges Muster
|
||||
{% else %}
|
||||
<i class="bi bi-exclamation-circle"></i> {{ alert.anomaly_type }}
|
||||
{% endif %}
|
||||
</h5>
|
||||
<span class="severity-badge severity-{{ alert.severity }}">
|
||||
{{ alert.severity }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if alert.company_name %}
|
||||
<div class="mb-2">
|
||||
<strong>Kunde:</strong> {{ alert.company_name }}
|
||||
{% if alert.license_key %}
|
||||
<span class="text-muted">({{ alert.license_key[:8] }}...)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="alert-timestamp">
|
||||
<i class="bi bi-clock"></i> {{ alert.detected_at|default(alert.startsAt) }}
|
||||
</div>
|
||||
|
||||
{% if alert.details %}
|
||||
<div class="alert-details">
|
||||
<strong>Details:</strong><br>
|
||||
{{ alert.details }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="alert-actions">
|
||||
{% if not alert.resolved %}
|
||||
<button class="btn btn-sm btn-success w-100 mb-2" onclick="resolveAlert('{{ alert.id }}')">
|
||||
<i class="bi bi-check-circle"></i> Als gelöst markieren
|
||||
</button>
|
||||
<button class="btn btn-sm btn-warning w-100 mb-2" onclick="investigateAlert('{{ alert.id }}')">
|
||||
<i class="bi bi-search"></i> Untersuchen
|
||||
</button>
|
||||
{% if alert.severity in ['critical', 'high'] %}
|
||||
<button class="btn btn-sm btn-danger w-100" onclick="blockLicense('{{ alert.license_id }}')">
|
||||
<i class="bi bi-shield-lock"></i> Lizenz blockieren
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-success text-center">
|
||||
<i class="bi bi-check-circle-fill"></i> Gelöst
|
||||
{% if alert.resolved_at %}
|
||||
<div class="small">{{ alert.resolved_at }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield-check" style="font-size: 4rem; color: #28a745;"></i>
|
||||
<h4 class="mt-3">Keine aktiven Alerts</h4>
|
||||
<p class="text-muted">Alle Systeme laufen normal</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Filter alerts by severity
|
||||
function filterAlerts(severity) {
|
||||
// Update active pill
|
||||
document.querySelectorAll('.filter-pill').forEach(pill => {
|
||||
pill.classList.remove('active');
|
||||
});
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Filter alert cards
|
||||
document.querySelectorAll('.alert-card').forEach(card => {
|
||||
if (severity === 'all' || card.dataset.severity === severity) {
|
||||
card.style.display = 'block';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve alert
|
||||
async function resolveAlert(alertId) {
|
||||
if (!confirm('Möchten Sie diesen Alert als gelöst markieren?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/alerts/${alertId}/resolve`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Alert wurde als gelöst markiert');
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Markieren des Alerts');
|
||||
}
|
||||
}
|
||||
|
||||
// Investigate alert
|
||||
function investigateAlert(alertId) {
|
||||
// In production, this would open a detailed investigation view
|
||||
alert('Detaillierte Untersuchung wird geöffnet...');
|
||||
}
|
||||
|
||||
// Block license
|
||||
async function blockLicense(licenseId) {
|
||||
if (!confirm('WARNUNG: Möchten Sie diese Lizenz wirklich blockieren? Der Kunde kann die Software nicht mehr nutzen!')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/licenses/${licenseId}/block`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Lizenz wurde blockiert');
|
||||
location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Blockieren der Lizenz');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh alerts every 60 seconds
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, 60000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
453
v2_adminpanel/templates/monitoring/analytics.html
Normale Datei
453
v2_adminpanel/templates/monitoring/analytics.html
Normale Datei
@ -0,0 +1,453 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Analytics & Lizenzserver Status{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Analytics Styles */
|
||||
.analytics-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.date-range-selector {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.export-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
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="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>
|
||||
|
||||
<!-- Date Range Selector -->
|
||||
<div class="date-range-selector">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<label>Zeitraum auswählen:</label>
|
||||
<select class="form-select" id="date-range" onchange="updateAnalytics()">
|
||||
<option value="today">Heute</option>
|
||||
<option value="week" selected>Letzte 7 Tage</option>
|
||||
<option value="month">Letzte 30 Tage</option>
|
||||
<option value="quarter">Letztes Quartal</option>
|
||||
<option value="year">Letztes Jahr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-outline-primary" onclick="refreshAnalytics()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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">
|
||||
<h5>Berichte exportieren</h5>
|
||||
<div class="export-buttons">
|
||||
<button class="btn btn-outline-success me-2" onclick="exportReport('excel')">
|
||||
<i class="bi bi-file-excel"></i> Excel Export
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="exportReport('csv')">
|
||||
<i class="bi bi-file-text"></i> CSV Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></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')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('active-licenses').textContent = data.active_licenses || '0';
|
||||
document.getElementById('total-validations').textContent = data.validations_last_minute || '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('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) {
|
||||
// Redirect to export endpoint with format parameter
|
||||
const hours = 24; // Default to 24 hours
|
||||
window.location.href = `/export/monitoring?format=${format}&hours=${hours}`;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
698
v2_adminpanel/templates/monitoring/live_dashboard.html
Normale Datei
698
v2_adminpanel/templates/monitoring/live_dashboard.html
Normale Datei
@ -0,0 +1,698 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Live Dashboard & Analytics{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Combined styles from both dashboards */
|
||||
.stats-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #28a745;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.activity-indicator {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.activity-active {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.activity-recent {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
.activity-inactive {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.geo-info {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
background: #f8f9fa;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.85rem;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* 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 & Analytics</h2>
|
||||
<p class="text-muted">Echtzeit-Übersicht und Analyse der Lizenznutzung</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-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" 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-3">
|
||||
<div class="stats-card">
|
||||
<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-3">
|
||||
<div class="stats-card">
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
{% 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>
|
||||
|
||||
<!-- 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> </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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden validation timeline data for chart -->
|
||||
<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 %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
|
||||
<script>
|
||||
let activityChart;
|
||||
let validationChart;
|
||||
let refreshInterval;
|
||||
let refreshCountdown = 30;
|
||||
|
||||
// Initialize activity chart
|
||||
function initActivityChart() {
|
||||
const ctx = document.getElementById('activityChart').getContext('2d');
|
||||
const timelineData = JSON.parse(document.getElementById('validation-timeline-data').textContent);
|
||||
|
||||
// Prepare data for chart
|
||||
const labels = timelineData.map(item => {
|
||||
const date = new Date(item.minute);
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
}).reverse();
|
||||
|
||||
const data = timelineData.map(item => item.validations).reverse();
|
||||
|
||||
activityChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Validierungen',
|
||||
data: data,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.1)',
|
||||
tension: 0.1,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
const timestamp = new Date(el.dataset.timestamp);
|
||||
const now = new Date();
|
||||
const seconds = Math.floor((now - timestamp) / 1000);
|
||||
|
||||
if (seconds < 60) {
|
||||
el.textContent = 'vor wenigen Sekunden';
|
||||
} else if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
el.textContent = `vor ${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
||||
} else {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
el.textContent = `vor ${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh data via AJAX
|
||||
async function refreshData() {
|
||||
try {
|
||||
// Get live stats
|
||||
const statsResponse = await fetch('/monitoring/api/live-stats');
|
||||
const stats = await statsResponse.json();
|
||||
|
||||
// Update stats cards
|
||||
document.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');
|
||||
const sessions = await sessionsResponse.json();
|
||||
|
||||
// Update sessions display
|
||||
updateSessionsDisplay(sessions);
|
||||
|
||||
// Fetch live stats for validation stream
|
||||
fetchLiveStats();
|
||||
|
||||
// Reset countdown
|
||||
refreshCountdown = 30;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update sessions display
|
||||
function updateSessionsDisplay(sessions) {
|
||||
const container = document.getElementById('active-sessions-container');
|
||||
|
||||
if (sessions.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||
<p>Keine aktiven Sessions in den letzten 5 Minuten</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionsHtml = sessions.map(session => {
|
||||
const secondsAgo = Math.floor(session.seconds_ago);
|
||||
let activityClass = 'activity-active';
|
||||
if (secondsAgo > 120) activityClass = 'activity-recent';
|
||||
if (secondsAgo > 240) activityClass = 'activity-inactive';
|
||||
|
||||
return `
|
||||
<div class="session-card">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-1">
|
||||
<span class="activity-indicator ${activityClass}"></span>
|
||||
${session.company_name}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-1">
|
||||
<i class="bi bi-key"></i> ${session.license_key.substring(0, 8)}...
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="geo-info">
|
||||
<i class="bi bi-geo-alt"></i> ${session.ip_address}
|
||||
<div><small>Hardware: ${session.hardware_id.substring(0, 12)}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="text-muted">
|
||||
<i class="bi bi-clock"></i>
|
||||
vor ${formatSecondsAgo(secondsAgo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = sessionsHtml;
|
||||
}
|
||||
|
||||
// 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';
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return `${minutes} Minute${minutes > 1 ? 'n' : ''}`;
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
return `${hours} Stunde${hours > 1 ? 'n' : ''}`;
|
||||
}
|
||||
|
||||
function updateAnalytics() {
|
||||
const range = document.getElementById('date-range').value;
|
||||
console.log('Updating analytics for range:', range);
|
||||
refreshData();
|
||||
}
|
||||
|
||||
function exportReport(format) {
|
||||
// Redirect to export endpoint with format parameter
|
||||
const hours = 24; // Default to 24 hours
|
||||
window.location.href = `/export/monitoring?format=${format}&hours=${hours}`;
|
||||
}
|
||||
|
||||
// Countdown timer
|
||||
function updateCountdown() {
|
||||
refreshCountdown--;
|
||||
document.getElementById('refresh-countdown').textContent = refreshCountdown;
|
||||
|
||||
if (refreshCountdown <= 0) {
|
||||
refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initActivityChart();
|
||||
initValidationChart();
|
||||
updateActivityTimes();
|
||||
|
||||
// Set up auto-refresh
|
||||
refreshInterval = setInterval(() => {
|
||||
updateCountdown();
|
||||
updateActivityTimes();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Clean up on page leave
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
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