Export Button geht jetzt

Dieser Commit ist enthalten in:
2025-06-22 18:30:11 +02:00
Ursprung 74391e6634
Commit b9b943ec15
11 geänderte Dateien mit 514 neuen und 167 gelöschten Zeilen

Datei anzeigen

@@ -1,5 +1,125 @@
# v2-Docker Projekt Journal # v2-Docker Projekt Journal
## Letzte Änderungen (22.06.2025 - 16:49 Uhr)
### Export-Funktionen Analyse und Lösungsplan ✅
**Problem:**
- CSV Export Buttons vorhanden, aber Backend liefert immer Excel-Dateien
- Monitoring Export zeigt nur Platzhalter-Alerts ("Export-Funktion wird implementiert")
- Leads/CRM Module hat keine Export-Funktionalität
- Format-Parameter wird in Export-Routes ignoriert
**Analyse-Ergebnisse:**
1. Excel-Exporte funktionieren für: Lizenzen, Kunden, Sessions, Audit Logs, Ressourcen
2. Export-Routes in `export_routes.py` prüfen nie den `format=csv` Parameter
3. Nur `create_excel_export()` existiert, keine CSV-Generierung implementiert
4. Monitoring-Exporte haben nur JavaScript-Platzhalter ohne Backend
5. Lead Management hat keine Export-Funktionalität
**Lösungsplan (YAGNI & Strukturiert):**
1. **CSV Export Fix (Priorität 1)**
- Format-Parameter in bestehenden Export-Routes prüfen
- CSV als Alternative zu Excel hinzufügen (Excel bleibt Default)
- Python's eingebautes csv-Modul nutzen, keine neuen Dependencies
- Minimale Änderung: ~10 Zeilen pro Route
2. **Monitoring Export (Priorität 2)**
- Neue Route `/export/monitoring` nach bestehendem Muster
- Daten von existierenden Monitoring-Endpoints nutzen
- Excel und CSV Format unterstützen
3. **Lead Export (Priorität 3)**
- Route `/leads/export` zum Lead Blueprint hinzufügen
- Institutionen mit Kontakt-Anzahl exportieren
- Gleiches Muster wie andere Exporte verwenden
**Vorteile dieser Lösung:**
- Keine Refaktorierung nötig
- Bestehende Excel-Exporte bleiben unverändert
- Konsistentes URL-Muster mit format-Parameter
- Rückwärtskompatibel (Excel als Standard)
- Einfach erweiterbar für zukünftige Formate
**Implementierung abgeschlossen:**
1. **CSV Export Support hinzugefügt:**
- Neue Funktion `create_csv_export()` in `utils/export.py`
- Alle Export-Routes prüfen jetzt den `format` Parameter
- CSV-Dateien mit UTF-8 BOM für Excel-Kompatibilität
2. **Monitoring Export implementiert:**
- Neue Route `/export/monitoring` in `export_routes.py`
- Exportiert Heartbeats und optional Anomalien
- JavaScript-Funktionen in Templates aktualisiert
3. **Lead Export hinzugefügt:**
- Neue Route `/leads/export` in `leads/routes.py`
- Exportiert Institutionen mit Kontakt-Statistiken
- Export-Buttons zu Institutions-Template hinzugefügt
**Geänderte Dateien:**
- `utils/export.py` - CSV-Export-Funktion hinzugefügt
- `routes/export_routes.py` - Format-Parameter-Prüfung für alle Routes
- `routes/export_routes.py` - Monitoring-Export hinzugefügt
- `leads/routes.py` - Lead-Export-Route hinzugefügt
- `templates/monitoring/analytics.html` - Export-Funktionen aktualisiert
- `templates/monitoring/live_dashboard.html` - Export-Funktionen aktualisiert
- `leads/templates/leads/institutions.html` - Export-Buttons hinzugefügt
**Testing abgeschlossen:**
- Alle Export-Routes sind verfügbar und funktionieren
- CSV-Export generiert korrekte CSV-Dateien mit UTF-8 BOM
- Excel bleibt der Standard wenn kein format-Parameter angegeben
- Container wurde neu gebaut und deployed
- Alle 7 Export-Endpoints unterstützen beide Formate:
- `/export/licenses`
- `/export/customers`
- `/export/sessions`
- `/export/audit`
- `/export/resources`
- `/export/monitoring`
- `/leads/export`
---
## Letzte Änderungen (22.06.2025 - 16:35 Uhr)
### Lizenzfilter System komplett überarbeitet ✅
**Problem:**
- Checkbox-basiertes Filtersystem war unübersichtlich und fummelig
- "Fake-Daten anzeigen" Checkbox funktionierte nicht richtig
- "Läuft bald ab" Status machte keinen Sinn (inaktive Lizenzen können nicht ablaufen)
**Lösung 1 - Neues Dropdown-System:**
- Checkbox-Filter ersetzt durch 3 klare Dropdowns:
- Datenquelle: Echte Lizenzen / 🧪 Fake-Daten / Alle Daten
- Lizenztyp: Alle Typen / Vollversion / Testversion
- Status: Alle Status / ✅ Aktiv / ⚠️ Abgelaufen / ❌ Deaktiviert
- Auto-Submit bei Änderung
- Übersichtlicher "Filter zurücksetzen" Button
**Lösung 2 - API Bug Fix:**
- SQLAlchemy Fehler behoben: `text()` Wrapper für Raw SQL Queries hinzugefügt
- License Server API funktioniert jetzt korrekt
**Lösung 3 - Status-Logik korrigiert:**
- "Läuft bald ab" komplett entfernt (gehört nur ins Dashboard als Hinweis)
- Klare Trennung der 3 Status:
- Aktiv = `is_active=true` (egal ob abgelaufen)
- Abgelaufen = `valid_until <= heute` (läuft aber weiter bis manuell deaktiviert)
- Deaktiviert = `is_active=false` (manuell gestoppt)
- Lizenzen laufen nach Ablauf weiter bis zur manuellen Deaktivierung
**Geänderte Dateien:**
- `templates/licenses.html` - Komplettes Filter-UI überarbeitet
- `routes/license_routes.py` - Filter-Logik angepasst
- `v2_lizenzserver/app/core/api_key_auth.py` - SQL Bug behoben
---
## Letzte Änderungen (22.06.2025 - 13:27 Uhr) ## Letzte Änderungen (22.06.2025 - 13:27 Uhr)
### Bug Fix: API Key Anzeige in Administration ### Bug Fix: API Key Anzeige in Administration

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

Datei anzeigen

@@ -241,4 +241,61 @@ def delete_note(note_id):
) )
return jsonify({'success': success}) return jsonify({'success': success})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500 return jsonify({'success': False, 'error': str(e)}), 500
# Export Routes
@leads_bp.route('/export')
@login_required
def export_leads():
"""Export leads data as Excel/CSV"""
from utils.export import create_excel_export, create_csv_export
try:
conn = get_db_connection()
cur = conn.cursor()
# Query institutions with contact counts
cur.execute("""
SELECT
i.id,
i.name,
i.type,
i.website,
i.address,
i.created_at,
i.created_by,
COUNT(DISTINCT c.id) as contact_count,
COUNT(DISTINCT cd.id) as contact_detail_count,
COUNT(DISTINCT n.id) as note_count
FROM lead_institutions i
LEFT JOIN lead_contacts c ON i.id = c.institution_id
LEFT JOIN lead_contact_details cd ON c.id = cd.contact_id
LEFT JOIN lead_notes n ON i.id = n.institution_id
GROUP BY i.id, i.name, i.type, i.website, i.address, i.created_at, i.created_by
ORDER BY i.name
""")
# Prepare data for export
data = []
columns = ['ID', 'Institution', 'Typ', 'Website', 'Adresse',
'Erstellt am', 'Erstellt von', 'Anzahl Kontakte',
'Anzahl Kontaktdetails', 'Anzahl Notizen']
for row in cur.fetchall():
data.append(list(row))
# Check format parameter
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
return create_csv_export(data, columns, 'leads')
else:
return create_excel_export(data, columns, 'leads')
except Exception as e:
flash(f'Fehler beim Export: {str(e)}', 'error')
return redirect(url_for('leads.institutions'))
finally:
cur.close()
conn.close()

