Add latest changes
Dieser Commit ist enthalten in:
@@ -219,7 +219,7 @@ function createBackup(type) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Erstelle...';
|
||||
|
||||
fetch('/backups/backup/create', {
|
||||
fetch('/admin/backup/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -249,7 +249,7 @@ function createBackup(type) {
|
||||
}
|
||||
|
||||
function downloadFromGitHub(backupId) {
|
||||
window.location.href = `/backups/backup/download/${backupId}?from_github=true`;
|
||||
window.location.href = `/admin/backup/download/${backupId}?from_github=true`;
|
||||
}
|
||||
|
||||
function showRestoreModal(backupId) {
|
||||
@@ -261,7 +261,7 @@ function showRestoreModal(backupId) {
|
||||
function confirmRestore() {
|
||||
const encryptionKey = document.getElementById('encryptionKey').value;
|
||||
|
||||
fetch(`/backups/backup/restore/${selectedBackupId}`, {
|
||||
fetch(`/admin/backup/restore/${selectedBackupId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<div id="session-timer" class="timer-normal me-3">
|
||||
⏱️ <span id="timer-display">5:00</span>
|
||||
⏱️ <span id="timer-display">15:00</span>
|
||||
</div>
|
||||
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
|
||||
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-light btn-sm me-2">👤 Profil</a>
|
||||
@@ -482,7 +482,7 @@
|
||||
|
||||
<script>
|
||||
// Session-Timer Konfiguration
|
||||
const SESSION_TIMEOUT = 5 * 60; // 5 Minuten in Sekunden
|
||||
const SESSION_TIMEOUT = 15 * 60; // 15 Minuten in Sekunden
|
||||
let timeRemaining = SESSION_TIMEOUT;
|
||||
let timerInterval;
|
||||
let warningShown = false;
|
||||
|
||||
@@ -174,6 +174,19 @@
|
||||
Jede generierte Lizenz kann auf maximal dieser Anzahl von Geräten gleichzeitig aktiviert werden.
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">
|
||||
Max. gleichzeitige Sessions pro Lizenz
|
||||
</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% for i in range(1, 11) %}
|
||||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Wie viele Geräte können gleichzeitig online sein. Muss kleiner oder gleich dem Gerätelimit sein.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,6 +259,30 @@ document.getElementById('validFrom').addEventListener('change', calculateValidUn
|
||||
document.getElementById('duration').addEventListener('input', calculateValidUntil);
|
||||
document.getElementById('durationType').addEventListener('change', calculateValidUntil);
|
||||
|
||||
// Funktion zur Anpassung der max_concurrent_sessions Optionen
|
||||
function updateConcurrentSessionsOptions() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSelect = document.getElementById('concurrentSessions');
|
||||
const currentValue = parseInt(concurrentSelect.value);
|
||||
|
||||
// Clear current options
|
||||
concurrentSelect.innerHTML = '';
|
||||
|
||||
// Add new options up to device limit
|
||||
for (let i = 1; i <= Math.min(deviceLimit, 10); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.text = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(currentValue, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
concurrentSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listener für Device Limit Änderungen
|
||||
document.getElementById('deviceLimit').addEventListener('change', updateConcurrentSessionsOptions);
|
||||
|
||||
// Setze heutiges Datum als Standard
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
@@ -510,5 +547,51 @@ function showCustomerTypeIndicator(type) {
|
||||
function hideCustomerTypeIndicator() {
|
||||
document.getElementById('customerTypeIndicator').classList.add('d-none');
|
||||
}
|
||||
|
||||
// Validation for concurrent sessions vs device limit
|
||||
document.getElementById('deviceLimit').addEventListener('change', validateSessionLimit);
|
||||
document.getElementById('concurrentSessions').addEventListener('change', validateSessionLimit);
|
||||
|
||||
function validateSessionLimit() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSessions = parseInt(document.getElementById('concurrentSessions').value);
|
||||
const sessionsSelect = document.getElementById('concurrentSessions');
|
||||
|
||||
// Update options to not exceed device limit
|
||||
sessionsSelect.innerHTML = '';
|
||||
for (let i = 1; i <= Math.min(10, deviceLimit); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.textContent = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(concurrentSessions, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
sessionsSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Show warning if adjusted
|
||||
if (concurrentSessions > deviceLimit) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-warning border-0 position-fixed bottom-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Gleichzeitige Sessions wurden auf ${deviceLimit} angepasst (Max. Gerätelimit).
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
setTimeout(() => toast.remove(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize validation on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateSessionLimit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -367,6 +367,7 @@ function updateLicenseView(customerId, licenses) {
|
||||
<th>Gültig bis</th>
|
||||
<th>Status</th>
|
||||
<th>Server Status</th>
|
||||
<th>Sessions</th>
|
||||
<th>Ressourcen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -448,6 +449,11 @@ function updateLicenseView(customerId, licenses) {
|
||||
<td>${license.valid_until || '-'}</td>
|
||||
<td><span class="badge ${statusClass}">${license.status}</span></td>
|
||||
<td>${serverStatusHtml}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
${license.active_sessions || 0}/${license.max_concurrent_sessions || 1}
|
||||
</span>
|
||||
</td>
|
||||
<td class="resources-cell">
|
||||
${resourcesHtml || '<span class="text-muted">-</span>'}
|
||||
</td>
|
||||
@@ -1086,7 +1092,7 @@ function showDeviceManagement(licenseId) {
|
||||
content += `
|
||||
<tr>
|
||||
<td>${device.device_name}</td>
|
||||
<td><small class="text-muted">${device.hardware_id.substring(0, 12)}...</small></td>
|
||||
<td><small class="text-muted">${device.hardware_fingerprint.substring(0, 12)}...</small></td>
|
||||
<td>${device.operating_system}</td>
|
||||
<td>${device.first_seen}</td>
|
||||
<td>${device.last_seen}</td>
|
||||
|
||||
@@ -156,6 +156,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Utilization -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-broadcast"></i> Session-Auslastung
|
||||
<span class="badge bg-info float-end">{{ stats.session_stats.total_active_sessions or 0 }} aktive Sessions</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-primary">{{ stats.session_stats.total_active_sessions or 0 }}</h3>
|
||||
<p class="text-muted mb-0">Aktive Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-success">{{ stats.session_stats.total_max_sessions or 0 }}</h3>
|
||||
<p class="text-muted mb-0">Maximale Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<h3 class="text-warning">{{ stats.session_stats.utilization_percent or 0 }}%</h3>
|
||||
<p class="text-muted mb-0">Auslastung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if stats.session_stats.licenses_at_limit > 0 %}
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>{{ stats.session_stats.licenses_at_limit }}</strong> Lizenz(en) haben ihr Session-Limit erreicht
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service Health Status -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
|
||||
@@ -65,6 +65,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">Max. gleichzeitige Sessions</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% set device_limit = license.get('device_limit', 3) %}
|
||||
{% set upper_limit = device_limit + 1 if device_limit < 11 else 11 %}
|
||||
{% for i in range(1, upper_limit) %}
|
||||
<option value="{{ i }}" {% if license.get('max_concurrent_sessions', 1) == i %}selected{% endif %}>
|
||||
{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">Wie viele Geräte können gleichzeitig online sein</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert {% if license.is_fake %}alert-warning{% else %}alert-success{% endif %} mt-3" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Status:</strong>
|
||||
|
||||
@@ -153,6 +153,19 @@
|
||||
Anzahl der Geräte, auf denen die Lizenz gleichzeitig aktiviert sein kann.
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="concurrentSessions" class="form-label">
|
||||
Max. gleichzeitige Sessions
|
||||
</label>
|
||||
<select class="form-select" id="concurrentSessions" name="max_concurrent_sessions" required>
|
||||
{% for i in range(1, 11) %}
|
||||
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }} {% if i == 1 %}Session{% else %}Sessions{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Wie viele Geräte können gleichzeitig online sein. Muss kleiner oder gleich dem Gerätelimit sein.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -574,5 +587,51 @@ function showCustomerTypeIndicator(type) {
|
||||
function hideCustomerTypeIndicator() {
|
||||
document.getElementById('customerTypeIndicator').classList.add('d-none');
|
||||
}
|
||||
|
||||
// Validation for concurrent sessions vs device limit
|
||||
document.getElementById('deviceLimit').addEventListener('change', validateSessionLimit);
|
||||
document.getElementById('concurrentSessions').addEventListener('change', validateSessionLimit);
|
||||
|
||||
function validateSessionLimit() {
|
||||
const deviceLimit = parseInt(document.getElementById('deviceLimit').value);
|
||||
const concurrentSessions = parseInt(document.getElementById('concurrentSessions').value);
|
||||
const sessionsSelect = document.getElementById('concurrentSessions');
|
||||
|
||||
// Update options to not exceed device limit
|
||||
sessionsSelect.innerHTML = '';
|
||||
for (let i = 1; i <= Math.min(10, deviceLimit); i++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = i;
|
||||
option.textContent = i + (i === 1 ? ' Session' : ' Sessions');
|
||||
if (i === Math.min(concurrentSessions, deviceLimit)) {
|
||||
option.selected = true;
|
||||
}
|
||||
sessionsSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// Show warning if adjusted
|
||||
if (concurrentSessions > deviceLimit) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-warning border-0 position-fixed bottom-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Gleichzeitige Sessions wurden auf ${deviceLimit} angepasst (Max. Gerätelimit).
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
setTimeout(() => toast.remove(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize validation on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
validateSessionLimit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
{{ sortable_header('Gültig von', 'valid_from', sort, order) }}
|
||||
{{ sortable_header('Gültig bis', 'valid_until', sort, order) }}
|
||||
{{ sortable_header('Status', 'status', sort, order) }}
|
||||
<th>Sessions</th>
|
||||
{{ sortable_header('Aktiv', 'active', sort, order) }}
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -225,6 +226,11 @@
|
||||
<span class="status-aktiv">✅ Aktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
{{ license.active_sessions or 0 }}/{{ license.max_concurrent_sessions or 1 }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-check form-switch form-switch-custom">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
|
||||
@@ -384,7 +384,7 @@
|
||||
`<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 class="text-muted">${v.hardware_fingerprint}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="badge bg-secondary">${v.ip_address}</span>
|
||||
|
||||
127
v2_adminpanel/templates/monitoring/device_limits.html
Normale Datei
127
v2_adminpanel/templates/monitoring/device_limits.html
Normale Datei
@@ -0,0 +1,127 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Device Limit Monitoring{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<h1 class="h3 mb-4">Device Limit Monitoring</h1>
|
||||
|
||||
<!-- Overall Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total Devices</h5>
|
||||
<p class="card-text display-6">{{ stats.total_devices }}</p>
|
||||
<small class="text-muted">of {{ stats.total_device_limit }} allowed</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Overall Usage</h5>
|
||||
<p class="card-text display-6">{{ stats.usage_percent }}%</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar {% if stats.usage_percent > 90 %}bg-danger{% elif stats.usage_percent > 70 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
role="progressbar" style="width: {{ stats.usage_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Active (24h)</h5>
|
||||
<p class="card-text display-6">{{ stats.active_24h }}</p>
|
||||
<small class="text-muted">devices seen today</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Active (7d)</h5>
|
||||
<p class="card-text display-6">{{ stats.active_7d }}</p>
|
||||
<small class="text-muted">devices seen this week</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warnings -->
|
||||
{% if warnings %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Device Limit Warnings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>License Key</th>
|
||||
<th>Customer</th>
|
||||
<th>Devices</th>
|
||||
<th>Limit</th>
|
||||
<th>Usage</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for warning in warnings %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('license_bp.license_detail', license_id=warning.license_id) }}">
|
||||
{{ warning.license_key }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ warning.customer_name }}<br>
|
||||
<small class="text-muted">{{ warning.customer_email }}</small>
|
||||
</td>
|
||||
<td>{{ warning.device_count }}</td>
|
||||
<td>{{ warning.device_limit }}</td>
|
||||
<td>
|
||||
<div class="progress" style="width: 100px;">
|
||||
<div class="progress-bar {% if warning.status == 'exceeded' %}bg-danger{% else %}bg-warning{% endif %}"
|
||||
role="progressbar" style="width: {{ warning.usage_percent }}%">
|
||||
{{ warning.usage_percent }}%
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if warning.status == 'exceeded' %}
|
||||
<span class="badge bg-danger">Exceeded</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Warning</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('license_bp.license_detail', license_id=warning.license_id) }}#devices"
|
||||
class="btn btn-sm btn-primary">
|
||||
View Devices
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i> All licenses are within their device limits.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-refresh every 30 seconds
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -327,7 +327,7 @@
|
||||
<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><small>Hardware: {{ session.hardware_fingerprint[:12] }}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
@@ -591,7 +591,7 @@
|
||||
<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><small>Hardware: ${session.hardware_fingerprint.substring(0, 12)}...</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
@@ -619,7 +619,7 @@
|
||||
`<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 class="text-muted">${v.hardware_fingerprint}</span>
|
||||
</span>
|
||||
<span>
|
||||
<span class="badge bg-secondary">${v.ip_address}</span>
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
</span>
|
||||
{{ event.description }}
|
||||
{% else %}
|
||||
Validierung von {{ event.ip_address }} • Gerät: {{ event.hardware_id[:8] }}...
|
||||
Validierung von {{ event.ip_address }} • Gerät: {{ event.hardware_fingerprint[:8] }}...
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren