Lizenzserver im Adminpanel

Dieser Commit ist enthalten in:
2025-06-18 22:48:22 +02:00
Ursprung 86d805c392
Commit 6d1a52b7e3
17 geänderte Dateien mit 2483 neuen und 32 gelöschten Zeilen

Datei anzeigen

@@ -12,3 +12,4 @@ python-dateutil
bcrypt
pyotp
qrcode[pil]
PyJWT

Datei anzeigen

@@ -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})

Datei anzeigen

@@ -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>

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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 %}

Datei anzeigen

@@ -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 %}