Lizenzserver im Adminpanel
Dieser Commit ist enthalten in:
@@ -12,3 +12,4 @@ python-dateutil
|
||||
bcrypt
|
||||
pyotp
|
||||
qrcode[pil]
|
||||
PyJWT
|
||||
|
||||
@@ -794,7 +794,12 @@ def license_anomalies():
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Fehler beim Laden der Anomalie-Daten: {str(e)}', 'error')
|
||||
return render_template('license_anomalies.html')
|
||||
return render_template('license_anomalies.html',
|
||||
anomalies=[],
|
||||
anomaly_stats=[],
|
||||
severity='all',
|
||||
resolved='false'
|
||||
)
|
||||
finally:
|
||||
if 'cur' in locals():
|
||||
cur.close()
|
||||
@@ -972,3 +977,25 @@ def license_live_stats():
|
||||
cur.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
@admin_bp.route("/api/admin/license/auth-token")
|
||||
@login_required
|
||||
def get_analytics_token():
|
||||
"""Get JWT token for accessing Analytics Service"""
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Generate a short-lived token for the analytics service
|
||||
payload = {
|
||||
'sub': session.get('user_id', 'admin'),
|
||||
'type': 'analytics_access',
|
||||
'exp': datetime.utcnow() + timedelta(hours=1),
|
||||
'iat': datetime.utcnow()
|
||||
}
|
||||
|
||||
# Use the same secret as configured in the analytics service
|
||||
jwt_secret = os.environ.get('JWT_SECRET', 'your-secret-key')
|
||||
token = jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||
|
||||
return jsonify({'token': token})
|
||||
|
||||
@@ -434,17 +434,11 @@
|
||||
</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="#">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
<a class="nav-link has-submenu" href="{{ url_for('admin.license_monitor') }}">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
<span>Lizenzserver</span>
|
||||
</a>
|
||||
<ul class="sidebar-submenu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'admin.license_monitor' %}active{% endif %}" href="{{ url_for('admin.license_monitor') }}">
|
||||
<i class="bi bi-speedometer2"></i>
|
||||
<span>Live Monitor</span>
|
||||
</a>
|
||||
</li>
|
||||
<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>
|
||||
|
||||
445
v2_adminpanel/templates/license_analytics.html
Normale Datei
445
v2_adminpanel/templates/license_analytics.html
Normale Datei
@@ -0,0 +1,445 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}License Analytics{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h3">License Analytics</h1>
|
||||
|
||||
<!-- Time Range Selector -->
|
||||
<div class="btn-group mb-3" role="group">
|
||||
<a href="{{ url_for('admin.license_analytics', days=7) }}"
|
||||
class="btn btn-sm {% if days == 7 %}btn-primary{% else %}btn-outline-primary{% endif %}">7 Tage</a>
|
||||
<a href="{{ url_for('admin.license_analytics', days=30) }}"
|
||||
class="btn btn-sm {% if days == 30 %}btn-primary{% else %}btn-outline-primary{% endif %}">30 Tage</a>
|
||||
<a href="{{ url_for('admin.license_analytics', days=90) }}"
|
||||
class="btn btn-sm {% if days == 90 %}btn-primary{% else %}btn-outline-primary{% endif %}">90 Tage</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Aktive Lizenzen</h5>
|
||||
<h2 class="text-primary" id="active-licenses">-</h2>
|
||||
<small class="text-muted">In den letzten {{ days }} Tagen</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Aktive Geräte</h5>
|
||||
<h2 class="text-info" id="active-devices">-</h2>
|
||||
<small class="text-muted">Unique Hardware IDs</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Validierungen</h5>
|
||||
<h2 class="text-success" id="total-validations">-</h2>
|
||||
<small class="text-muted">Gesamt in {{ days }} Tagen</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Churn Risk</h5>
|
||||
<h2 class="text-warning" id="churn-risk">-</h2>
|
||||
<small class="text-muted">Kunden mit hohem Risiko</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Trends Chart -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Nutzungstrends</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="usageTrendsChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Metrics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Performance Metriken</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="performanceChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Lizenzverteilung</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="distributionChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Analysis -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Revenue Analysis</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="revenue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lizenztyp</th>
|
||||
<th>Anzahl Lizenzen</th>
|
||||
<th>Aktive Lizenzen</th>
|
||||
<th>Gesamtumsatz</th>
|
||||
<th>Aktiver Umsatz</th>
|
||||
<th>Inaktiver Umsatz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Wird von JavaScript gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Performers -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Top 10 Aktive Lizenzen</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm" id="top-licenses">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lizenz</th>
|
||||
<th>Kunde</th>
|
||||
<th>Geräte</th>
|
||||
<th>Validierungen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Wird von JavaScript gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Churn Risk Kunden</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm" id="churn-risk-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kunde</th>
|
||||
<th>Lizenzen</th>
|
||||
<th>Letzte Aktivität</th>
|
||||
<th>Risk Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Wird von JavaScript gefüllt -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Patterns Heatmap -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Nutzungsmuster (Heatmap)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="usagePatternsChart" height="60"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// Configuration
|
||||
const API_BASE_URL = '/api/v1/analytics';
|
||||
const DAYS = {{ days }};
|
||||
|
||||
// Fetch data from Analytics Service
|
||||
async function fetchAnalyticsData() {
|
||||
try {
|
||||
// Get JWT token first (from license server auth)
|
||||
const tokenResponse = await fetch('/api/admin/license/auth-token');
|
||||
const tokenData = await tokenResponse.json();
|
||||
const token = tokenData.token;
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
|
||||
// Fetch all analytics data
|
||||
const [usage, performance, distribution, revenue, patterns, churnRisk] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/usage?days=${DAYS}`, { headers }).then(r => r.json()),
|
||||
fetch(`${API_BASE_URL}/performance?days=${DAYS}`, { headers }).then(r => r.json()),
|
||||
fetch(`${API_BASE_URL}/distribution`, { headers }).then(r => r.json()),
|
||||
fetch(`${API_BASE_URL}/revenue?days=${DAYS}`, { headers }).then(r => r.json()),
|
||||
fetch(`${API_BASE_URL}/patterns`, { headers }).then(r => r.json()),
|
||||
fetch(`${API_BASE_URL}/churn-risk`, { headers }).then(r => r.json())
|
||||
]);
|
||||
|
||||
// Update UI with fetched data
|
||||
updateSummaryCards(usage.data, distribution.data, churnRisk.data);
|
||||
createUsageTrendsChart(usage.data);
|
||||
createPerformanceChart(performance.data);
|
||||
createDistributionChart(distribution.data);
|
||||
updateRevenueTable(revenue.data);
|
||||
updateChurnRiskTable(churnRisk.data);
|
||||
createUsagePatternsHeatmap(patterns.data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching analytics data:', error);
|
||||
// Fallback to database data if API is not available
|
||||
useFallbackData();
|
||||
}
|
||||
}
|
||||
|
||||
// Update summary cards
|
||||
function updateSummaryCards(usageData, distributionData, churnData) {
|
||||
if (usageData && usageData.length > 0) {
|
||||
const totalValidations = usageData.reduce((sum, day) => sum + day.total_heartbeats, 0);
|
||||
const uniqueLicenses = new Set(usageData.flatMap(day => day.active_licenses)).size;
|
||||
const uniqueDevices = new Set(usageData.flatMap(day => day.active_devices)).size;
|
||||
|
||||
document.getElementById('active-licenses').textContent = uniqueLicenses.toLocaleString();
|
||||
document.getElementById('active-devices').textContent = uniqueDevices.toLocaleString();
|
||||
document.getElementById('total-validations').textContent = totalValidations.toLocaleString();
|
||||
}
|
||||
|
||||
if (churnData && churnData.length > 0) {
|
||||
const highRiskCount = churnData.filter(c => c.churn_risk === 'high').length;
|
||||
document.getElementById('churn-risk').textContent = highRiskCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Create usage trends chart
|
||||
function createUsageTrendsChart(data) {
|
||||
const ctx = document.getElementById('usageTrendsChart').getContext('2d');
|
||||
|
||||
const chartData = {
|
||||
labels: data.map(d => new Date(d.date).toLocaleDateString('de-DE')),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Aktive Lizenzen',
|
||||
data: data.map(d => d.active_licenses),
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
||||
tension: 0.1
|
||||
},
|
||||
{
|
||||
label: 'Aktive Geräte',
|
||||
data: data.map(d => d.active_devices),
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||
tension: 0.1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create performance chart
|
||||
function createPerformanceChart(data) {
|
||||
const ctx = document.getElementById('performanceChart').getContext('2d');
|
||||
|
||||
const chartData = {
|
||||
labels: data.map(d => new Date(d.hour).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit'
|
||||
})),
|
||||
datasets: [{
|
||||
label: 'Validierungen pro Stunde',
|
||||
data: data.map(d => d.validation_count),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.6)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create distribution chart
|
||||
function createDistributionChart(data) {
|
||||
const ctx = document.getElementById('distributionChart').getContext('2d');
|
||||
|
||||
const chartData = {
|
||||
labels: data.map(d => `${d.license_type} (${d.is_test ? 'Test' : 'Prod'})`),
|
||||
datasets: [{
|
||||
label: 'Lizenzverteilung',
|
||||
data: data.map(d => d.active_count),
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 206, 86, 0.6)',
|
||||
'rgba(75, 192, 192, 0.6)',
|
||||
'rgba(153, 102, 255, 0.6)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: chartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update revenue table
|
||||
function updateRevenueTable(data) {
|
||||
const tbody = document.querySelector('#revenue-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${row.license_type}</td>
|
||||
<td>${row.total_licenses}</td>
|
||||
<td>${row.total_licenses > 0 ? Math.round((row.active_revenue / row.total_revenue) * 100) : 0}%</td>
|
||||
<td>€${(row.total_revenue || 0).toFixed(2)}</td>
|
||||
<td>€${(row.active_revenue || 0).toFixed(2)}</td>
|
||||
<td>€${(row.inactive_revenue || 0).toFixed(2)}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Update churn risk table
|
||||
function updateChurnRiskTable(data) {
|
||||
const tbody = document.querySelector('#churn-risk-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.filter(d => d.churn_risk !== 'low').slice(0, 10).forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
const riskClass = row.churn_risk === 'high' ? 'danger' : 'warning';
|
||||
tr.innerHTML = `
|
||||
<td>${row.customer_id}</td>
|
||||
<td>${row.total_licenses}</td>
|
||||
<td>${row.avg_days_since_activity ? Math.round(row.avg_days_since_activity) + ' Tage' : '-'}</td>
|
||||
<td><span class="badge bg-${riskClass}">${row.churn_risk}</span></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// Create usage patterns heatmap
|
||||
function createUsagePatternsHeatmap(data) {
|
||||
// Implementation for heatmap would go here
|
||||
// For now, just log the data
|
||||
console.log('Usage patterns data:', data);
|
||||
}
|
||||
|
||||
// Fallback to use existing database data
|
||||
function useFallbackData() {
|
||||
// Use the data passed from the server template
|
||||
const usageTrends = {{ usage_trends | tojson | safe }};
|
||||
const licenseMetrics = {{ license_metrics | tojson | safe }};
|
||||
const deviceDistribution = {{ device_distribution | tojson | safe }};
|
||||
const revenueAnalysis = {{ revenue_analysis | tojson | safe }};
|
||||
|
||||
// Create charts with fallback data
|
||||
if (usageTrends) {
|
||||
const ctx = document.getElementById('usageTrendsChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: usageTrends.map(d => d[0]),
|
||||
datasets: [{
|
||||
label: 'Aktive Lizenzen',
|
||||
data: usageTrends.map(d => d[1]),
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
tension: 0.1
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
fetchAnalyticsData();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
241
v2_adminpanel/templates/license_anomalies.html
Normale Datei
241
v2_adminpanel/templates/license_anomalies.html
Normale Datei
@@ -0,0 +1,241 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}License Anomalien{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h3">Anomalie-Erkennung</h1>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Schweregrad</label>
|
||||
<select name="severity" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="low" {% if request.args.get('severity') == 'low' %}selected{% endif %}>Niedrig</option>
|
||||
<option value="medium" {% if request.args.get('severity') == 'medium' %}selected{% endif %}>Mittel</option>
|
||||
<option value="high" {% if request.args.get('severity') == 'high' %}selected{% endif %}>Hoch</option>
|
||||
<option value="critical" {% if request.args.get('severity') == 'critical' %}selected{% endif %}>Kritisch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="resolved" class="form-select">
|
||||
<option value="false" {% if request.args.get('resolved', 'false') == 'false' %}selected{% endif %}>Ungelöst</option>
|
||||
<option value="true" {% if request.args.get('resolved') == 'true' %}selected{% endif %}>Gelöst</option>
|
||||
<option value="">Alle</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Anomalie-Typ</label>
|
||||
<select name="anomaly_type" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="multiple_ips" {% if request.args.get('anomaly_type') == 'multiple_ips' %}selected{% endif %}>Multiple IPs</option>
|
||||
<option value="rapid_hardware_change" {% if request.args.get('anomaly_type') == 'rapid_hardware_change' %}selected{% endif %}>Schneller Hardware-Wechsel</option>
|
||||
<option value="suspicious_pattern" {% if request.args.get('anomaly_type') == 'suspicious_pattern' %}selected{% endif %}>Verdächtiges Muster</option>
|
||||
<option value="concurrent_use" {% if request.args.get('anomaly_type') == 'concurrent_use' %}selected{% endif %}>Gleichzeitige Nutzung</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary">Filter anwenden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title text-danger">Kritisch</h5>
|
||||
<h2 class="mb-0">
|
||||
{% set critical_count = namespace(value=0) %}
|
||||
{% for stat in anomaly_stats if stat[1] == 'critical' %}
|
||||
{% set critical_count.value = critical_count.value + stat[2] %}
|
||||
{% endfor %}
|
||||
{{ critical_count.value }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title text-warning">Hoch</h5>
|
||||
<h2 class="mb-0">
|
||||
{% set high_count = namespace(value=0) %}
|
||||
{% for stat in anomaly_stats if stat[1] == 'high' %}
|
||||
{% set high_count.value = high_count.value + stat[2] %}
|
||||
{% endfor %}
|
||||
{{ high_count.value }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title text-info">Mittel</h5>
|
||||
<h2 class="mb-0">
|
||||
{% set medium_count = namespace(value=0) %}
|
||||
{% for stat in anomaly_stats if stat[1] == 'medium' %}
|
||||
{% set medium_count.value = medium_count.value + stat[2] %}
|
||||
{% endfor %}
|
||||
{{ medium_count.value }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title text-muted">Niedrig</h5>
|
||||
<h2 class="mb-0">
|
||||
{% set low_count = namespace(value=0) %}
|
||||
{% for stat in anomaly_stats if stat[1] == 'low' %}
|
||||
{% set low_count.value = low_count.value + stat[2] %}
|
||||
{% endfor %}
|
||||
{{ low_count.value }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anomaly List -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Anomalien</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Lizenz</th>
|
||||
<th>Typ</th>
|
||||
<th>Schweregrad</th>
|
||||
<th>Details</th>
|
||||
<th>Status</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for anomaly in anomalies %}
|
||||
<tr class="{% if anomaly[6] == 'critical' %}table-danger{% elif anomaly[6] == 'high' %}table-warning{% endif %}">
|
||||
<td>{{ anomaly[3].strftime('%d.%m.%Y %H:%M') if anomaly[3] else '-' }}</td>
|
||||
<td>
|
||||
{% if anomaly[8] %}
|
||||
<small>{{ anomaly[8][:8] }}...</small><br>
|
||||
<span class="text-muted">{{ anomaly[9] }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Unbekannt</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ anomaly[5] }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if anomaly[6] == 'critical' %}danger{% elif anomaly[6] == 'high' %}warning{% elif anomaly[6] == 'medium' %}info{% else %}secondary{% endif %}">
|
||||
{{ anomaly[6] }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-link" onclick="showDetails('{{ anomaly[7] }}')">
|
||||
Details anzeigen
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% if anomaly[2] %}
|
||||
<span class="badge bg-success">Gelöst</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Ungelöst</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not anomaly[2] %}
|
||||
<button class="btn btn-sm btn-success" onclick="resolveAnomaly('{{ anomaly[0] }}')">
|
||||
Lösen
|
||||
</button>
|
||||
{% else %}
|
||||
<small class="text-muted">{{ anomaly[4].strftime('%d.%m %H:%M') if anomaly[4] else '' }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">Keine Anomalien gefunden</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div class="modal fade" id="detailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Anomalie Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="anomalyDetails"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolve Modal -->
|
||||
<div class="modal fade" id="resolveModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="resolveForm" method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Anomalie lösen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ergriffene Maßnahme</label>
|
||||
<textarea name="action_taken" class="form-control" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success">Als gelöst markieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function showDetails(detailsJson) {
|
||||
try {
|
||||
const details = JSON.parse(detailsJson);
|
||||
document.getElementById('anomalyDetails').textContent = JSON.stringify(details, null, 2);
|
||||
new bootstrap.Modal(document.getElementById('detailsModal')).show();
|
||||
} catch (e) {
|
||||
alert('Fehler beim Anzeigen der Details');
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAnomaly(anomalyId) {
|
||||
const form = document.getElementById('resolveForm');
|
||||
form.action = `/lizenzserver/anomaly/${anomalyId}/resolve`;
|
||||
new bootstrap.Modal(document.getElementById('resolveModal')).show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
239
v2_adminpanel/templates/license_config.html
Normale Datei
239
v2_adminpanel/templates/license_config.html
Normale Datei
@@ -0,0 +1,239 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Lizenzserver Konfiguration{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h3">Lizenzserver Konfiguration</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature Flags -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Feature Flags</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Status</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for flag in feature_flags %}
|
||||
<tr>
|
||||
<td><strong>{{ flag[1] }}</strong></td>
|
||||
<td><small>{{ flag[2] }}</small></td>
|
||||
<td>
|
||||
{% if flag[3] %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('admin.toggle_feature_flag', flag_id=flag[0]) }}" style="display: inline;">
|
||||
<button type="submit" class="btn btn-sm {% if flag[3] %}btn-danger{% else %}btn-success{% endif %}">
|
||||
{% if flag[3] %}Deaktivieren{% else %}Aktivieren{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Keine Feature Flags konfiguriert</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Clients -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">API Clients</h5>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#newApiClientModal">
|
||||
<i class="bi bi-plus"></i> Neuer Client
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>API Key</th>
|
||||
<th>Status</th>
|
||||
<th>Erstellt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for client in api_clients %}
|
||||
<tr>
|
||||
<td>{{ client[1] }}</td>
|
||||
<td>
|
||||
<code>{{ client[2][:12] }}...</code>
|
||||
<button class="btn btn-sm btn-link" onclick="copyToClipboard('{{ client[2] }}')">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% if client[3] %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ client[4].strftime('%d.%m.%Y') if client[4] else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Keine API Clients vorhanden</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rate Limits -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Rate Limits</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API Key</th>
|
||||
<th>Requests/Minute</th>
|
||||
<th>Requests/Stunde</th>
|
||||
<th>Requests/Tag</th>
|
||||
<th>Burst Size</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for limit in rate_limits %}
|
||||
<tr>
|
||||
<td><code>{{ limit[1][:12] }}...</code></td>
|
||||
<td>{{ limit[2] }}</td>
|
||||
<td>{{ limit[3] }}</td>
|
||||
<td>{{ limit[4] }}</td>
|
||||
<td>{{ limit[5] }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-warning" onclick="editRateLimit('{{ limit[0] }}', {{ limit[2] }}, {{ limit[3] }}, {{ limit[4] }})">
|
||||
Bearbeiten
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Keine Rate Limits konfiguriert</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New API Client Modal -->
|
||||
<div class="modal fade" id="newApiClientModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="/lizenzserver/config/api-client">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Neuer API Client</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea name="description" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Rate Limit Modal -->
|
||||
<div class="modal fade" id="editRateLimitModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="editRateLimitForm" method="post">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Rate Limit bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Requests pro Minute</label>
|
||||
<input type="number" name="requests_per_minute" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Requests pro Stunde</label>
|
||||
<input type="number" name="requests_per_hour" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Requests pro Tag</label>
|
||||
<input type="number" name="requests_per_day" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
alert('API Key wurde in die Zwischenablage kopiert!');
|
||||
});
|
||||
}
|
||||
|
||||
function editRateLimit(id, rpm, rph, rpd) {
|
||||
const form = document.getElementById('editRateLimitForm');
|
||||
form.action = `/lizenzserver/config/rate-limit/${id}`;
|
||||
form.requests_per_minute.value = rpm;
|
||||
form.requests_per_hour.value = rph;
|
||||
form.requests_per_day.value = rpd;
|
||||
new bootstrap.Modal(document.getElementById('editRateLimitModal')).show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren