453 Zeilen
15 KiB
HTML
453 Zeilen
15 KiB
HTML
{% 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 %} |