UI Verbesserung (Bei Log war Button)
Dieser Commit ist enthalten in:
@@ -29,6 +29,7 @@ from io import BytesIO
|
||||
import base64
|
||||
import json
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -2797,6 +2798,130 @@ def export_licenses():
|
||||
download_name=f'{filename}.xlsx'
|
||||
)
|
||||
|
||||
@app.route("/export/audit")
|
||||
@login_required
|
||||
def export_audit():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Holen der Filter-Parameter
|
||||
filter_user = request.args.get('user', '')
|
||||
filter_action = request.args.get('action', '')
|
||||
filter_entity = request.args.get('entity', '')
|
||||
export_format = request.args.get('format', 'excel')
|
||||
|
||||
# SQL Query mit Filtern
|
||||
query = """
|
||||
SELECT id, timestamp, username, action, entity_type, entity_id,
|
||||
old_values, new_values, ip_address, user_agent, additional_info
|
||||
FROM audit_log
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if filter_user:
|
||||
query += " AND username ILIKE %s"
|
||||
params.append(f'%{filter_user}%')
|
||||
|
||||
if filter_action:
|
||||
query += " AND action = %s"
|
||||
params.append(filter_action)
|
||||
|
||||
if filter_entity:
|
||||
query += " AND entity_type = %s"
|
||||
params.append(filter_entity)
|
||||
|
||||
query += " ORDER BY timestamp DESC"
|
||||
|
||||
cur.execute(query, params)
|
||||
audit_logs = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
for log in audit_logs:
|
||||
action_text = {
|
||||
'CREATE': 'Erstellt',
|
||||
'UPDATE': 'Bearbeitet',
|
||||
'DELETE': 'Gelöscht',
|
||||
'LOGIN': 'Anmeldung',
|
||||
'LOGOUT': 'Abmeldung',
|
||||
'AUTO_LOGOUT': 'Auto-Logout',
|
||||
'EXPORT': 'Export',
|
||||
'GENERATE_KEY': 'Key generiert',
|
||||
'CREATE_BATCH': 'Batch erstellt',
|
||||
'BACKUP': 'Backup erstellt',
|
||||
'LOGIN_2FA_SUCCESS': '2FA-Anmeldung',
|
||||
'LOGIN_2FA_BACKUP': '2FA-Backup-Code',
|
||||
'LOGIN_2FA_FAILED': '2FA-Fehlgeschlagen',
|
||||
'LOGIN_BLOCKED': 'Login-Blockiert',
|
||||
'RESTORE': 'Wiederhergestellt',
|
||||
'PASSWORD_CHANGE': 'Passwort geändert',
|
||||
'2FA_ENABLED': '2FA aktiviert',
|
||||
'2FA_DISABLED': '2FA deaktiviert'
|
||||
}.get(log[3], log[3])
|
||||
|
||||
data.append({
|
||||
'ID': log[0],
|
||||
'Zeitstempel': log[1].strftime('%d.%m.%Y %H:%M:%S'),
|
||||
'Benutzer': log[2],
|
||||
'Aktion': action_text,
|
||||
'Entität': log[4],
|
||||
'Entität-ID': log[5] or '',
|
||||
'IP-Adresse': log[8] or '',
|
||||
'Zusatzinfo': log[10] or ''
|
||||
})
|
||||
|
||||
# DataFrame erstellen
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Timestamp für Dateiname
|
||||
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f'audit_log_export_{timestamp}'
|
||||
|
||||
# Audit Log für Export
|
||||
log_audit('EXPORT', 'audit_log',
|
||||
additional_info=f"{export_format.upper()} Export mit {len(data)} Einträgen")
|
||||
|
||||
if export_format == 'csv':
|
||||
# CSV Export
|
||||
output = io.StringIO()
|
||||
# UTF-8 BOM für Excel
|
||||
output.write('\ufeff')
|
||||
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||
mimetype='text/csv;charset=utf-8',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.csv'
|
||||
)
|
||||
else:
|
||||
# Excel Export
|
||||
output = BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False, sheet_name='Audit Log')
|
||||
|
||||
# Spaltenbreiten anpassen
|
||||
worksheet = writer.sheets['Audit Log']
|
||||
for idx, col in enumerate(df.columns):
|
||||
max_length = max(
|
||||
df[col].astype(str).map(len).max(),
|
||||
len(col)
|
||||
) + 2
|
||||
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.xlsx'
|
||||
)
|
||||
|
||||
@app.route("/export/customers")
|
||||
@login_required
|
||||
def export_customers():
|
||||
@@ -2878,6 +3003,273 @@ def export_customers():
|
||||
download_name=f'{filename}.xlsx'
|
||||
)
|
||||
|
||||
@app.route("/export/sessions")
|
||||
@login_required
|
||||
def export_sessions():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Holen des Session-Typs (active oder ended)
|
||||
session_type = request.args.get('type', 'active')
|
||||
export_format = request.args.get('format', 'excel')
|
||||
|
||||
# Daten je nach Typ abrufen
|
||||
if session_type == 'active':
|
||||
# Aktive Lizenz-Sessions
|
||||
cur.execute("""
|
||||
SELECT s.id, l.license_key, c.name as customer_name, s.session_id,
|
||||
s.started_at, s.last_heartbeat,
|
||||
EXTRACT(EPOCH FROM (NOW() - s.started_at))::INT as duration_seconds,
|
||||
s.ip_address, s.user_agent
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_id = l.id
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
WHERE s.is_active = true
|
||||
ORDER BY s.last_heartbeat DESC
|
||||
""")
|
||||
sessions = cur.fetchall()
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
for sess in sessions:
|
||||
duration = sess[6]
|
||||
hours = duration // 3600
|
||||
minutes = (duration % 3600) // 60
|
||||
seconds = duration % 60
|
||||
|
||||
data.append({
|
||||
'Session-ID': sess[0],
|
||||
'Lizenzschlüssel': sess[1],
|
||||
'Kunde': sess[2],
|
||||
'Session-ID (Tech)': sess[3],
|
||||
'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'),
|
||||
'Letzte Aktivität': sess[5].strftime('%d.%m.%Y %H:%M:%S'),
|
||||
'Dauer': f"{hours}h {minutes}m {seconds}s",
|
||||
'IP-Adresse': sess[7],
|
||||
'Browser': sess[8]
|
||||
})
|
||||
|
||||
sheet_name = 'Aktive Sessions'
|
||||
filename_prefix = 'aktive_sessions'
|
||||
else:
|
||||
# Beendete Lizenz-Sessions
|
||||
cur.execute("""
|
||||
SELECT s.id, l.license_key, c.name as customer_name, s.session_id,
|
||||
s.started_at, s.ended_at,
|
||||
EXTRACT(EPOCH FROM (s.ended_at - s.started_at))::INT as duration_seconds,
|
||||
s.ip_address, s.user_agent
|
||||
FROM sessions s
|
||||
JOIN licenses l ON s.license_id = l.id
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
WHERE s.is_active = false AND s.ended_at IS NOT NULL
|
||||
ORDER BY s.ended_at DESC
|
||||
LIMIT 1000
|
||||
""")
|
||||
sessions = cur.fetchall()
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
for sess in sessions:
|
||||
duration = sess[6] if sess[6] else 0
|
||||
hours = duration // 3600
|
||||
minutes = (duration % 3600) // 60
|
||||
seconds = duration % 60
|
||||
|
||||
data.append({
|
||||
'Session-ID': sess[0],
|
||||
'Lizenzschlüssel': sess[1],
|
||||
'Kunde': sess[2],
|
||||
'Session-ID (Tech)': sess[3],
|
||||
'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'),
|
||||
'Endzeit': sess[5].strftime('%d.%m.%Y %H:%M:%S') if sess[5] else '',
|
||||
'Dauer': f"{hours}h {minutes}m {seconds}s",
|
||||
'IP-Adresse': sess[7],
|
||||
'Browser': sess[8]
|
||||
})
|
||||
|
||||
sheet_name = 'Beendete Sessions'
|
||||
filename_prefix = 'beendete_sessions'
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# DataFrame erstellen
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Timestamp für Dateiname
|
||||
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f'{filename_prefix}_export_{timestamp}'
|
||||
|
||||
# Audit Log für Export
|
||||
log_audit('EXPORT', 'sessions',
|
||||
additional_info=f"{export_format.upper()} Export von {session_type} Sessions mit {len(data)} Einträgen")
|
||||
|
||||
if export_format == 'csv':
|
||||
# CSV Export
|
||||
output = io.StringIO()
|
||||
# UTF-8 BOM für Excel
|
||||
output.write('\ufeff')
|
||||
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||
mimetype='text/csv;charset=utf-8',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.csv'
|
||||
)
|
||||
else:
|
||||
# Excel Export
|
||||
output = BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False, sheet_name=sheet_name)
|
||||
|
||||
# Spaltenbreiten anpassen
|
||||
worksheet = writer.sheets[sheet_name]
|
||||
for idx, col in enumerate(df.columns):
|
||||
max_length = max(
|
||||
df[col].astype(str).map(len).max(),
|
||||
len(col)
|
||||
) + 2
|
||||
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.xlsx'
|
||||
)
|
||||
|
||||
@app.route("/export/resources")
|
||||
@login_required
|
||||
def export_resources():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
# Holen der Filter-Parameter
|
||||
filter_type = request.args.get('type', '')
|
||||
filter_status = request.args.get('status', '')
|
||||
search_query = request.args.get('search', '')
|
||||
show_test = request.args.get('show_test', 'false').lower() == 'true'
|
||||
export_format = request.args.get('format', 'excel')
|
||||
|
||||
# SQL Query mit Filtern
|
||||
query = """
|
||||
SELECT r.id, r.type, r.value, r.status, r.license_id, r.created_at, r.allocated_at,
|
||||
l.license_key, c.name as customer_name, c.email as customer_email,
|
||||
l.type as license_type
|
||||
FROM resource_pools r
|
||||
LEFT JOIN licenses l ON r.license_id = l.id
|
||||
LEFT JOIN customers c ON l.customer_id = c.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
|
||||
# Filter für Testdaten
|
||||
if not show_test:
|
||||
query += " AND (l.is_test = false OR l.is_test IS NULL)"
|
||||
|
||||
# Filter für Ressourcentyp
|
||||
if filter_type:
|
||||
query += " AND r.type = %s"
|
||||
params.append(filter_type)
|
||||
|
||||
# Filter für Status
|
||||
if filter_status:
|
||||
query += " AND r.status = %s"
|
||||
params.append(filter_status)
|
||||
|
||||
# Suchfilter
|
||||
if search_query:
|
||||
query += " AND (r.value ILIKE %s OR l.license_key ILIKE %s OR c.name ILIKE %s)"
|
||||
params.extend([f'%{search_query}%', f'%{search_query}%', f'%{search_query}%'])
|
||||
|
||||
query += " ORDER BY r.id DESC"
|
||||
|
||||
cur.execute(query, params)
|
||||
resources = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Daten für Export vorbereiten
|
||||
data = []
|
||||
for res in resources:
|
||||
status_text = {
|
||||
'available': 'Verfügbar',
|
||||
'allocated': 'Zugewiesen',
|
||||
'quarantine': 'Quarantäne'
|
||||
}.get(res[3], res[3])
|
||||
|
||||
type_text = {
|
||||
'domain': 'Domain',
|
||||
'ipv4': 'IPv4',
|
||||
'phone': 'Telefon'
|
||||
}.get(res[1], res[1])
|
||||
|
||||
data.append({
|
||||
'ID': res[0],
|
||||
'Typ': type_text,
|
||||
'Ressource': res[2],
|
||||
'Status': status_text,
|
||||
'Lizenzschlüssel': res[7] or '',
|
||||
'Kunde': res[8] or '',
|
||||
'Kunden-Email': res[9] or '',
|
||||
'Lizenztyp': res[10] or '',
|
||||
'Erstellt am': res[5].strftime('%d.%m.%Y %H:%M:%S') if res[5] else '',
|
||||
'Zugewiesen am': res[6].strftime('%d.%m.%Y %H:%M:%S') if res[6] else ''
|
||||
})
|
||||
|
||||
# DataFrame erstellen
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Timestamp für Dateiname
|
||||
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f'resources_export_{timestamp}'
|
||||
|
||||
# Audit Log für Export
|
||||
log_audit('EXPORT', 'resources',
|
||||
additional_info=f"{export_format.upper()} Export mit {len(data)} Ressourcen")
|
||||
|
||||
if export_format == 'csv':
|
||||
# CSV Export
|
||||
output = io.StringIO()
|
||||
# UTF-8 BOM für Excel
|
||||
output.write('\ufeff')
|
||||
df.to_csv(output, index=False, sep=';', encoding='utf-8')
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(output.getvalue().encode('utf-8')),
|
||||
mimetype='text/csv;charset=utf-8',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.csv'
|
||||
)
|
||||
else:
|
||||
# Excel Export
|
||||
output = BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, index=False, sheet_name='Resources')
|
||||
|
||||
# Spaltenbreiten anpassen
|
||||
worksheet = writer.sheets['Resources']
|
||||
for idx, col in enumerate(df.columns):
|
||||
max_length = max(
|
||||
df[col].astype(str).map(len).max(),
|
||||
len(col)
|
||||
) + 2
|
||||
worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50)
|
||||
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
as_attachment=True,
|
||||
download_name=f'{filename}.xlsx'
|
||||
)
|
||||
|
||||
@app.route("/audit")
|
||||
@login_required
|
||||
def audit_log():
|
||||
|
||||
@@ -63,14 +63,8 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="mb-4">
|
||||
<h2>📝 Log</h2>
|
||||
<div>
|
||||
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
|
||||
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
|
||||
<a href="/sessions" class="btn btn-secondary">🟢 Sessions</a>
|
||||
<a href="/backups" class="btn btn-secondary">💾 Backups</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
@@ -119,7 +113,20 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="/audit" class="btn btn-outline-secondary">Zurücksetzen</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/audit" class="btn btn-outline-secondary">Zurücksetzen</a>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/export/audit?format=excel&user={{ filter_user }}&action={{ filter_action }}&entity={{ filter_entity }}">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||
<li><a class="dropdown-item" href="/export/audit?format=csv&user={{ filter_user }}&action={{ filter_action }}&entity={{ filter_entity }}">
|
||||
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -444,24 +444,6 @@
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-header mt-4">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/export/licenses?format=excel">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
<span>Excel Export</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/export/licenses?format=csv">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
<span>CSV Export</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
|
||||
@@ -6,7 +6,25 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h2 class="mb-4">Kunden & Lizenzen</h2>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">Kunden & Lizenzen</h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/export/customers?format=excel{% if request.args.get('show_test') %}&include_test=true{% endif %}">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Kunden (Excel)</a></li>
|
||||
<li><a class="dropdown-item" href="/export/customers?format=csv{% if request.args.get('show_test') %}&include_test=true{% endif %}">
|
||||
<i class="bi bi-file-earmark-text"></i> Kunden (CSV)</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="/export/licenses?format=excel{% if request.args.get('show_test') %}&include_test=true{% endif %}{% if selected_customer_id %}&customer_id={{ selected_customer_id }}{% endif %}">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Lizenzen (Excel)</a></li>
|
||||
<li><a class="dropdown-item" href="/export/licenses?format=csv{% if request.args.get('show_test') %}&include_test=true{% endif %}{% if selected_customer_id %}&customer_id={{ selected_customer_id }}{% endif %}">
|
||||
<i class="bi bi-file-earmark-text"></i> Lizenzen (CSV)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Kundenliste (Links) -->
|
||||
|
||||
@@ -290,7 +290,20 @@
|
||||
<div class="card-header bg-white">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
|
||||
<span class="badge bg-secondary">{{ total }} Einträge</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/export/resources?format=excel&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||
<li><a class="dropdown-item" href="/export/resources?format=csv&type={{ filter_type }}&status={{ filter_status }}&search={{ search_query }}&show_test={{ show_test }}">
|
||||
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ total }} Einträge</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
|
||||
@@ -56,7 +56,23 @@
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Session-Tracking</h2>
|
||||
<div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><h6 class="dropdown-header">Aktive Sessions</h6></li>
|
||||
<li><a class="dropdown-item" href="/export/sessions?type=active&format=excel">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||
<li><a class="dropdown-item" href="/export/sessions?type=active&format=csv">
|
||||
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">Beendete Sessions</h6></li>
|
||||
<li><a class="dropdown-item" href="/export/sessions?type=ended&format=excel">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li>
|
||||
<li><a class="dropdown-item" href="/export/sessions?type=ended&format=csv">
|
||||
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren