Dieser Commit ist enthalten in:
2025-06-08 22:37:03 +02:00
Ursprung fb83559d58
Commit 472998d16b
11 geänderte Dateien mit 499 neuen und 85 gelöschten Zeilen

Datei anzeigen

@@ -1394,4 +1394,61 @@ ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME Z
- ✅ Copy-Buttons mit Clipboard-API - ✅ Copy-Buttons mit Clipboard-API
- ✅ Toggle-Switches ändern Lizenzstatus - ✅ Toggle-Switches ändern Lizenzstatus
- ✅ Bulk-Operationen vollständig implementiert - ✅ Bulk-Operationen vollständig implementiert
- ✅ Testdaten erfolgreich eingefügt - ✅ Testdaten erfolgreich eingefügt
### 2025-06-08: UI/UX Überarbeitung - Phase 4 (Sortierbare Tabellen)
**Problem:**
- Keine Möglichkeit, Tabellen nach verschiedenen Spalten zu sortieren
- Besonders bei großen Datenmengen schwer zu navigieren
**Lösung - Hybrid-Ansatz:**
1. **Client-seitige Sortierung für kleine Tabellen:**
- Dashboard (3 kleine Übersichtstabellen)
- Blocked IPs (typischerweise wenige Einträge)
- Backups (begrenzte Anzahl)
- JavaScript-basierte Sortierung ohne Reload
2. **Server-seitige Sortierung für große Tabellen:**
- Licenses (potenziell tausende Einträge)
- Customers (viele Kunden möglich)
- Audit Log (wächst kontinuierlich)
- Sessions (viele aktive/beendete Sessions)
- URL-Parameter für Sortierung mit SQL ORDER BY
**Implementierung:**
1. **Client-seitige Sortierung:**
- Generische JavaScript-Funktion in base.html
- CSS-Klasse `.sortable-table` für betroffene Tabellen
- Sortier-Indikatoren (↑↓↕) bei Hover/Active
- Unterstützung für Text, Zahlen und deutsche Datumsformate
2. **Server-seitige Sortierung:**
- Query-Parameter `sort` und `order` in Routes
- Whitelist für erlaubte Sortierfelder (SQL-Injection-Schutz)
- Makro-Funktionen für sortierbare Header
- Sortier-Parameter in Pagination-Links erhalten
**Geänderte Dateien:**
- `v2_adminpanel/templates/base.html`: CSS und JavaScript für Sortierung
- `v2_adminpanel/templates/dashboard.html`: Client-seitige Sortierung
- `v2_adminpanel/templates/blocked_ips.html`: Client-seitige Sortierung
- `v2_adminpanel/templates/backups.html`: Client-seitige Sortierung
- `v2_adminpanel/templates/licenses.html`: Server-seitige Sortierung
- `v2_adminpanel/templates/customers.html`: Server-seitige Sortierung
- `v2_adminpanel/templates/audit_log.html`: Server-seitige Sortierung
- `v2_adminpanel/templates/sessions.html`: Server-seitige Sortierung (2 Tabellen)
- `v2_adminpanel/app.py`: 4 Routes erweitert für Sortierung
**Besonderheiten:**
- Sessions-Seite hat zwei unabhängige Tabellen mit eigenen Sortierparametern
- Intelligente Datentyp-Erkennung (numeric, date) für korrekte Sortierung
- Visuelle Sortier-Indikatoren zeigen aktuelle Sortierung
- Alle anderen Filter und Suchparameter bleiben bei Sortierung erhalten
**Status:**
- ✅ Client-seitige Sortierung für kleine Tabellen
- ✅ Server-seitige Sortierung für große Tabellen
- ✅ Sortier-Indikatoren und visuelle Rückmeldung
- ✅ SQL-Injection-Schutz durch Whitelisting
- ✅ Vollständige Integration mit bestehenden Features

Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist

Datei anzeigen

@@ -1323,8 +1323,31 @@ def licenses():
filter_type = request.args.get('type', '') filter_type = request.args.get('type', '')
filter_status = request.args.get('status', '') filter_status = request.args.get('status', '')
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
sort = request.args.get('sort', 'valid_until')
order = request.args.get('order', 'desc')
per_page = 20 per_page = 20
# Whitelist für erlaubte Sortierfelder
allowed_sort_fields = {
'id': 'l.id',
'license_key': 'l.license_key',
'customer': 'c.name',
'email': 'c.email',
'type': 'l.license_type',
'valid_from': 'l.valid_from',
'valid_until': 'l.valid_until',
'status': 'status',
'active': 'l.is_active'
}
# Validierung
if sort not in allowed_sort_fields:
sort = 'valid_until'
if order not in ['asc', 'desc']:
order = 'desc'
sort_field = allowed_sort_fields[sort]
# SQL Query mit optionaler Suche und Filtern # SQL Query mit optionaler Suche und Filtern
query = """ query = """
SELECT l.id, l.license_key, c.name, c.email, l.license_type, SELECT l.id, l.license_key, c.name, c.email, l.license_type,
@@ -1374,7 +1397,20 @@ def licenses():
# Pagination # Pagination
offset = (page - 1) * per_page offset = (page - 1) * per_page
query += " ORDER BY l.valid_until DESC LIMIT %s OFFSET %s"
# Spezialbehandlung für berechnete Felder
if sort == 'status':
# Für Status müssen wir die CASE-Bedingung in ORDER BY wiederholen
query += f""" ORDER BY
CASE
WHEN l.is_active = FALSE THEN 'deaktiviert'
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab'
ELSE 'aktiv'
END {order.upper()} LIMIT %s OFFSET %s"""
else:
query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
params.extend([per_page, offset]) params.extend([per_page, offset])
cur.execute(query, params) cur.execute(query, params)
@@ -1394,6 +1430,8 @@ def licenses():
page=page, page=page,
total_pages=total_pages, total_pages=total_pages,
total=total, total=total,
sort=sort,
order=order,
username=session.get('username')) username=session.get('username'))
@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"]) @app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
@@ -1508,8 +1546,28 @@ def customers():
# Parameter # Parameter
search = request.args.get('search', '').strip() search = request.args.get('search', '').strip()
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
sort = request.args.get('sort', 'created_at')
order = request.args.get('order', 'desc')
per_page = 20 per_page = 20
# Whitelist für erlaubte Sortierfelder
allowed_sort_fields = {
'id': 'c.id',
'name': 'c.name',
'email': 'c.email',
'created_at': 'c.created_at',
'licenses': 'license_count',
'active_licenses': 'active_licenses'
}
# Validierung
if sort not in allowed_sort_fields:
sort = 'created_at'
if order not in ['asc', 'desc']:
order = 'desc'
sort_field = allowed_sort_fields[sort]
# SQL Query mit optionaler Suche # SQL Query mit optionaler Suche
base_query = """ base_query = """
SELECT c.id, c.name, c.email, c.created_at, SELECT c.id, c.name, c.email, c.created_at,
@@ -1544,9 +1602,9 @@ def customers():
# Pagination # Pagination
offset = (page - 1) * per_page offset = (page - 1) * per_page
query = base_query + """ query = base_query + f"""
GROUP BY c.id, c.name, c.email, c.created_at GROUP BY c.id, c.name, c.email, c.created_at
ORDER BY c.created_at DESC ORDER BY {sort_field} {order.upper()}
LIMIT %s OFFSET %s LIMIT %s OFFSET %s
""" """
params.extend([per_page, offset]) params.extend([per_page, offset])
@@ -1566,6 +1624,8 @@ def customers():
page=page, page=page,
total_pages=total_pages, total_pages=total_pages,
total=total, total=total,
sort=sort,
order=order,
username=session.get('username')) username=session.get('username'))
@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"]) @app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
@@ -1678,8 +1738,44 @@ def sessions():
conn = get_connection() conn = get_connection()
cur = conn.cursor() cur = conn.cursor()
# Sortierparameter
active_sort = request.args.get('active_sort', 'last_heartbeat')
active_order = request.args.get('active_order', 'desc')
ended_sort = request.args.get('ended_sort', 'ended_at')
ended_order = request.args.get('ended_order', 'desc')
# Whitelist für erlaubte Sortierfelder - Aktive Sessions
active_sort_fields = {
'customer': 'c.name',
'license': 'l.license_key',
'ip': 's.ip_address',
'started': 's.started_at',
'last_heartbeat': 's.last_heartbeat',
'inactive': 'minutes_inactive'
}
# Whitelist für erlaubte Sortierfelder - Beendete Sessions
ended_sort_fields = {
'customer': 'c.name',
'license': 'l.license_key',
'ip': 's.ip_address',
'started': 's.started_at',
'ended_at': 's.ended_at',
'duration': 'duration_minutes'
}
# Validierung
if active_sort not in active_sort_fields:
active_sort = 'last_heartbeat'
if ended_sort not in ended_sort_fields:
ended_sort = 'ended_at'
if active_order not in ['asc', 'desc']:
active_order = 'desc'
if ended_order not in ['asc', 'desc']:
ended_order = 'desc'
# Aktive Sessions abrufen # Aktive Sessions abrufen
cur.execute(""" cur.execute(f"""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.user_agent, s.started_at, s.last_heartbeat, s.user_agent, s.started_at, s.last_heartbeat,
EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
@@ -1687,12 +1783,12 @@ def sessions():
JOIN licenses l ON s.license_id = l.id JOIN licenses l ON s.license_id = l.id
JOIN customers c ON l.customer_id = c.id JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = TRUE WHERE s.is_active = TRUE
ORDER BY s.last_heartbeat DESC ORDER BY {active_sort_fields[active_sort]} {active_order.upper()}
""") """)
active_sessions = cur.fetchall() active_sessions = cur.fetchall()
# Inaktive Sessions der letzten 24 Stunden # Inaktive Sessions der letzten 24 Stunden
cur.execute(""" cur.execute(f"""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.started_at, s.ended_at, s.started_at, s.ended_at,
EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
@@ -1701,7 +1797,7 @@ def sessions():
JOIN customers c ON l.customer_id = c.id JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = FALSE WHERE s.is_active = FALSE
AND s.ended_at > NOW() - INTERVAL '24 hours' AND s.ended_at > NOW() - INTERVAL '24 hours'
ORDER BY s.ended_at DESC ORDER BY {ended_sort_fields[ended_sort]} {ended_order.upper()}
LIMIT 50 LIMIT 50
""") """)
recent_sessions = cur.fetchall() recent_sessions = cur.fetchall()
@@ -1712,6 +1808,10 @@ def sessions():
return render_template("sessions.html", return render_template("sessions.html",
active_sessions=active_sessions, active_sessions=active_sessions,
recent_sessions=recent_sessions, recent_sessions=recent_sessions,
active_sort=active_sort,
active_order=active_order,
ended_sort=ended_sort,
ended_order=ended_order,
username=session.get('username')) username=session.get('username'))
@app.route("/session/end/<int:session_id>", methods=["POST"]) @app.route("/session/end/<int:session_id>", methods=["POST"])
@@ -1914,8 +2014,27 @@ def audit_log():
filter_action = request.args.get('action', '').strip() filter_action = request.args.get('action', '').strip()
filter_entity = request.args.get('entity', '').strip() filter_entity = request.args.get('entity', '').strip()
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
sort = request.args.get('sort', 'timestamp')
order = request.args.get('order', 'desc')
per_page = 50 per_page = 50
# Whitelist für erlaubte Sortierfelder
allowed_sort_fields = {
'timestamp': 'timestamp',
'username': 'username',
'action': 'action',
'entity': 'entity_type',
'ip': 'ip_address'
}
# Validierung
if sort not in allowed_sort_fields:
sort = 'timestamp'
if order not in ['asc', 'desc']:
order = 'desc'
sort_field = allowed_sort_fields[sort]
# SQL Query mit optionalen Filtern # SQL Query mit optionalen Filtern
query = """ query = """
SELECT id, timestamp, username, action, entity_type, entity_id, SELECT id, timestamp, username, action, entity_type, entity_id,
@@ -1946,7 +2065,7 @@ def audit_log():
# Pagination # Pagination
offset = (page - 1) * per_page offset = (page - 1) * per_page
query += " ORDER BY timestamp DESC LIMIT %s OFFSET %s" query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
params.extend([per_page, offset]) params.extend([per_page, offset])
cur.execute(query, params) cur.execute(query, params)
@@ -1974,6 +2093,8 @@ def audit_log():
page=page, page=page,
total_pages=total_pages, total_pages=total_pages,
total=total, total=total,
sort=sort,
order=order,
username=session.get('username')) username=session.get('username'))
@app.route("/backups") @app.route("/backups")

Datei anzeigen

@@ -2,6 +2,27 @@
{% block title %}Log{% endblock %} {% block title %}Log{% endblock %}
{% macro sortable_header(label, field, current_sort, current_order) %}
<th>
{% if current_sort == field %}
<a href="{{ url_for('audit_log', sort=field, order='desc' if current_order == 'asc' else 'asc', user=filter_user, action=filter_action, entity=filter_entity, page=1) }}"
class="server-sortable">
{% else %}
<a href="{{ url_for('audit_log', sort=field, order='asc', user=filter_user, action=filter_action, entity=filter_entity, page=1) }}"
class="server-sortable">
{% endif %}
{{ label }}
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
{% if current_sort == field %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% endmacro %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.audit-details { .audit-details {
@@ -95,12 +116,12 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Zeitstempel</th> {{ sortable_header('Zeitstempel', 'timestamp', sort, order) }}
<th>Benutzer</th> {{ sortable_header('Benutzer', 'username', sort, order) }}
<th>Aktion</th> {{ sortable_header('Aktion', 'action', sort, order) }}
<th>Entität</th> {{ sortable_header('Entität', 'entity', sort, order) }}
<th>Details</th> <th>Details</th>
<th>IP-Adresse</th> {{ sortable_header('IP-Adresse', 'ip', sort, order) }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -194,31 +215,31 @@
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
<!-- Erste Seite --> <!-- Erste Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('audit_log', page=1, user=filter_user, action=filter_action, entity=filter_entity) }}">Erste</a> <a class="page-link" href="{{ url_for('audit_log', page=1, user=filter_user, action=filter_action, entity=filter_entity, sort=sort, order=order) }}">Erste</a>
</li> </li>
<!-- Vorherige Seite --> <!-- Vorherige Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('audit_log', page=page-1, user=filter_user, action=filter_action, entity=filter_entity) }}"></a> <a class="page-link" href="{{ url_for('audit_log', page=page-1, user=filter_user, action=filter_action, entity=filter_entity, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Seitenzahlen --> <!-- Seitenzahlen -->
{% for p in range(1, total_pages + 1) %} {% for p in range(1, total_pages + 1) %}
{% if p >= page - 2 and p <= page + 2 %} {% if p >= page - 2 and p <= page + 2 %}
<li class="page-item {% if p == page %}active{% endif %}"> <li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('audit_log', page=p, user=filter_user, action=filter_action, entity=filter_entity) }}">{{ p }}</a> <a class="page-link" href="{{ url_for('audit_log', page=p, user=filter_user, action=filter_action, entity=filter_entity, sort=sort, order=order) }}">{{ p }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- Nächste Seite --> <!-- Nächste Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('audit_log', page=page+1, user=filter_user, action=filter_action, entity=filter_entity) }}"></a> <a class="page-link" href="{{ url_for('audit_log', page=page+1, user=filter_user, action=filter_action, entity=filter_entity, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Letzte Seite --> <!-- Letzte Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('audit_log', page=total_pages, user=filter_user, action=filter_action, entity=filter_entity) }}">Letzte</a> <a class="page-link" href="{{ url_for('audit_log', page=total_pages, user=filter_user, action=filter_action, entity=filter_entity, sort=sort, order=order) }}">Letzte</a>
</li> </li>
</ul> </ul>
<p class="text-center text-muted"> <p class="text-center text-muted">

Datei anzeigen

@@ -58,15 +58,15 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover sortable-table">
<thead> <thead>
<tr> <tr>
<th>Zeitstempel</th> <th class="sortable" data-type="date">Zeitstempel</th>
<th>Dateiname</th> <th class="sortable">Dateiname</th>
<th>Größe</th> <th class="sortable" data-type="numeric">Größe</th>
<th>Typ</th> <th class="sortable">Typ</th>
<th>Status</th> <th class="sortable">Status</th>
<th>Erstellt von</th> <th class="sortable">Erstellt von</th>
<th>Details</th> <th>Details</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>

Datei anzeigen

@@ -162,6 +162,61 @@
width: 1.2em; width: 1.2em;
height: 1.2em; height: 1.2em;
} }
/* Sortable Table Styles */
.sortable-table th.sortable {
cursor: pointer;
user-select: none;
position: relative;
padding-right: 25px;
}
.sortable-table th.sortable:hover {
background-color: #e9ecef;
}
.sortable-table th.sortable::after {
content: '↕';
position: absolute;
right: 8px;
opacity: 0.3;
}
.sortable-table th.sortable.asc::after {
content: '↑';
opacity: 1;
color: var(--status-active);
}
.sortable-table th.sortable.desc::after {
content: '↓';
opacity: 1;
color: var(--status-active);
}
/* Server-side sortable styles */
.server-sortable {
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
justify-content: space-between;
}
.server-sortable:hover {
color: var(--bs-primary);
text-decoration: none;
}
.sort-indicator {
margin-left: 5px;
font-size: 0.8em;
}
.sort-indicator.active {
color: var(--bs-primary);
}
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
@@ -314,6 +369,81 @@
// Initial Heartbeat // Initial Heartbeat
extendSession(); extendSession();
// Client-side table sorting
document.addEventListener('DOMContentLoaded', function() {
// Initialize all sortable tables
const sortableTables = document.querySelectorAll('.sortable-table');
sortableTables.forEach(table => {
const headers = table.querySelectorAll('th.sortable');
headers.forEach((header, index) => {
header.addEventListener('click', function() {
sortTable(table, index, header);
});
});
});
});
function sortTable(table, columnIndex, header) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const isNumeric = header.dataset.type === 'numeric';
const isDate = header.dataset.type === 'date';
// Determine sort direction
let direction = 'asc';
if (header.classList.contains('asc')) {
direction = 'desc';
}
// Remove all sort classes from headers
table.querySelectorAll('th.sortable').forEach(th => {
th.classList.remove('asc', 'desc');
});
// Add appropriate class to clicked header
header.classList.add(direction);
// Sort rows
rows.sort((a, b) => {
let aValue = a.cells[columnIndex].textContent.trim();
let bValue = b.cells[columnIndex].textContent.trim();
// Handle different data types
if (isNumeric) {
aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
} else if (isDate) {
// Parse German date format (DD.MM.YYYY HH:MM)
aValue = parseGermanDate(aValue);
bValue = parseGermanDate(bValue);
} else {
// Text comparison with locale support for umlauts
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
}
if (aValue < bValue) return direction === 'asc' ? -1 : 1;
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
return 0;
});
// Reorder rows in DOM
rows.forEach(row => tbody.appendChild(row));
}
function parseGermanDate(dateStr) {
// Handle DD.MM.YYYY HH:MM format
const parts = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(\d{2}:\d{2})?/);
if (parts) {
const [_, day, month, year, time] = parts;
const timeStr = time || '00:00';
return new Date(`${year}-${month}-${day}T${timeStr}`);
}
return new Date(0);
}
</script> </script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}

Datei anzeigen

@@ -18,17 +18,17 @@
<div class="card-body"> <div class="card-body">
{% if blocked_ips %} {% if blocked_ips %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover sortable-table">
<thead> <thead>
<tr> <tr>
<th>IP-Adresse</th> <th class="sortable">IP-Adresse</th>
<th>Versuche</th> <th class="sortable" data-type="numeric">Versuche</th>
<th>Erster Versuch</th> <th class="sortable" data-type="date">Erster Versuch</th>
<th>Letzter Versuch</th> <th class="sortable" data-type="date">Letzter Versuch</th>
<th>Gesperrt bis</th> <th class="sortable" data-type="date">Gesperrt bis</th>
<th>Letzter User</th> <th class="sortable">Letzter User</th>
<th>Letzte Meldung</th> <th class="sortable">Letzte Meldung</th>
<th>Status</th> <th class="sortable">Status</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>

Datei anzeigen

@@ -2,6 +2,27 @@
{% block title %}Kundenverwaltung{% endblock %} {% block title %}Kundenverwaltung{% endblock %}
{% macro sortable_header(label, field, current_sort, current_order) %}
<th>
{% if current_sort == field %}
<a href="{{ url_for('customers', sort=field, order='desc' if current_order == 'asc' else 'asc', search=search, page=1) }}"
class="server-sortable">
{% else %}
<a href="{{ url_for('customers', sort=field, order='asc', search=search, page=1) }}"
class="server-sortable">
{% endif %}
{{ label }}
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
{% if current_sort == field %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% endmacro %}
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@@ -51,11 +72,11 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>ID</th> {{ sortable_header('ID', 'id', sort, order) }}
<th>Name</th> {{ sortable_header('Name', 'name', sort, order) }}
<th>E-Mail</th> {{ sortable_header('E-Mail', 'email', sort, order) }}
<th>Erstellt am</th> {{ sortable_header('Erstellt am', 'created_at', sort, order) }}
<th>Lizenzen (Aktiv/Gesamt)</th> {{ sortable_header('Lizenzen (Aktiv/Gesamt)', 'licenses', sort, order) }}
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@@ -105,31 +126,31 @@
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
<!-- Erste Seite --> <!-- Erste Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('customers', page=1, search=search) }}">Erste</a> <a class="page-link" href="{{ url_for('customers', page=1, search=search, sort=sort, order=order) }}">Erste</a>
</li> </li>
<!-- Vorherige Seite --> <!-- Vorherige Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('customers', page=page-1, search=search) }}"></a> <a class="page-link" href="{{ url_for('customers', page=page-1, search=search, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Seitenzahlen --> <!-- Seitenzahlen -->
{% for p in range(1, total_pages + 1) %} {% for p in range(1, total_pages + 1) %}
{% if p >= page - 2 and p <= page + 2 %} {% if p >= page - 2 and p <= page + 2 %}
<li class="page-item {% if p == page %}active{% endif %}"> <li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('customers', page=p, search=search) }}">{{ p }}</a> <a class="page-link" href="{{ url_for('customers', page=p, search=search, sort=sort, order=order) }}">{{ p }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- Nächste Seite --> <!-- Nächste Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('customers', page=page+1, search=search) }}"></a> <a class="page-link" href="{{ url_for('customers', page=page+1, search=search, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Letzte Seite --> <!-- Letzte Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('customers', page=total_pages, search=search) }}">Letzte</a> <a class="page-link" href="{{ url_for('customers', page=total_pages, search=search, sort=sort, order=order) }}">Letzte</a>
</li> </li>
</ul> </ul>
<p class="text-center text-muted"> <p class="text-center text-muted">

Datei anzeigen

@@ -221,14 +221,14 @@
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm mb-0"> <table class="table table-sm mb-0 sortable-table">
<thead> <thead>
<tr> <tr>
<th>Zeit</th> <th class="sortable" data-type="date">Zeit</th>
<th>IP-Adresse</th> <th class="sortable">IP-Adresse</th>
<th>Versuche</th> <th class="sortable" data-type="numeric">Versuche</th>
<th>Fehlermeldung</th> <th class="sortable">Fehlermeldung</th>
<th>Status</th> <th class="sortable">Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -266,12 +266,12 @@
<div class="card-body"> <div class="card-body">
{% if stats.expiring_licenses %} {% if stats.expiring_licenses %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm"> <table class="table table-sm sortable-table">
<thead> <thead>
<tr> <tr>
<th>Kunde</th> <th class="sortable">Kunde</th>
<th>Lizenz</th> <th class="sortable">Lizenz</th>
<th>Tage</th> <th class="sortable" data-type="numeric">Tage</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -301,12 +301,12 @@
<div class="card-body"> <div class="card-body">
{% if stats.recent_licenses %} {% if stats.recent_licenses %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm"> <table class="table table-sm sortable-table">
<thead> <thead>
<tr> <tr>
<th>Kunde</th> <th class="sortable">Kunde</th>
<th>Lizenz</th> <th class="sortable">Lizenz</th>
<th>Status</th> <th class="sortable">Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

Datei anzeigen

@@ -2,6 +2,27 @@
{% block title %}Lizenzübersicht{% endblock %} {% block title %}Lizenzübersicht{% endblock %}
{% macro sortable_header(label, field, current_sort, current_order) %}
<th>
{% if current_sort == field %}
<a href="{{ url_for('licenses', sort=field, order='desc' if current_order == 'asc' else 'asc', search=search, type=filter_type, status=filter_status, page=1) }}"
class="server-sortable">
{% else %}
<a href="{{ url_for('licenses', sort=field, order='asc', search=search, type=filter_type, status=filter_status, page=1) }}"
class="server-sortable">
{% endif %}
{{ label }}
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
{% if current_sort == field %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% endmacro %}
{% block extra_css %} {% block extra_css %}
{% endblock %} {% endblock %}
@@ -82,15 +103,15 @@
<th class="checkbox-cell"> <th class="checkbox-cell">
<input type="checkbox" class="form-check-input form-check-input-custom" id="selectAll"> <input type="checkbox" class="form-check-input form-check-input-custom" id="selectAll">
</th> </th>
<th>ID</th> {{ sortable_header('ID', 'id', sort, order) }}
<th>Lizenzschlüssel</th> {{ sortable_header('Lizenzschlüssel', 'license_key', sort, order) }}
<th>Kunde</th> {{ sortable_header('Kunde', 'customer', sort, order) }}
<th>E-Mail</th> {{ sortable_header('E-Mail', 'email', sort, order) }}
<th>Typ</th> {{ sortable_header('Typ', 'type', sort, order) }}
<th>Gültig von</th> {{ sortable_header('Gültig von', 'valid_from', sort, order) }}
<th>Gültig bis</th> {{ sortable_header('Gültig bis', 'valid_until', sort, order) }}
<th>Status</th> {{ sortable_header('Status', 'status', sort, order) }}
<th>Aktiv</th> {{ sortable_header('Aktiv', 'active', sort, order) }}
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@@ -169,31 +190,31 @@
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
<!-- Erste Seite --> <!-- Erste Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('licenses', page=1, search=search, type=filter_type, status=filter_status) }}">Erste</a> <a class="page-link" href="{{ url_for('licenses', page=1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Erste</a>
</li> </li>
<!-- Vorherige Seite --> <!-- Vorherige Seite -->
<li class="page-item {% if page == 1 %}disabled{% endif %}"> <li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('licenses', page=page-1, search=search, type=filter_type, status=filter_status) }}"></a> <a class="page-link" href="{{ url_for('licenses', page=page-1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Seitenzahlen --> <!-- Seitenzahlen -->
{% for p in range(1, total_pages + 1) %} {% for p in range(1, total_pages + 1) %}
{% if p >= page - 2 and p <= page + 2 %} {% if p >= page - 2 and p <= page + 2 %}
<li class="page-item {% if p == page %}active{% endif %}"> <li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('licenses', page=p, search=search, type=filter_type, status=filter_status) }}">{{ p }}</a> <a class="page-link" href="{{ url_for('licenses', page=p, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">{{ p }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- Nächste Seite --> <!-- Nächste Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('licenses', page=page+1, search=search, type=filter_type, status=filter_status) }}"></a> <a class="page-link" href="{{ url_for('licenses', page=page+1, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}"></a>
</li> </li>
<!-- Letzte Seite --> <!-- Letzte Seite -->
<li class="page-item {% if page == total_pages %}disabled{% endif %}"> <li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('licenses', page=total_pages, search=search, type=filter_type, status=filter_status) }}">Letzte</a> <a class="page-link" href="{{ url_for('licenses', page=total_pages, search=search, type=filter_type, status=filter_status, sort=sort, order=order) }}">Letzte</a>
</li> </li>
</ul> </ul>
<p class="text-center text-muted"> <p class="text-center text-muted">

Datei anzeigen

@@ -2,6 +2,48 @@
{% block title %}Session-Tracking{% endblock %} {% block title %}Session-Tracking{% endblock %}
{% macro active_sortable_header(label, field, current_sort, current_order) %}
<th>
{% if current_sort == field %}
<a href="{{ url_for('sessions', active_sort=field, active_order='desc' if current_order == 'asc' else 'asc', ended_sort=ended_sort, ended_order=ended_order) }}"
class="server-sortable">
{% else %}
<a href="{{ url_for('sessions', active_sort=field, active_order='asc', ended_sort=ended_sort, ended_order=ended_order) }}"
class="server-sortable">
{% endif %}
{{ label }}
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
{% if current_sort == field %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% endmacro %}
{% macro ended_sortable_header(label, field, current_sort, current_order) %}
<th>
{% if current_sort == field %}
<a href="{{ url_for('sessions', active_sort=active_sort, active_order=active_order, ended_sort=field, ended_order='desc' if current_order == 'asc' else 'asc') }}"
class="server-sortable">
{% else %}
<a href="{{ url_for('sessions', active_sort=active_sort, active_order=active_order, ended_sort=field, ended_order='asc') }}"
class="server-sortable">
{% endif %}
{{ label }}
<span class="sort-indicator{% if current_sort == field %} active{% endif %}">
{% if current_sort == field %}
{% if current_order == 'asc' %}↑{% else %}↓{% endif %}
{% else %}
{% endif %}
</span>
</a>
</th>
{% endmacro %}
{% block extra_css %} {% block extra_css %}
<style> <style>
.session-active { background-color: #d4edda; } .session-active { background-color: #d4edda; }
@@ -30,12 +72,12 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Kunde</th> {{ active_sortable_header('Kunde', 'customer', active_sort, active_order) }}
<th>Lizenz</th> {{ active_sortable_header('Lizenz', 'license', active_sort, active_order) }}
<th>IP-Adresse</th> {{ active_sortable_header('IP-Adresse', 'ip', active_sort, active_order) }}
<th>Gestartet</th> {{ active_sortable_header('Gestartet', 'started', active_sort, active_order) }}
<th>Letzter Heartbeat</th> {{ active_sortable_header('Letzter Heartbeat', 'last_heartbeat', active_sort, active_order) }}
<th>Inaktiv seit</th> {{ active_sortable_header('Inaktiv seit', 'inactive', active_sort, active_order) }}
<th>Aktion</th> <th>Aktion</th>
</tr> </tr>
</thead> </thead>
@@ -89,12 +131,12 @@
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Kunde</th> {{ ended_sortable_header('Kunde', 'customer', ended_sort, ended_order) }}
<th>Lizenz</th> {{ ended_sortable_header('Lizenz', 'license', ended_sort, ended_order) }}
<th>IP-Adresse</th> {{ ended_sortable_header('IP-Adresse', 'ip', ended_sort, ended_order) }}
<th>Gestartet</th> {{ ended_sortable_header('Gestartet', 'started', ended_sort, ended_order) }}
<th>Beendet</th> {{ ended_sortable_header('Beendet', 'ended_at', ended_sort, ended_order) }}
<th>Dauer</th> {{ ended_sortable_header('Dauer', 'duration', ended_sort, ended_order) }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>