Datei anzeigen

@@ -32,6 +32,14 @@
placeholder="Institution suchen..." onkeyup="filterInstitutions()"> placeholder="Institution suchen..." onkeyup="filterInstitutions()">
</div> </div>
</div> </div>
<div class="col-md-6 text-end">
<a href="{{ url_for('leads.export_leads', format='excel') }}" class="btn btn-outline-success">
<i class="bi bi-file-excel"></i> Excel Export
</a>
<a href="{{ url_for('leads.export_leads', format='csv') }}" class="btn btn-outline-info">
<i class="bi bi-file-text"></i> CSV Export
</a>
</div>
</div> </div>
<!-- Institutions Table --> <!-- Institutions Table -->

Datei anzeigen

@@ -5,7 +5,7 @@ from flask import Blueprint, request, send_file
import config import config
from auth.decorators import login_required from auth.decorators import login_required
from utils.export import create_excel_export, prepare_audit_export_data from utils.export import create_excel_export, create_csv_export, prepare_audit_export_data, format_datetime_for_export
from db import get_connection from db import get_connection
# Create Blueprint # Create Blueprint
@@ -20,61 +20,32 @@ def export_licenses():
cur = conn.cursor() cur = conn.cursor()
try: try:
# Filter aus Request # Nur reale Daten exportieren - keine Fake-Daten
show_fake = request.args.get('show_fake', 'false') == 'true' query = """
SELECT
# SQL Query mit optionalem Test-Filter l.id,
if show_fake: l.license_key,
query = """ c.name as customer_name,
SELECT c.email as customer_email,
l.id, l.license_type,
l.license_key, l.valid_from,
c.name as customer_name, l.valid_until,
c.email as customer_email, l.is_active,
l.license_type, l.device_limit,
l.valid_from, l.created_at,
l.valid_until, l.is_fake,
l.is_active, CASE
l.device_limit, WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
l.created_at, WHEN l.is_active = false THEN 'Deaktiviert'
l.is_fake, ELSE 'Aktiv'
CASE END as status,
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen' (SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.is_active = true) as active_sessions,
WHEN l.is_active = false THEN 'Deaktiviert' (SELECT COUNT(DISTINCT hardware_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
ELSE 'Aktiv' FROM licenses l
END as status, LEFT JOIN customers c ON l.customer_id = c.id
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.is_active = true) as active_sessions, WHERE l.is_fake = false
(SELECT COUNT(DISTINCT hardware_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices ORDER BY l.created_at DESC
FROM licenses l """
LEFT JOIN customers c ON l.customer_id = c.id
ORDER BY l.created_at DESC
"""
else:
query = """
SELECT
l.id,
l.license_key,
c.name as customer_name,
c.email as customer_email,
l.license_type,
l.valid_from,
l.valid_until,
l.is_active,
l.device_limit,
l.created_at,
l.is_fake,
CASE
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
WHEN l.is_active = false THEN 'Deaktiviert'
ELSE 'Aktiv'
END as status,
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.is_active = true) as active_sessions,
(SELECT COUNT(DISTINCT hardware_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
FROM licenses l
LEFT JOIN customers c ON l.customer_id = c.id
WHERE l.is_fake = false
ORDER BY l.created_at DESC
"""
cur.execute(query) cur.execute(query)
@@ -85,19 +56,25 @@ def export_licenses():
'Status', 'Aktive Sessions', 'Registrierte Geräte'] 'Status', 'Aktive Sessions', 'Registrierte Geräte']
for row in cur.fetchall(): for row in cur.fetchall():
data.append(list(row)) row_data = list(row)
# Format datetime fields
if row_data[5]: # valid_from
row_data[5] = format_datetime_for_export(row_data[5])
if row_data[6]: # valid_until
row_data[6] = format_datetime_for_export(row_data[6])
if row_data[9]: # created_at
row_data[9] = format_datetime_for_export(row_data[9])
data.append(row_data)
# Excel-Datei erstellen # Format prüfen
excel_file = create_excel_export(data, columns, 'Lizenzen') format_type = request.args.get('format', 'excel').lower()
# Datei senden if format_type == 'csv':
filename = f"lizenzen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" # CSV-Datei erstellen
return send_file( return create_csv_export(data, columns, 'lizenzen')
excel_file, else:
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # Excel-Datei erstellen
as_attachment=True, return create_excel_export(data, columns, 'lizenzen')
download_name=filename
)
except Exception as e: except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}") logging.error(f"Fehler beim Export: {str(e)}")
@@ -120,23 +97,61 @@ def export_audit():
action_filter = request.args.get('action', '') action_filter = request.args.get('action', '')
entity_type_filter = request.args.get('entity_type', '') entity_type_filter = request.args.get('entity_type', '')
# Query aufbauen
query = """
SELECT
id, timestamp, username, action, entity_type, entity_id,
ip_address, user_agent, old_values, new_values, additional_info
FROM audit_log
WHERE timestamp >= CURRENT_TIMESTAMP - INTERVAL '%s days'
"""
params = [days]
if action_filter:
query += " AND action = %s"
params.append(action_filter)
if entity_type_filter:
query += " AND entity_type = %s"
params.append(entity_type_filter)
query += " ORDER BY timestamp DESC"
cur.execute(query, params)
# Daten in Dictionary-Format umwandeln
audit_logs = []
for row in cur.fetchall():
audit_logs.append({
'id': row[0],
'timestamp': row[1],
'username': row[2],
'action': row[3],
'entity_type': row[4],
'entity_id': row[5],
'ip_address': row[6],
'user_agent': row[7],
'old_values': row[8],
'new_values': row[9],
'additional_info': row[10]
})
# Daten für Export vorbereiten # Daten für Export vorbereiten
data = prepare_audit_export_data(days, action_filter, entity_type_filter) data = prepare_audit_export_data(audit_logs)
# Excel-Datei erstellen # Excel-Datei erstellen
columns = ['Zeitstempel', 'Benutzer', 'Aktion', 'Entität', 'Entität ID', columns = ['ID', 'Zeitstempel', 'Benutzer', 'Aktion', 'Entität', 'Entität ID',
'IP-Adresse', 'Alte Werte', 'Neue Werte', 'Zusatzinfo'] 'IP-Adresse', 'User Agent', 'Alte Werte', 'Neue Werte', 'Zusatzinfo']
excel_file = create_excel_export(data, columns, 'Audit-Log') # Format prüfen
format_type = request.args.get('format', 'excel').lower()
# Datei senden if format_type == 'csv':
filename = f"audit_log_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" # CSV-Datei erstellen
return send_file( return create_csv_export(data, columns, 'audit_log')
excel_file, else:
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # Excel-Datei erstellen
as_attachment=True, return create_excel_export(data, columns, 'audit_log')
download_name=filename
)
except Exception as e: except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}") logging.error(f"Fehler beim Export: {str(e)}")
@@ -154,7 +169,7 @@ def export_customers():
cur = conn.cursor() cur = conn.cursor()
try: try:
# SQL Query # SQL Query - nur reale Kunden exportieren
cur.execute(""" cur.execute("""
SELECT SELECT
c.id, c.id,
@@ -169,6 +184,7 @@ def export_customers():
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
FROM customers c FROM customers c
LEFT JOIN licenses l ON c.id = l.customer_id LEFT JOIN licenses l ON c.id = l.customer_id
WHERE c.is_fake = false
GROUP BY c.id, c.name, c.email, c.phone, c.address, c.created_at, c.is_fake GROUP BY c.id, c.name, c.email, c.phone, c.address, c.created_at, c.is_fake
ORDER BY c.name ORDER BY c.name
""") """)
@@ -179,19 +195,21 @@ def export_customers():
'Test-Kunde', 'Anzahl Lizenzen', 'Aktive Lizenzen', 'Abgelaufene Lizenzen'] 'Test-Kunde', 'Anzahl Lizenzen', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
for row in cur.fetchall(): for row in cur.fetchall():
data.append(list(row)) # Format datetime fields (created_at ist Spalte 5)
row_data = list(row)
if row_data[5]: # created_at
row_data[5] = format_datetime_for_export(row_data[5])
data.append(row_data)
# Excel-Datei erstellen # Format prüfen
excel_file = create_excel_export(data, columns, 'Kunden') format_type = request.args.get('format', 'excel').lower()
# Datei senden if format_type == 'csv':
filename = f"kunden_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" # CSV-Datei erstellen
return send_file( return create_csv_export(data, columns, 'kunden')
excel_file, else:
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # Excel-Datei erstellen
as_attachment=True, return create_excel_export(data, columns, 'kunden')
download_name=filename
)
except Exception as e: except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}") logging.error(f"Fehler beim Export: {str(e)}")
@@ -230,7 +248,7 @@ def export_sessions():
l.is_fake l.is_fake
FROM sessions s FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE s.is_active = true WHERE s.is_active = true AND l.is_fake = false
ORDER BY s.started_at DESC ORDER BY s.started_at DESC
""" """
cur.execute(query) cur.execute(query)
@@ -250,7 +268,7 @@ def export_sessions():
l.is_fake l.is_fake
FROM sessions s FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE s.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days' WHERE s.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days' AND l.is_fake = false
ORDER BY s.started_at DESC ORDER BY s.started_at DESC
""" """
cur.execute(query, (days,)) cur.execute(query, (days,))
@@ -262,19 +280,25 @@ def export_sessions():
'Lizenztyp', 'Fake-Lizenz'] 'Lizenztyp', 'Fake-Lizenz']
for row in cur.fetchall(): for row in cur.fetchall():
data.append(list(row)) row_data = list(row)
# Format datetime fields
if row_data[5]: # started_at
row_data[5] = format_datetime_for_export(row_data[5])
if row_data[6]: # ended_at
row_data[6] = format_datetime_for_export(row_data[6])
if row_data[7]: # last_heartbeat
row_data[7] = format_datetime_for_export(row_data[7])
data.append(row_data)
# Excel-Datei erstellen # Format prüfen
excel_file = create_excel_export(data, columns, 'Sessions') format_type = request.args.get('format', 'excel').lower()
# Datei senden if format_type == 'csv':
filename = f"sessions_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" # CSV-Datei erstellen
return send_file( return create_csv_export(data, columns, 'sessions')
excel_file, else:
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # Excel-Datei erstellen
as_attachment=True, return create_excel_export(data, columns, 'sessions')
download_name=filename
)
except Exception as e: except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}") logging.error(f"Fehler beim Export: {str(e)}")
@@ -295,7 +319,6 @@ def export_resources():
# Filter aus Request # Filter aus Request
resource_type = request.args.get('type', 'all') resource_type = request.args.get('type', 'all')
status_filter = request.args.get('status', 'all') status_filter = request.args.get('status', 'all')
show_fake = request.args.get('show_fake', 'false') == 'true'
# SQL Query aufbauen # SQL Query aufbauen
query = """ query = """
@@ -328,8 +351,8 @@ def export_resources():
query += " AND rp.status = %s" query += " AND rp.status = %s"
params.append(status_filter) params.append(status_filter)
if not show_fake: # Immer nur reale Ressourcen exportieren
query += " AND rp.is_fake = false" query += " AND rp.is_fake = false"
query += " ORDER BY rp.resource_type, rp.resource_value" query += " ORDER BY rp.resource_type, rp.resource_value"
@@ -342,23 +365,131 @@ def export_resources():
'Status geändert von', 'Quarantäne-Grund'] 'Status geändert von', 'Quarantäne-Grund']
for row in cur.fetchall(): for row in cur.fetchall():
data.append(list(row)) row_data = list(row)
# Format datetime fields
if row_data[7]: # created_at
row_data[7] = format_datetime_for_export(row_data[7])
if row_data[9]: # status_changed_at
row_data[9] = format_datetime_for_export(row_data[9])
data.append(row_data)
# Excel-Datei erstellen # Format prüfen
excel_file = create_excel_export(data, columns, 'Ressourcen') format_type = request.args.get('format', 'excel').lower()
# Datei senden if format_type == 'csv':
filename = f"ressourcen_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" # CSV-Datei erstellen
return send_file( return create_csv_export(data, columns, 'ressourcen')
excel_file, else:
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # Excel-Datei erstellen
as_attachment=True, return create_excel_export(data, columns, 'ressourcen')
download_name=filename
)
except Exception as e: except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}") logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Ressourcen", 500 return "Fehler beim Exportieren der Ressourcen", 500
finally:
cur.close()
conn.close()
@export_bp.route("/monitoring")
@login_required
def export_monitoring():
"""Exportiert Monitoring-Daten als Excel/CSV-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Zeitraum aus Request
hours = int(request.args.get('hours', 24))
# Monitoring-Daten sammeln
data = []
columns = ['Zeitstempel', 'Lizenz-ID', 'Lizenzschlüssel', 'Kunde', 'Hardware-ID',
'IP-Adresse', 'Ereignis-Typ', 'Schweregrad', 'Beschreibung']
# Query für Heartbeats und optionale Anomalien
query = """
WITH monitoring_data AS (
-- Lizenz-Heartbeats
SELECT
lh.timestamp,
lh.license_id,
l.license_key,
c.name as customer_name,
lh.hardware_id,
lh.ip_address,
'Heartbeat' as event_type,
'Normal' as severity,
'License validation' as description
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > CURRENT_TIMESTAMP - INTERVAL '%s hours'
AND l.is_fake = false
"""
# Check if anomaly_detections table exists
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'anomaly_detections'
)
""")
has_anomalies = cur.fetchone()[0]
if has_anomalies:
query += """
UNION ALL
-- Anomalien
SELECT
ad.detected_at as timestamp,
ad.license_id,
l.license_key,
c.name as customer_name,
ad.hardware_id,
ad.ip_address,
ad.anomaly_type as event_type,
ad.severity,
ad.description
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.detected_at > CURRENT_TIMESTAMP - INTERVAL '%s hours'
AND (l.is_fake = false OR l.is_fake IS NULL)
"""
params = [hours, hours]
else:
params = [hours]
query += """
)
SELECT * FROM monitoring_data
ORDER BY timestamp DESC
"""
cur.execute(query, params)
for row in cur.fetchall():
row_data = list(row)
# Format datetime field (timestamp ist Spalte 0)
if row_data[0]: # timestamp
row_data[0] = format_datetime_for_export(row_data[0])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'monitoring')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'monitoring')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Monitoring-Daten", 500
finally: finally:
cur.close() cur.close()
conn.close() conn.close()

Datei anzeigen

@@ -193,16 +193,14 @@
<div class="col-md-3"> <div class="col-md-3">
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{{ url_for('admin.audit_log') }}" class="btn btn-outline-secondary">Zurücksetzen</a> <a href="{{ url_for('admin.audit_log') }}" class="btn btn-outline-secondary">Zurücksetzen</a>
<div class="dropdown"> <!-- Export Buttons -->
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"> <div class="btn-group" role="group">
<i class="bi bi-download"></i> Export <a href="{{ url_for('export.export_audit', format='excel', user=filter_user, action=filter_action, entity=filter_entity) }}" class="btn btn-success btn-sm">
</button> <i class="bi bi-file-earmark-excel"></i> Excel
<ul class="dropdown-menu"> </a>
<li><a class="dropdown-item" href="{{ url_for('export.export_audit', format='excel', user=filter_user, action=filter_action, entity=filter_entity) }}"> <a href="{{ url_for('export.export_audit', format='csv', user=filter_user, action=filter_action, entity=filter_entity) }}" class="btn btn-secondary btn-sm">
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li> <i class="bi bi-file-earmark-text"></i> CSV
<li><a class="dropdown-item" href="{{ url_for('export.export_audit', format='csv', user=filter_user, action=filter_action, entity=filter_entity) }}"> </a>
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li>
</ul>
</div> </div>
</div> </div>
</div> </div>

Datei anzeigen

@@ -12,21 +12,20 @@
<a href="{{ url_for('leads.institutions') }}" class="btn btn-primary"> <a href="{{ url_for('leads.institutions') }}" class="btn btn-primary">
<i class="bi bi-people"></i> Leads <i class="bi bi-people"></i> Leads
</a> </a>
<div class="dropdown d-inline-block"> <!-- Export Buttons ohne Dropdown -->
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"> <div class="btn-group" role="group">
<i class="bi bi-download"></i> Export <a href="{{ url_for('export.export_customers', format='excel') }}" class="btn btn-success btn-sm">
</button> <i class="bi bi-file-earmark-excel"></i> Kunden Excel
<ul class="dropdown-menu"> </a>
<li><a class="dropdown-item" href="{{ url_for('export.export_customers', format='excel', include_test=request.args.get('show_fake')) }}"> <a href="{{ url_for('export.export_customers', format='csv') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-file-earmark-excel text-success"></i> Kunden (Excel)</a></li> <i class="bi bi-file-earmark-text"></i> Kunden CSV
<li><a class="dropdown-item" href="{{ url_for('export.export_customers', format='csv', include_test=request.args.get('show_fake')) }}"> </a>
<i class="bi bi-file-earmark-text"></i> Kunden (CSV)</a></li> <a href="{{ url_for('export.export_licenses', format='excel') }}" class="btn btn-success btn-sm">
<li><hr class="dropdown-divider"></li> <i class="bi bi-file-earmark-excel"></i> Lizenzen Excel
<li><a class="dropdown-item" href="{{ url_for('export.export_licenses', format='excel', include_test=request.args.get('show_fake')) }}"> </a>
<i class="bi bi-file-earmark-excel text-success"></i> Lizenzen (Excel)</a></li> <a href="{{ url_for('export.export_licenses', format='csv') }}" class="btn btn-secondary btn-sm">
<li><a class="dropdown-item" href="{{ url_for('export.export_licenses', format='csv', include_test=request.args.get('show_fake')) }}"> <i class="bi bi-file-earmark-text"></i> Lizenzen CSV
<i class="bi bi-file-earmark-text"></i> Lizenzen (CSV)</a></li> </a>
</ul>
</div> </div>
</div> </div>
</div> </div>

