UI Verbesserung (Bei Log war Button)

Dieser Commit ist enthalten in:
2025-06-10 23:55:30 +02:00
Ursprung 3313563bd9
Commit e24a4e53b6
6 geänderte Dateien mit 457 neuen und 29 gelöschten Zeilen

Datei anzeigen

@@ -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():

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@@ -290,9 +290,22 @@
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
<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">
{% if resources %}
<div class="table-responsive">

Datei anzeigen

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