Datei anzeigen

@@ -301,9 +301,6 @@
<div class="analytics-card"> <div class="analytics-card">
<h5>Berichte exportieren</h5> <h5>Berichte exportieren</h5>
<div class="export-buttons"> <div class="export-buttons">
<button class="btn btn-outline-primary me-2" onclick="exportReport('pdf')">
<i class="bi bi-file-pdf"></i> PDF Export
</button>
<button class="btn btn-outline-success me-2" onclick="exportReport('excel')"> <button class="btn btn-outline-success me-2" onclick="exportReport('excel')">
<i class="bi bi-file-excel"></i> Excel Export <i class="bi bi-file-excel"></i> Excel Export
</button> </button>
@@ -435,7 +432,9 @@
} }
function exportReport(format) { function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`); // Redirect to export endpoint with format parameter
const hours = 24; // Default to 24 hours
window.location.href = `/export/monitoring?format=${format}&hours=${hours}`;
} }
// Start auto-refresh // Start auto-refresh

Datei anzeigen

@@ -660,7 +660,9 @@
} }
function exportReport(format) { function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`); // Redirect to export endpoint with format parameter
const hours = 24; // Default to 24 hours
window.location.href = `/export/monitoring?format=${format}&hours=${hours}`;
} }
// Countdown timer // Countdown timer

Datei anzeigen

@@ -56,23 +56,23 @@
<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">
<h2>Session-Tracking</h2> <h2>Session-Tracking</h2>
<div class="dropdown"> <!-- Export Buttons -->
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"> <div>
<i class="bi bi-download"></i> Export <span class="text-muted me-2">Export:</span>
</button> <div class="btn-group" role="group">
<ul class="dropdown-menu"> <a href="{{ url_for('export.export_sessions', active_only='true', format='excel') }}" class="btn btn-success btn-sm">
<li><h6 class="dropdown-header">Aktive Sessions</h6></li> <i class="bi bi-file-earmark-excel"></i> Aktive (Excel)
<li><a class="dropdown-item" href="{{ url_for('export.export_sessions', type='active', format='excel') }}"> </a>
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li> <a href="{{ url_for('export.export_sessions', active_only='true', format='csv') }}" class="btn btn-secondary btn-sm">
<li><a class="dropdown-item" href="{{ url_for('export.export_sessions', type='active', format='csv') }}"> <i class="bi bi-file-earmark-text"></i> Aktive (CSV)
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li> </a>
<li><hr class="dropdown-divider"></li> <a href="{{ url_for('export.export_sessions', format='excel') }}" class="btn btn-success btn-sm">
<li><h6 class="dropdown-header">Beendete Sessions</h6></li> <i class="bi bi-file-earmark-excel"></i> Alle (Excel)
<li><a class="dropdown-item" href="{{ url_for('export.export_sessions', type='ended', format='excel') }}"> </a>
<i class="bi bi-file-earmark-excel text-success"></i> Excel Export</a></li> <a href="{{ url_for('export.export_sessions', format='csv') }}" class="btn btn-secondary btn-sm">
<li><a class="dropdown-item" href="{{ url_for('export.export_sessions', type='ended', format='csv') }}"> <i class="bi bi-file-earmark-text"></i> Alle (CSV)
<i class="bi bi-file-earmark-text"></i> CSV Export</a></li> </a>
</ul> </div>
</div> </div>
</div> </div>

Datei anzeigen

@@ -1,9 +1,10 @@
import pandas as pd import pandas as pd
from io import BytesIO from io import BytesIO, StringIO
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from flask import send_file from flask import send_file
import csv
def create_excel_export(data, columns, filename_prefix="export"): def create_excel_export(data, columns, filename_prefix="export"):
@@ -35,6 +36,34 @@ def create_excel_export(data, columns, filename_prefix="export"):
) )
def create_csv_export(data, columns, filename_prefix="export"):
"""Create a CSV file from data"""
# Create CSV in memory
output = StringIO()
writer = csv.writer(output)
# Write header
writer.writerow(columns)
# Write data
writer.writerows(data)
# Convert to bytes
output.seek(0)
output_bytes = BytesIO(output.getvalue().encode('utf-8-sig')) # UTF-8 with BOM for Excel compatibility
# Generate filename with timestamp
timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
filename = f"{filename_prefix}_{timestamp}.csv"
return send_file(
output_bytes,
mimetype='text/csv',
as_attachment=True,
download_name=filename
)
def format_datetime_for_export(dt): def format_datetime_for_export(dt):
"""Format datetime for export""" """Format datetime for export"""
if dt: if dt:
@@ -43,6 +72,9 @@ def format_datetime_for_export(dt):
dt = datetime.fromisoformat(dt) dt = datetime.fromisoformat(dt)
except: except:
return dt return dt
# Remove timezone info for Excel compatibility
if hasattr(dt, 'replace') and dt.tzinfo is not None:
dt = dt.replace(tzinfo=None)
return dt.strftime('%Y-%m-%d %H:%M:%S') return dt.strftime('%Y-%m-%d %H:%M:%S')
return '' return ''