diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 0238830..a95b7a2 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -63,7 +63,8 @@
       "Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"BACKUP|LOGIN_2FA_SUCCESS\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/app.py)",
       "Bash(sed:*)",
       "Bash(python:*)",
-      "Bash(awk:*)"
+      "Bash(awk:*)",
+      "Bash(./backup_before_cleanup.sh:*)"
     ],
     "deny": []
   }
diff --git a/JOURNAL.md b/JOURNAL.md
index fc95379..be0c5f5 100644
--- a/JOURNAL.md
+++ b/JOURNAL.md
@@ -1,2732 +1,2732 @@
-# v2-Docker Projekt Journal
-
-## Letzte Änderungen (06.01.2025)
-
-### Gerätelimit-Feature implementiert
-- **Datenbank-Schema erweitert**:
-  - Neue Spalte `device_limit` in `licenses` Tabelle (Standard: 3, Range: 1-10)
-  - Neue Tabelle `device_registrations` für Hardware-ID Tracking
-  - Indizes für Performance-Optimierung hinzugefügt
-  
-- **UI-Anpassungen**:
-  - Einzellizenz-Formular: Dropdown für Gerätelimit (1-10 Geräte)
-  - Batch-Formular: Gerätelimit pro Lizenz auswählbar
-  - Lizenz-Bearbeitung: Gerätelimit änderbar
-  - Lizenz-Anzeige: Zeigt aktive Geräte (z.B. "💻 2/3")
-  
-- **Backend-Änderungen**:
-  - Lizenz-Erstellung speichert device_limit
-  - Batch-Erstellung berücksichtigt device_limit
-  - Lizenz-Update kann device_limit ändern
-  - API-Endpoints liefern Geräteinformationen
-  
-- **Migration**:
-  - Skript `migrate_device_limit.sql` erstellt
-  - Setzt device_limit = 3 für alle bestehenden Lizenzen
-
-### Vollständig implementiert:
-✅ Device Management UI (Geräte pro Lizenz anzeigen/verwalten)
-✅ Device Validation Logic (Prüfung bei Geräte-Registrierung)
-✅ API-Endpoints für Geräte-Registrierung/Deregistrierung
-
-### API-Endpoints:
-- `GET /api/license/<id>/devices` - Listet alle Geräte einer Lizenz
-- `POST /api/license/<id>/register-device` - Registriert ein neues Gerät
-- `POST /api/license/<id>/deactivate-device/<device_id>` - Deaktiviert ein Gerät
-
-### Features:
-- Geräte-Registrierung mit Hardware-ID Validierung
-- Automatische Prüfung des Gerätelimits
-- Reaktivierung deaktivierter Geräte möglich
-- Geräte-Verwaltung UI mit Modal-Dialog
-- Anzeige von Gerätename, OS, IP, Registrierungsdatum
-- Admin kann Geräte manuell deaktivieren
-
----
-
-## Projektübersicht
-Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker-basierter Architektur.
-
-### Technische Anforderungen
-- **Lokaler Betrieb**: Docker mit 4GB RAM und 40GB Speicher
-- **Internet-Zugriff**: 
-  - Admin Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
-  - API Server: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com
-- **Datenbank**: PostgreSQL mit 2 Admin-Usern
-- **Ziel**: PoC für spätere VPS-Migration
-
----
-
-## Best Practices für Produktiv-Migration
-
-### Passwort-Management
-Für die Migration auf Hetzner/VPS müssen die Credentials sicher verwaltet werden:
-
-1. **Environment Variables erstellen:**
-   ```bash
-   # .env.example (ins Git Repository)
-   POSTGRES_USER=changeme
-   POSTGRES_PASSWORD=changeme
-   POSTGRES_DB=changeme
-   SECRET_KEY=generate-a-secure-key
-   ADMIN_USER_1=changeme
-   ADMIN_PASS_1=changeme
-   ADMIN_USER_2=changeme
-   ADMIN_PASS_2=changeme
-   
-   # .env (NICHT ins Git, auf Server erstellen)
-   POSTGRES_USER=produktiv_user
-   POSTGRES_PASSWORD=sicheres_passwort_min_20_zeichen
-   POSTGRES_DB=v2docker_prod
-   SECRET_KEY=generierter_64_zeichen_key
-   # etc.
-   ```
-
-2. **Sichere Passwörter generieren:**
-   - Mindestens 20 Zeichen
-   - Mix aus Groß-/Kleinbuchstaben, Zahlen, Sonderzeichen
-   - Verschiedene Passwörter für Dev/Staging/Prod
-   - Password-Generator verwenden (z.B. `openssl rand -base64 32`)
-
-3. **Erweiterte Sicherheit (Optional):**
-   - HashiCorp Vault für zentrale Secret-Verwaltung
-   - Docker Secrets (für Docker Swarm)
-   - Cloud-Lösungen: AWS Secrets Manager, Azure Key Vault
-
-4. **Wichtige Checkliste:**
-   - [ ] `.env` in `.gitignore` aufnehmen
-   - [ ] Neue Credentials für Produktion generieren
-   - [ ] Backup der Credentials an sicherem Ort
-   - [ ] Regelmäßige Passwort-Rotation planen
-   - [ ] Keine Default-Passwörter verwenden
-
----
-
-## Änderungsprotokoll
-
-### 2025-06-06 - Journal erstellt
-- Initialer Projektstand dokumentiert
-- Aufgabenliste priorisiert
-- Technische Anforderungen festgehalten
-
-### 2025-06-06 - UTF-8 Support implementiert
-- Flask App Konfiguration für UTF-8 hinzugefügt (JSON_AS_ASCII=False)
-- PostgreSQL Verbindung mit UTF-8 client_encoding
-- HTML Forms mit accept-charset="UTF-8"
-- Dockerfile mit deutschen Locale-Einstellungen (de_DE.UTF-8)
-- PostgreSQL Container mit UTF-8 Initialisierung
-- init.sql mit SET client_encoding = 'UTF8'
-
-**Geänderte Dateien:**
-- v2_adminpanel/app.py
-- v2_adminpanel/templates/index.html
-- v2_adminpanel/init.sql
-- v2_adminpanel/Dockerfile
-- v2/docker-compose.yaml
-
-**Nächster Test:**
-- Container neu bauen und starten
-- Kundennamen mit Umlauten testen (z.B. "Müller GmbH", "Björn Schäfer")
-- Email mit Umlauten testen
-
-### 2025-06-06 - Lizenzübersicht implementiert
-- Neue Route `/licenses` für Lizenzübersicht
-- SQL-Query mit JOIN zwischen licenses und customers
-- Status-Berechnung (aktiv, läuft bald ab, abgelaufen)
-- Farbcodierung für verschiedene Status
-- Navigation zwischen Lizenz erstellen und Übersicht
-
-**Neue Features:**
-- Anzeige aller Lizenzen mit Kundeninformationen
-- Status-Anzeige basierend auf Ablaufdatum
-- Unterscheidung zwischen Voll- und Testversion
-- Responsive Tabelle mit Bootstrap
-- Link von Dashboard zur Übersicht und zurück
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/app.py (neue Route hinzugefügt)
-- v2_adminpanel/templates/licenses.html (neu erstellt)
-- v2_adminpanel/templates/index.html (Navigation ergänzt)
-
-**Nächster Test:**
-- Container neu starten
-- Mehrere Lizenzen mit verschiedenen Ablaufdaten erstellen
-- Lizenzübersicht unter /licenses aufrufen
-
-### 2025-06-06 - Lizenz bearbeiten/löschen implementiert
-- Neue Routen für Bearbeiten und Löschen von Lizenzen
-- Bearbeitungsformular mit vorausgefüllten Werten
-- Aktiv/Inaktiv-Status kann geändert werden
-- Lösch-Bestätigung per JavaScript confirm()
-- Kunde kann nicht geändert werden (nur Lizenzdetails)
-
-**Neue Features:**
-- `/license/edit/<id>` - Bearbeitungsformular
-- `/license/delete/<id>` - Lizenz löschen (POST)
-- Aktionen-Spalte in der Lizenzübersicht
-- Buttons für Bearbeiten und Löschen
-- Checkbox für Aktiv-Status
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/app.py (edit_license und delete_license Routen)
-- v2_adminpanel/templates/licenses.html (Aktionen-Spalte hinzugefügt)
-- v2_adminpanel/templates/edit_license.html (neu erstellt)
-
-**Sicherheit:**
-- Login-Required für alle Aktionen
-- POST-only für Löschvorgänge
-- Bestätigungsdialog vor dem Löschen
-
-### 2025-06-06 - Kundenverwaltung implementiert
-- Komplette CRUD-Funktionalität für Kunden
-- Übersicht zeigt Anzahl aktiver/gesamter Lizenzen pro Kunde
-- Kunden können nur gelöscht werden, wenn sie keine Lizenzen haben
-- Bearbeitungsseite zeigt alle Lizenzen des Kunden
-
-**Neue Features:**
-- `/customers` - Kundenübersicht mit Statistiken
-- `/customer/edit/<id>` - Kunde bearbeiten (Name, E-Mail)
-- `/customer/delete/<id>` - Kunde löschen (nur ohne Lizenzen)
-- Navigation zwischen allen drei Hauptbereichen
-- Anzeige der Kundenlizenzen beim Bearbeiten
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/app.py (customers, edit_customer, delete_customer Routen)
-- v2_adminpanel/templates/customers.html (neu erstellt)
-- v2_adminpanel/templates/edit_customer.html (neu erstellt)
-- v2_adminpanel/templates/index.html (Navigation erweitert)
-- v2_adminpanel/templates/licenses.html (Navigation erweitert)
-
-**Besonderheiten:**
-- Lösch-Button ist deaktiviert, wenn Kunde Lizenzen hat
-- Aktive Lizenzen werden separat gezählt (nicht abgelaufen + aktiv)
-- UTF-8 Support für Kundennamen mit Umlauten
-
-### 2025-06-06 - Dashboard mit Statistiken implementiert
-- Übersichtliches Dashboard als neue Startseite
-- Statistik-Karten mit wichtigen Kennzahlen
-- Listen für bald ablaufende und zuletzt erstellte Lizenzen
-- Routing angepasst: Dashboard (/) und Lizenz erstellen (/create)
-
-**Neue Features:**
-- Statistik-Karten: Kunden, Lizenzen gesamt, Aktive, Ablaufende
-- Aufteilung nach Lizenztypen (Vollversion/Testversion)
-- Aufteilung nach Status (Aktiv/Abgelaufen)
-- Top 10 bald ablaufende Lizenzen mit Restlaufzeit
-- Letzte 5 erstellte Lizenzen mit Status
-- Hover-Effekt auf Statistik-Karten
-- Einheitliche Navigation mit Dashboard-Link
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/app.py (dashboard() komplett überarbeitet, create_license() Route)
-- v2_adminpanel/templates/dashboard.html (neu erstellt)
-- v2_adminpanel/templates/index.html (Navigation erweitert)
-- v2_adminpanel/templates/licenses.html (Navigation angepasst)
-- v2_adminpanel/templates/customers.html (Navigation angepasst)
-
-**Dashboard-Inhalte:**
-- 4 Hauptstatistiken als Karten
-- Lizenztyp-Verteilung
-- Status-Verteilung
-- Warnung für bald ablaufende Lizenzen
-- Übersicht der neuesten Aktivitäten
-
-### 2025-06-06 - Suchfunktion implementiert
-- Volltextsuche für Lizenzen und Kunden
-- Case-insensitive Suche mit LIKE-Operator
-- Suchergebnisse mit Hervorhebung des Suchbegriffs
-- Suche zurücksetzen Button
-
-**Neue Features:**
-- **Lizenzsuche**: Sucht in Lizenzschlüssel, Kundenname und E-Mail
-- **Kundensuche**: Sucht in Kundenname und E-Mail
-- Suchformular mit autofocus für schnelle Eingabe
-- Anzeige des aktiven Suchbegriffs
-- Unterschiedliche Meldungen für leere Ergebnisse
-
-**Geänderte Dateien:**
-- v2_adminpanel/app.py (licenses() und customers() mit Suchlogik erweitert)
-- v2_adminpanel/templates/licenses.html (Suchformular hinzugefügt)
-- v2_adminpanel/templates/customers.html (Suchformular hinzugefügt)
-
-**Technische Details:**
-- GET-Parameter für Suche
-- SQL LIKE mit LOWER() für Case-Insensitive Suche
-- Wildcards (%) für Teilstring-Suche
-- UTF-8 kompatibel für deutsche Umlaute
-
-### 2025-06-06 - Filter und Pagination implementiert
-- Erweiterte Filteroptionen für Lizenzübersicht
-- Pagination für große Datenmengen (20 Einträge pro Seite)
-- Filter bleiben bei Seitenwechsel erhalten
-
-**Neue Features für Lizenzen:**
-- **Filter nach Typ**: Alle, Vollversion, Testversion
-- **Filter nach Status**: Alle, Aktiv, Läuft bald ab, Abgelaufen, Deaktiviert
-- **Kombinierbar mit Suche**: Filter und Suche funktionieren zusammen
-- **Pagination**: Navigation durch mehrere Seiten
-- **Ergebnisanzeige**: Zeigt Anzahl gefilterter Ergebnisse
-
-**Neue Features für Kunden:**
-- **Pagination**: 20 Kunden pro Seite
-- **Seitennavigation**: Erste, Letzte, Vor, Zurück
-- **Kombiniert mit Suche**: Suchparameter bleiben erhalten
-
-**Geänderte Dateien:**
-- v2_adminpanel/app.py (licenses() und customers() mit Filter/Pagination erweitert)
-- v2_adminpanel/templates/licenses.html (Filter-Formular und Pagination hinzugefügt)
-- v2_adminpanel/templates/customers.html (Pagination hinzugefügt)
-
-**Technische Details:**
-- SQL WHERE-Klauseln für Filter
-- LIMIT/OFFSET für Pagination
-- URL-Parameter bleiben bei Navigation erhalten
-- Responsive Bootstrap-Komponenten
-
-### 2025-06-06 - Session-Tracking implementiert
-- Neue Tabelle für Session-Verwaltung
-- Anzeige aktiver und beendeter Sessions
-- Manuelles Beenden von Sessions möglich
-- Dashboard zeigt Anzahl aktiver Sessions
-
-**Neue Features:**
-- **Sessions-Tabelle**: Speichert Session-ID, IP, User-Agent, Zeitstempel
-- **Aktive Sessions**: Zeigt alle laufenden Sessions mit Inaktivitätszeit
-- **Session-Historie**: Letzte 24 Stunden beendeter Sessions
-- **Session beenden**: Admins können Sessions manuell beenden
-- **Farbcodierung**: Grün (aktiv), Gelb (>5 Min inaktiv), Rot (lange inaktiv)
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/init.sql (sessions Tabelle hinzugefügt)
-- v2_adminpanel/app.py (sessions() und end_session() Routen)
-- v2_adminpanel/templates/sessions.html (neu erstellt)
-- v2_adminpanel/templates/dashboard.html (Session-Statistik)
-- Alle Templates (Session-Navigation hinzugefügt)
-
-**Technische Details:**
-- Heartbeat-basiertes Tracking (last_heartbeat)
-- Automatische Inaktivitätsberechnung
-- Session-Dauer Berechnung
-- Responsive Tabellen mit Bootstrap
-
-**Hinweis:**
-Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden.
-
-### 2025-06-06 - Export-Funktion implementiert
-- CSV und Excel Export für Lizenzen und Kunden
-- Formatierte Ausgabe mit deutschen Datumsformaten
-- UTF-8 Unterstützung für Sonderzeichen
-
-**Neue Features:**
-- **Lizenz-Export**: Alle Lizenzen mit Kundeninformationen
-- **Kunden-Export**: Alle Kunden mit Lizenzstatistiken
-- **Format-Optionen**: Excel (.xlsx) und CSV (.csv)
-- **Deutsche Formatierung**: Datum als dd.mm.yyyy, Status auf Deutsch
-- **UTF-8 Export**: Korrekte Kodierung für Umlaute
-- **Export-Buttons**: Dropdown-Menüs in Lizenz- und Kundenübersicht
-
-**Geänderte Dateien:**
-- v2_adminpanel/app.py (export_licenses() und export_customers() Routen)
-- v2_adminpanel/requirements.txt (pandas und openpyxl hinzugefügt)
-- v2_adminpanel/templates/licenses.html (Export-Dropdown hinzugefügt)
-- v2_adminpanel/templates/customers.html (Export-Dropdown hinzugefügt)
-
-**Technische Details:**
-- Pandas für Datenverarbeitung
-- OpenPyXL für Excel-Export
-- CSV mit Semikolon-Trennung für deutsche Excel-Kompatibilität
-- Automatische Spaltenbreite in Excel
-- BOM für UTF-8 CSV (Excel-Kompatibilität)
-
-### 2025-06-06 - Audit-Log implementiert
-- Vollständiges Änderungsprotokoll für alle Aktionen
-- Filterbare Übersicht mit Pagination
-- Detaillierte Anzeige von Änderungen
-
-**Neue Features:**
-- **Audit-Log-Tabelle**: Speichert alle Änderungen mit Zeitstempel, Benutzer, IP
-- **Protokollierte Aktionen**: CREATE, UPDATE, DELETE, LOGIN, LOGOUT, EXPORT
-- **JSON-Speicherung**: Alte und neue Werte als JSONB für flexible Abfragen
-- **Filter-Optionen**: Nach Benutzer, Aktion und Entität
-- **Detail-Anzeige**: Aufklappbare Änderungsdetails
-- **Navigation**: Audit-Link in allen Templates
-
-**Geänderte/Neue Dateien:**
-- v2_adminpanel/init.sql (audit_log Tabelle mit Indizes)
-- v2_adminpanel/app.py (log_audit() Funktion und audit_log() Route)
-- v2_adminpanel/templates/audit_log.html (neu erstellt)
-- Alle Templates (Audit-Navigation hinzugefügt)
-
-**Technische Details:**
-- JSONB für strukturierte Datenspeicherung
-- Performance-Indizes auf timestamp, username und entity
-- Farbcodierung für verschiedene Aktionen
-- 50 Einträge pro Seite mit Pagination
-- IP-Adresse und User-Agent Tracking
-
-### 2025-06-06 - PostgreSQL UTF-8 Locale konfiguriert
-- Eigenes PostgreSQL Dockerfile für deutsche Locale
-- Sicherstellung der UTF-8 Unterstützung auf Datenbankebene
-
-**Neue Features:**
-- **PostgreSQL Dockerfile**: Installiert deutsche Locale (de_DE.UTF-8)
-- **Locale-Umgebungsvariablen**: LANG, LANGUAGE, LC_ALL gesetzt
-- **Docker Compose Update**: Verwendet jetzt eigenes PostgreSQL-Image
-
-**Neue Dateien:**
-- v2_postgres/Dockerfile (neu erstellt)
-
-**Geänderte Dateien:**
-- v2/docker-compose.yaml (postgres Service nutzt jetzt build statt image)
-
-**Technische Details:**
-- Basis-Image: postgres:14
-- Locale-Installation über apt-get
-- locale-gen für de_DE.UTF-8
-- Vollständige UTF-8 Unterstützung für deutsche Sonderzeichen
-
-### 2025-06-07 - Backup-Funktionalität implementiert
-- Verschlüsselte Backups mit manueller und automatischer Ausführung
-- Backup-Historie mit Download und Wiederherstellung
-- Dashboard-Integration für Backup-Status
-
-**Neue Features:**
-- **Backup-Erstellung**: Manuell und automatisch (täglich 3:00 Uhr)
-- **Verschlüsselung**: AES-256 mit Fernet, Key aus ENV oder automatisch generiert
-- **Komprimierung**: GZIP-Komprimierung vor Verschlüsselung
-- **Backup-Historie**: Vollständige Übersicht aller Backups
-- **Wiederherstellung**: Mit optionalem Verschlüsselungs-Passwort
-- **Download-Funktion**: Backups können heruntergeladen werden
-- **Dashboard-Widget**: Zeigt letztes Backup-Status
-- **E-Mail-Vorbereitung**: Struktur für Benachrichtigungen (deaktiviert)
-
-**Neue/Geänderte Dateien:**
-- v2_adminpanel/init.sql (backup_history Tabelle hinzugefügt)
-- v2_adminpanel/requirements.txt (cryptography, apscheduler hinzugefügt)
-- v2_adminpanel/app.py (Backup-Funktionen und Routen)
-- v2_adminpanel/templates/backups.html (neu erstellt)
-- v2_adminpanel/templates/dashboard.html (Backup-Status-Widget)
-- v2_adminpanel/Dockerfile (PostgreSQL-Client installiert)
-- v2/.env (EMAIL_ENABLED und BACKUP_ENCRYPTION_KEY)
-- Alle Templates (Backup-Navigation hinzugefügt)
-
-**Technische Details:**
-- Speicherort: C:\Users\Administrator\Documents\GitHub\v2-Docker\backups\
-- Dateiformat: backup_v2docker_YYYYMMDD_HHMMSS_encrypted.sql.gz.enc
-- APScheduler für automatische Backups
-- pg_dump/psql für Datenbank-Operationen
-- Audit-Log für alle Backup-Aktionen
-- Sicherheitsabfrage bei Wiederherstellung
-
-### 2025-06-07 - HTTPS/SSL und Internet-Zugriff implementiert
-- Nginx Reverse Proxy für externe Erreichbarkeit eingerichtet
-- SSL-Zertifikate von IONOS mit vollständiger Certificate Chain integriert
-- Netzwerkkonfiguration für feste IP-Adresse
-- DynDNS und Port-Forwarding konfiguriert
-
-**Neue Features:**
-- **Nginx Reverse Proxy**: Leitet HTTPS-Anfragen an Container weiter
-- **SSL-Zertifikate**: Wildcard-Zertifikat von IONOS für *.z5m7q9dk3ah2v1plx6ju.com
-- **Certificate Chain**: Server-, Intermediate- und Root-Zertifikate kombiniert
-- **Subdomain-Routing**: admin-panel-undso und api-software-undso
-- **Port-Forwarding**: FRITZ!Box 443 → 192.168.178.88
-- **Feste IP**: Windows-PC auf 192.168.178.88 konfiguriert
-
-**Neue/Geänderte Dateien:**
-- v2_nginx/nginx.conf (Reverse Proxy Konfiguration)
-- v2_nginx/Dockerfile (Nginx Container mit SSL)
-- v2_nginx/ssl/fullchain.pem (Certificate Chain)
-- v2_nginx/ssl/privkey.pem (Private Key)
-- v2/docker-compose.yaml (nginx Service hinzugefügt)
-- set-static-ip.ps1 (PowerShell Script für feste IP)
-- reset-to-dhcp.ps1 (PowerShell Script für DHCP)
-
-**Technische Details:**
-- SSL-Termination am Nginx Reverse Proxy
-- Backend-Kommunikation über Docker-internes Netzwerk
-- Admin-Panel nur noch über Nginx erreichbar (Port 443 nicht mehr exposed)
-- License-Server behält externen Port 8443 für direkte API-Zugriffe
-- Intermediate Certificates aus ZIP extrahiert und korrekt verkettet
-
-**Zugangsdaten:**
-- Admin-Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
-- Benutzer 1: rac00n
-- Benutzer 2: w@rh@mm3r
-
-**Status:**
-- ✅ Admin-Panel extern erreichbar ohne SSL-Warnungen
-- ✅ Reverse Proxy funktioniert
-- ✅ SSL-Zertifikate korrekt konfiguriert
-- ✅ Netzwerk-Setup abgeschlossen
-
-### 2025-06-07 - Projekt-Cleanup durchgeführt
-- Redundante und überflüssige Dateien entfernt
-- Projektstruktur verbessert und organisiert
-
-**Durchgeführte Änderungen:**
-1. **Entfernte Dateien:**
-   - v2_adminpanel/templates/.env (Duplikat der Haupt-.env)
-   - v2_postgreSQL/ (leeres Verzeichnis)
-   - SSL-Zertifikate aus Root-Verzeichnis (7 Dateien)
-   - Ungenutzer `json` Import aus app.py
-
-2. **Organisatorische Verbesserungen:**
-   - PowerShell-Scripts in neuen `scripts/` Ordner verschoben
-   - SSL-Zertifikate nur noch in v2_nginx/ssl/
-   - Keine Konfigurationsdateien mehr in Template-Verzeichnissen
-
-**Technische Details:**
-- Docker-Container wurden gestoppt und nach Cleanup neu gestartet
-- Alle Services laufen wieder normal
-- Keine funktionalen Änderungen, nur Struktur-Verbesserungen
-
-**Ergebnis:**
-- Verbesserte Projektstruktur
-- Erhöhte Sicherheit (keine SSL-Zertifikate im Root)
-- Klarere Dateiorganisation
-
-### 2025-06-07 - SSL "Nicht sicher" Problem behoben
-- Chrome-Warnung trotz gültigem Zertifikat analysiert und behoben
-- Ursache: Selbstsigniertes Zertifikat in der Admin Panel Flask-App
-
-**Durchgeführte Änderungen:**
-1. **Admin Panel Konfiguration (app.py):**
-   - Von HTTPS mit selbstsigniertem Zertifikat auf HTTP Port 5000 umgestellt
-   - `ssl_context='adhoc'` entfernt
-   - Flask läuft jetzt auf `0.0.0.0:5000` statt HTTPS
-
-2. **Dockerfile Anpassung (v2_adminpanel/Dockerfile):**
-   - EXPOSE Port von 443 auf 5000 geändert
-   - Container exponiert jetzt HTTP statt HTTPS
-
-3. **Nginx Konfiguration (nginx.conf):**
-   - proxy_pass von `https://admin-panel:443` auf `http://admin-panel:5000` geändert
-   - `proxy_ssl_verify off` entfernt (nicht mehr benötigt)
-   - Sicherheits-Header für beide Domains hinzugefügt:
-     - Strict-Transport-Security (HSTS) - erzwingt HTTPS für 1 Jahr
-     - X-Content-Type-Options - verhindert MIME-Type Sniffing
-     - X-Frame-Options - Schutz vor Clickjacking
-     - X-XSS-Protection - aktiviert XSS-Filter
-     - Referrer-Policy - kontrolliert Referrer-Informationen
-
-**Technische Details:**
-- Externer Traffic nutzt weiterhin HTTPS mit gültigen IONOS-Zertifikaten
-- Interne Kommunikation zwischen Nginx und Admin Panel läuft über HTTP (sicher im Docker-Netzwerk)
-- Kein selbstsigniertes Zertifikat mehr in der Zertifikatskette
-- SSL-Termination erfolgt ausschließlich am Nginx Reverse Proxy
-
-**Docker Neustart:**
-- Container gestoppt (`docker-compose down`)
-- Images neu gebaut (`docker-compose build`)
-- Container neu gestartet (`docker-compose up -d`)
-- Alle Services laufen normal
-
-**Ergebnis:**
-- ✅ "Nicht sicher" Warnung in Chrome behoben
-- ✅ Saubere SSL-Konfiguration ohne Mixed Content
-- ✅ Verbesserte Sicherheits-Header implementiert
-- ✅ Admin Panel zeigt jetzt grünes Schloss-Symbol
-
-### 2025-06-07 - Sicherheitslücke geschlossen: License Server Port
-- Direkter Zugriff auf License Server Port 8443 entfernt
-- Sicherheitsanalyse der exponierten Ports durchgeführt
-
-**Identifiziertes Problem:**
-- License Server war direkt auf Port 8443 von außen erreichbar
-- Umging damit die Nginx-Sicherheitsschicht und Security Headers
-- Besonders kritisch, da nur Platzhalter ohne echte Sicherheit
-
-**Durchgeführte Änderung:**
-- Port-Mapping für License Server in docker-compose.yaml entfernt
-- Service ist jetzt nur noch über Nginx Reverse Proxy erreichbar
-- Gleiche Sicherheitskonfiguration wie Admin Panel
-
-**Aktuelle Port-Exposition:**
-- ✅ Nginx: Port 80/443 (benötigt für externen Zugriff)
-- ✅ PostgreSQL: Keine Ports exponiert (gut)
-- ✅ Admin Panel: Nur über Nginx erreichbar
-- ✅ License Server: Nur über Nginx erreichbar (vorher direkt auf 8443)
-
-**Weitere identifizierte Sicherheitsthemen:**
-1. Credentials im Klartext in .env Datei
-2. SSL-Zertifikate im Repository gespeichert
-3. License Server noch nicht implementiert
-
-**Empfehlung:** Docker-Container neu starten für Änderungsübernahme
-
-### 2025-06-07 - License Server Port 8443 wieder aktiviert
-- Port 8443 für direkten Zugriff auf License Server wieder geöffnet
-- Notwendig für Client-Software Lizenzprüfung
-
-**Begründung:**
-- Client-Software benötigt direkten Zugriff für Lizenzprüfung
-- Umgehung von möglichen Firewall-Blockaden auf Port 443
-- Weniger Latenz ohne Nginx-Proxy
-- Flexibilität für verschiedene Client-Implementierungen
-
-**Konfiguration:**
-- License Server erreichbar über:
-  - Direkt: Port 8443 (für Client-Software)
-  - Via Nginx: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com (für Browser/Tests)
-
-**Sicherheitshinweis:**
-- Port 8443 ist wieder direkt exponiert
-- License Server muss vor Produktivbetrieb implementiert werden mit:
-  - Eigener SSL-Konfiguration
-  - API-Key Authentifizierung
-  - Rate Limiting
-  - Input-Validierung
-
-**Status:**
-- Port-Mapping in docker-compose.yaml wiederhergestellt
-- Änderung erfordert Docker-Neustart
-
-### 2025-06-07 - Rate-Limiting und Brute-Force-Schutz implementiert
-- Umfassender Schutz vor Login-Angriffen mit IP-Sperre
-- Dashboard-Integration für Sicherheitsüberwachung
-
-**Implementierte Features:**
-1. **Rate-Limiting System:**
-   - 5 Login-Versuche erlaubt, danach 24h IP-Sperre
-   - Progressive Fehlermeldungen (zufällig aus 5 lustigen Varianten)
-   - CAPTCHA nach 2 Fehlversuchen (Google reCAPTCHA v2 vorbereitet)
-   - E-Mail-Benachrichtigung bei Sperrung (vorbereitet, deaktiviert für PoC)
-
-2. **Timing-Attack Schutz:**
-   - Mindestens 1 Sekunde Antwortzeit bei allen Login-Versuchen
-   - Gleiche Antwortzeit bei richtigem/falschem Username
-   - Verhindert Username-Enumeration
-
-3. **Lustige Fehlermeldungen (zufällig):**
-   - "NOPE!"
-   - "ACCESS DENIED, TRY HARDER"
-   - "WRONG! 🚫"
-   - "COMPUTER SAYS NO"
-   - "YOU FAILED"
-
-4. **Dashboard-Sicherheitswidget:**
-   - Sicherheitslevel-Anzeige (NORMAL/ERHÖHT/KRITISCH)
-   - Anzahl gesperrter IPs
-   - Fehlversuche heute
-   - Letzte 5 Sicherheitsereignisse mit Details
-
-5. **IP-Verwaltung:**
-   - Übersicht aller gesperrten IPs
-   - Manuelles Entsperren möglich
-   - Login-Versuche zurücksetzen
-   - Detaillierte Informationen pro IP
-
-6. **Audit-Log Erweiterungen:**
-   - LOGIN_SUCCESS - Erfolgreiche Anmeldung
-   - LOGIN_FAILED - Fehlgeschlagener Versuch
-   - LOGIN_BLOCKED - IP wurde gesperrt
-   - UNBLOCK_IP - IP manuell entsperrt
-   - CLEAR_ATTEMPTS - Versuche zurückgesetzt
-
-**Neue/Geänderte Dateien:**
-- v2_adminpanel/init.sql (login_attempts Tabelle)
-- v2_adminpanel/app.py (Rate-Limiting Logik, neue Routen)
-- v2_adminpanel/templates/login.html (Fehlermeldungs-Styling, CAPTCHA)
-- v2_adminpanel/templates/dashboard.html (Sicherheitswidget)
-- v2_adminpanel/templates/blocked_ips.html (neu - IP-Verwaltung)
-
-**Technische Details:**
-- IP-Ermittlung berücksichtigt Proxy-Header (X-Forwarded-For)
-- Fehlermeldungen mit Animation (shake-effect)
-- Farbcodierung: Rot für Fehler, Lila für Sperre, Orange für CAPTCHA
-- Automatische Bereinigung alter Einträge möglich
-
-**Sicherheitsverbesserungen:**
-- Schutz vor Brute-Force-Angriffen
-- Timing-Attack-Schutz implementiert
-- IP-basierte Sperrung für 24 Stunden
-- Audit-Trail für alle Sicherheitsereignisse
-
-**Hinweis für Produktion:**
-- CAPTCHA-Keys müssen in .env konfiguriert werden
-- E-Mail-Server für Benachrichtigungen einrichten
-- Rate-Limits können über Konstanten angepasst werden
-
-### 2025-06-07 - Session-Timeout mit Live-Timer implementiert
-- 5 Minuten Inaktivitäts-Timeout mit visueller Countdown-Anzeige
-- Automatische Session-Verlängerung bei Benutzeraktivität
-
-**Implementierte Features:**
-1. **Session-Timeout Backend:**
-   - Flask Session-Timeout auf 5 Minuten konfiguriert
-   - Heartbeat-Endpoint für Keep-Alive
-   - Automatisches Session-Update bei jeder Aktion
-
-2. **Live-Timer in der Navbar:**
-   - Countdown von 5:00 bis 0:00
-   - Position: Zwischen Logo und Username
-   - Farbwechsel nach verbleibender Zeit:
-     - Grün: > 2 Minuten
-     - Gelb: 1-2 Minuten
-     - Rot: < 1 Minute
-     - Blinkend: < 30 Sekunden
-
-3. **Benutzerinteraktion:**
-   - Timer wird bei jeder Aktivität zurückgesetzt
-   - Tracking von: Klicks, Tastatureingaben, Mausbewegungen
-   - Automatischer Heartbeat bei Aktivität
-   - Warnung bei < 1 Minute mit "Session verlängern" Button
-
-4. **Base-Template System:**
-   - Neue base.html als Basis für alle Admin-Seiten
-   - Alle Templates (außer login.html) nutzen jetzt base.html
-   - Einheitliches Layout und Timer auf allen Seiten
-
-**Neue/Geänderte Dateien:**
-- v2_adminpanel/app.py (Session-Konfiguration, Heartbeat-Endpoint)
-- v2_adminpanel/templates/base.html (neu - Base-Template mit Timer)
-- Alle anderen Templates aktualisiert für Template-Vererbung
-
-**Technische Details:**
-- JavaScript-basierter Countdown-Timer
-- AJAX-Heartbeat alle 5 Sekunden bei Aktivität
-- LocalStorage für Tab-Synchronisation möglich
-- Automatischer Logout bei 0:00
-- Fetch-Interceptor für automatische Session-Verlängerung
-
-**Sicherheitsverbesserung:**
-- Automatischer Logout nach 5 Minuten Inaktivität
-- Verhindert vergessene Sessions
-- Visuelles Feedback für Session-Status
-
-### 2025-06-07 - Session-Timeout Bug behoben
-- Problem: Session-Timeout funktionierte nicht korrekt - Session blieb länger als 5 Minuten aktiv
-- Ursache: login_required Decorator aktualisierte last_activity bei JEDEM Request
-
-**Durchgeführte Änderungen:**
-1. **login_required Decorator (app.py):**
-   - Prüft jetzt ob Session abgelaufen ist (5 Minuten seit last_activity)
-   - Aktualisiert last_activity NICHT mehr automatisch
-   - Führt AUTO_LOGOUT mit Audit-Log bei Timeout durch
-   - Speichert Username vor session.clear() für korrektes Logging
-
-2. **Heartbeat-Endpoint (app.py):**
-   - Geändert zu POST-only Endpoint
-   - Aktualisiert explizit last_activity wenn aufgerufen
-   - Wird nur bei aktiver Benutzerinteraktion aufgerufen
-
-3. **Frontend Timer (base.html):**
-   - Heartbeat wird als POST Request gesendet
-   - trackActivity() ruft extendSession() ohne vorheriges resetTimer() auf
-   - Timer wird erst nach erfolgreichem Heartbeat zurückgesetzt
-   - AJAX Interceptor ignoriert Heartbeat-Requests
-
-4. **Audit-Log Erweiterung:**
-   - Neue Aktion AUTO_LOGOUT hinzugefügt
-   - Orange Farbcodierung (#fd7e14)
-   - Zeigt Grund des Timeouts im Audit-Log
-
-**Ergebnis:**
-- ✅ Session läuft nach exakt 5 Minuten Inaktivität ab
-- ✅ Benutzeraktivität verlängert Session korrekt
-- ✅ AUTO_LOGOUT wird im Audit-Log protokolliert
-- ✅ Visueller Timer zeigt verbleibende Zeit
-
-### 2025-06-07 - Session-Timeout weitere Verbesserungen
-- Zusätzliche Fixes nach Test-Feedback implementiert
-
-**Weitere durchgeführte Änderungen:**
-1. **Fehlender Import behoben:**
-   - `flash` zu Flask-Imports hinzugefügt für Timeout-Warnmeldungen
-
-2. **Session-Cookie-Konfiguration erweitert (app.py):**
-   - SESSION_COOKIE_HTTPONLY = True (Sicherheit gegen XSS)
-   - SESSION_COOKIE_SECURE = False (intern HTTP, extern HTTPS via Nginx)
-   - SESSION_COOKIE_SAMESITE = 'Lax' (CSRF-Schutz)
-   - SESSION_COOKIE_NAME = 'admin_session' (eindeutiger Name)
-   - SESSION_REFRESH_EACH_REQUEST = False (verhindert automatische Verlängerung)
-
-3. **Session-Handling verbessert:**
-   - Entfernt: session.permanent = True aus login_required decorator
-   - Hinzugefügt: session.modified = True im Heartbeat für explizites Speichern
-   - Debug-Logging für Session-Timeout-Prüfung hinzugefügt
-
-4. **Nginx-Konfiguration:**
-   - Bereits korrekt konfiguriert für Heartbeat-Weiterleitung
-   - Proxy-Headers für korrekte IP-Weitergabe
-
-**Technische Details:**
-- Flask-Session mit Filesystem-Backend nutzt jetzt korrekte Cookie-Einstellungen
-- Session-Cookie wird nicht mehr automatisch bei jedem Request verlängert
-- Explizite Session-Modifikation nur bei Heartbeat-Requests
-- Debug-Logs zeigen Zeit seit letzter Aktivität für Troubleshooting
-
-**Status:**
-- ✅ Session-Timeout-Mechanismus vollständig implementiert
-- ✅ Debug-Logging für Session-Überwachung aktiv
-- ✅ Cookie-Sicherheitseinstellungen optimiert
-
-### 2025-06-07 - CAPTCHA Backend-Validierung implementiert
-- Google reCAPTCHA v2 Backend-Verifizierung hinzugefügt
-
-**Implementierte Features:**
-1. **verify_recaptcha() Funktion (app.py):**
-   - Validiert CAPTCHA-Response mit Google API
-   - Fallback: Wenn RECAPTCHA_SECRET_KEY nicht konfiguriert, wird CAPTCHA übersprungen (für PoC)
-   - Timeout von 5 Sekunden für API-Request
-   - Error-Handling für Netzwerkfehler
-   - Logging für Debugging und Fehleranalyse
-
-2. **Login-Route Erweiterungen:**
-   - CAPTCHA wird nach 2 Fehlversuchen angezeigt
-   - Prüfung ob CAPTCHA-Response vorhanden
-   - Validierung der CAPTCHA-Response gegen Google API
-   - Unterschiedliche Fehlermeldungen für fehlende/ungültige CAPTCHA
-   - Site Key wird aus Environment-Variable an Template übergeben
-
-3. **Environment-Konfiguration (.env):**
-   - RECAPTCHA_SITE_KEY (für Frontend)
-   - RECAPTCHA_SECRET_KEY (für Backend-Validierung)
-   - Beide auskommentiert für PoC-Phase
-
-4. **Dependencies:**
-   - requests Library zu requirements.txt hinzugefügt
-
-**Sicherheitsaspekte:**
-- CAPTCHA verhindert automatisierte Brute-Force-Angriffe
-- Timing-Attack-Schutz bleibt auch bei CAPTCHA-Prüfung aktiv
-- Bei Netzwerkfehlern wird CAPTCHA als bestanden gewertet (Verfügbarkeit vor Sicherheit)
-- Secret Key wird niemals im Frontend exponiert
-
-**Verwendung:**
-1. Google reCAPTCHA v2 Keys erstellen: https://www.google.com/recaptcha/admin
-2. Keys in .env eintragen:
-   ```
-   RECAPTCHA_SITE_KEY=your-site-key
-   RECAPTCHA_SECRET_KEY=your-secret-key
-   ```
-3. Container neu starten
-
-**Status:**
-- ✅ CAPTCHA-Frontend bereits vorhanden (login.html)
-- ✅ Backend-Validierung vollständig implementiert
-- ✅ Fallback für PoC-Betrieb ohne Google-Keys
-- ✅ Integration in Rate-Limiting-System
-- ⚠️ CAPTCHA-Keys noch nicht konfiguriert (für PoC deaktiviert)
-
-**Anleitung für Google reCAPTCHA Keys:**
-
-1. **Registrierung bei Google reCAPTCHA:**
-   - Gehe zu: https://www.google.com/recaptcha/admin/create
-   - Melde dich mit Google-Konto an
-   - Label eingeben: "v2-Docker Admin Panel"
-   - Typ wählen: "reCAPTCHA v2" → "Ich bin kein Roboter"-Kästchen
-   - Domains hinzufügen:
-     ```
-     admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
-     localhost
-     ```
-   - Nutzungsbedingungen akzeptieren
-   - Senden klicken
-
-2. **Keys erhalten:**
-   - Site Key (öffentlich für Frontend)
-   - Secret Key (geheim für Backend-Validierung)
-
-3. **Keys in .env eintragen:**
-   ```bash
-   RECAPTCHA_SITE_KEY=6Ld...
-   RECAPTCHA_SECRET_KEY=6Ld...
-   ```
-
-4. **Container neu starten:**
-   ```bash
-   docker-compose down
-   docker-compose up -d
-   ```
-
-**Kosten:**
-- Kostenlos bis 1 Million Anfragen pro Monat
-- Danach: $1.00 pro 1000 zusätzliche Anfragen
-- Für dieses Projekt reicht die kostenlose Version vollkommen aus
-
-**Test-Keys für Entwicklung:**
-- Site Key: `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI`
-- Secret Key: `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe`
-- ⚠️ Diese Keys nur für lokale Tests verwenden, niemals produktiv!
-
-**Aktueller Status:**
-- Code ist vollständig implementiert und getestet
-- CAPTCHA wird nach 2 Fehlversuchen angezeigt
-- Ohne konfigurierte Keys wird CAPTCHA-Prüfung übersprungen
-- Für Produktion müssen nur die Keys in .env eingetragen werden
-
-### 2025-06-07 - License Key Generator implementiert
-- Automatische Generierung von Lizenzschlüsseln mit definiertem Format
-
-**Implementiertes Format:**
-`AF-YYYYMMFT-XXXX-YYYY-ZZZZ`
-- **AF** = Account Factory (feste Produktkennung)
-- **YYYY** = Jahr (z.B. 2025)
-- **MM** = Monat (z.B. 06)
-- **FT** = Lizenztyp (F=Fullversion, T=Testversion)
-- **XXXX-YYYY-ZZZZ** = Zufällige alphanumerische Zeichen (ohne verwirrende wie 0/O, 1/I/l)
-
-**Beispiele:**
-- Vollversion: `AF-202506F-A7K9-M3P2-X8R4`
-- Testversion: `AF-202512T-B2N5-K8L3-Q9W7`
-
-**Implementierte Features:**
-
-1. **Backend-Funktionen (app.py):**
-   - `generate_license_key()` - Generiert Keys mit kryptografisch sicherem Zufallsgenerator
-   - `validate_license_key()` - Validiert das Key-Format mit Regex
-   - Verwendet `secrets` statt `random` für Sicherheit
-   - Erlaubte Zeichen: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (ohne verwirrende)
-
-2. **API-Endpoint:**
-   - POST `/api/generate-license-key` - JSON API für Key-Generierung
-   - Prüft auf Duplikate in der Datenbank (max. 10 Versuche)
-   - Audit-Log-Eintrag bei jeder Generierung
-   - Login-Required geschützt
-
-3. **Frontend-Verbesserungen (index.html):**
-   - Generate-Button neben License Key Input
-   - Placeholder und Pattern-Attribut für Format-Hinweis
-   - Auto-Uppercase bei manueller Eingabe
-   - Visuelles Feedback bei erfolgreicher Generierung
-   - Format-Hinweis unter dem Eingabefeld
-
-4. **JavaScript-Features:**
-   - AJAX-basierte Key-Generierung ohne Seiten-Reload
-   - Automatische Prüfung bei Lizenztyp-Änderung
-   - Ladeindikator während der Generierung
-   - Fehlerbehandlung mit Benutzer-Feedback
-   - Standard-Datum-Einstellungen (heute + 1 Jahr)
-
-5. **Validierung:**
-   - Server-seitige Format-Validierung beim Speichern
-   - Flash-Message bei ungültigem Format
-   - Automatische Großschreibung des Keys
-   - Pattern-Validierung im HTML-Formular
-
-6. **Weitere Fixes:**
-   - Form Action von "/" auf "/create" korrigiert
-   - Flash-Messages mit Bootstrap Toasts implementiert
-   - GENERATE_KEY Aktion zum Audit-Log hinzugefügt (Farbe: #20c997)
-
-**Technische Details:**
-- Keine vorhersagbaren Muster durch `secrets.choice()`
-- Datum im Key zeigt Erstellungszeitpunkt
-- Lizenztyp direkt im Key erkennbar
-- Kollisionsprüfung gegen Datenbank
-
-**Status:**
-- ✅ Backend-Generierung vollständig implementiert
-- ✅ Frontend mit Generate-Button und JavaScript
-- ✅ Validierung und Fehlerbehandlung
-- ✅ Audit-Log-Integration
-- ✅ Form-Action-Bug behoben
-
-### 2025-06-07 - Batch-Lizenzgenerierung implementiert
-- Mehrere Lizenzen auf einmal für einen Kunden erstellen
-
-**Implementierte Features:**
-
-1. **Batch-Formular (/batch):**
-   - Kunde und E-Mail eingeben
-   - Anzahl der Lizenzen (1-100)
-   - Lizenztyp (Vollversion/Testversion)
-   - Gültigkeitszeitraum für alle Lizenzen
-   - Vorschau-Modal zeigt Key-Format
-   - Standard-Datum-Einstellungen (heute + 1 Jahr)
-
-2. **Backend-Verarbeitung:**
-   - Route `/batch` für GET (Formular) und POST (Generierung)
-   - Generiert die angegebene Anzahl eindeutiger Keys
-   - Speichert alle in einer Transaktion
-   - Kunde wird automatisch angelegt (falls nicht vorhanden)
-   - ON CONFLICT für existierende Kunden
-   - Audit-Log-Eintrag mit CREATE_BATCH Aktion
-
-3. **Ergebnis-Seite:**
-   - Zeigt alle generierten Lizenzen in Tabellenform
-   - Kundeninformationen und Gültigkeitszeitraum
-   - Einzelne Keys können kopiert werden (📋 Button)
-   - Alle Keys auf einmal kopieren
-   - Druckfunktion für physische Ausgabe
-   - Link zur Lizenzübersicht mit Kundenfilter
-
-4. **Export-Funktionalität:**
-   - Route `/batch/export` für CSV-Download
-   - Speichert Batch-Daten in Session für Export
-   - CSV mit UTF-8 BOM für Excel-Kompatibilität
-   - Enthält Kundeninfo, Generierungsdatum und alle Keys
-   - Format: Nr;Lizenzschlüssel;Typ
-   - Dateiname: batch_licenses_KUNDE_TIMESTAMP.csv
-
-5. **Integration:**
-   - Batch-Button in Navigation (Dashboard, Einzellizenz-Seite)
-   - CREATE_BATCH Aktion im Audit-Log (Farbe: #6610f2)
-   - Session-basierte Export-Daten
-   - Flash-Messages für Feedback
-
-**Sicherheit:**
-- Limit von 100 Lizenzen pro Batch
-- Login-Required für alle Routen
-- Transaktionale Datenbank-Operationen
-- Validierung der Eingaben
-
-**Beispiel-Workflow:**
-1. Admin geht zu `/batch`
-2. Gibt Kunde "Firma GmbH", Anzahl "25", Typ "Vollversion" ein
-3. System generiert 25 eindeutige Keys
-4. Ergebnis-Seite zeigt alle Keys
-5. Admin kann CSV exportieren oder Keys kopieren
-6. Kunde erhält die Lizenzen
-
-**Status:**
-- ✅ Batch-Formular vollständig implementiert
-- ✅ Backend-Generierung mit Transaktionen
-- ✅ Export als CSV
-- ✅ Copy-to-Clipboard Funktionalität
-- ✅ Audit-Log-Integration
-- ✅ Navigation aktualisiert
-
-## 2025-06-06: Implementierung Searchable Dropdown für Kundenauswahl
-
-**Problem:**
-- Bei der Lizenzerstellung wurde immer ein neuer Kunde angelegt
-- Keine Möglichkeit, Lizenzen für bestehende Kunden zu erstellen
-- Bei vielen Kunden wäre ein normales Dropdown unübersichtlich
-
-**Lösung:**
-1. **Select2 Library** für searchable Dropdown integriert
-2. **API-Endpoint `/api/customers`** für die Kundensuche erstellt
-3. **Frontend angepasst:**
-   - Searchable Dropdown mit Live-Suche
-   - Option "Neuer Kunde" im Dropdown
-   - Eingabefelder erscheinen nur bei "Neuer Kunde"
-4. **Backend-Logik verbessert:**
-   - Prüfung ob neuer oder bestehender Kunde
-   - E-Mail-Duplikatsprüfung vor Kundenerstellung
-   - Separate Audit-Logs für Kunde und Lizenz
-5. **Datenbank:**
-   - UNIQUE Constraint auf E-Mail-Spalte hinzugefügt
-
-**Änderungen:**
-- `app.py`: Neuer API-Endpoint `/api/customers`, angepasste Routes `/create` und `/batch`
-- `base.html`: Select2 CSS und JS eingebunden
-- `index.html`: Kundenauswahl mit Select2 implementiert
-- `batch_form.html`: Kundenauswahl mit Select2 implementiert
-- `init.sql`: UNIQUE Constraint für E-Mail
-
-**Status:**
-- ✅ API-Endpoint funktioniert mit Pagination
-- ✅ Select2 Dropdown mit Suchfunktion
-- ✅ Neue/bestehende Kunden können ausgewählt werden
-- ✅ E-Mail-Duplikate werden verhindert
-- ✅ Sowohl Einzellizenz als auch Batch unterstützt
-
-## 2025-06-06: Automatische Ablaufdatum-Berechnung
-
-**Problem:**
-- Manuelles Eingeben von Start- und Enddatum war umständlich
-- Fehleranfällig bei der Datumseingabe
-- Nicht intuitiv für Standard-Laufzeiten
-
-**Lösung:**
-1. **Frontend-Änderungen:**
-   - Startdatum + Laufzeit (Zahl) + Einheit (Tage/Monate/Jahre)
-   - Ablaufdatum wird automatisch berechnet und angezeigt (read-only)
-   - Standard: 1 Jahr Laufzeit voreingestellt
-2. **Backend-Validierung:**
-   - Server-seitige Berechnung zur Sicherheit
-   - Verwendung von `python-dateutil` für korrekte Monats-/Jahresberechnungen
-3. **Benutzerfreundlichkeit:**
-   - Sofortige Neuberechnung bei Änderungen
-   - Visuelle Hervorhebung des berechneten Datums
-
-**Änderungen:**
-- `index.html`: Laufzeit-Eingabe statt Ablaufdatum
-- `batch_form.html`: Laufzeit-Eingabe statt Ablaufdatum
-- `app.py`: Datum-Berechnung in `/create` und `/batch` Routes
-- `requirements.txt`: `python-dateutil` hinzugefügt
-
-**Status:**
-- ✅ Automatische Berechnung funktioniert
-- ✅ Frontend zeigt berechnetes Datum sofort an
-- ✅ Backend validiert die Berechnung
-- ✅ Standardwert (1 Jahr) voreingestellt
-
-## 2025-06-06: Bugfix - created_at für licenses Tabelle
-
-**Problem:**
-- Batch-Generierung schlug fehl mit "Fehler bei der Batch-Generierung!"
-- INSERT Statement versuchte `created_at` zu setzen, aber Spalte existierte nicht
-- Inkonsistenz: Einzellizenzen hatten kein created_at, Batch-Lizenzen versuchten es zu setzen
-
-**Lösung:**
-1. **Datenbank-Schema erweitert:**
-   - `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` zur licenses Tabelle hinzugefügt
-   - Migration für bestehende Datenbanken implementiert
-   - Konsistent mit customers Tabelle
-2. **Code bereinigt:**
-   - Explizites `created_at` aus Batch-INSERT entfernt
-   - Datenbank setzt nun automatisch den Zeitstempel bei ALLEN Lizenzen
-
-**Änderungen:**
-- `init.sql`: created_at Spalte zur licenses Tabelle mit DEFAULT-Wert
-- `init.sql`: Migration für bestehende Datenbanken
-- `app.py`: Entfernt explizites created_at aus batch_licenses()
-
-**Status:**
-- ✅ Alle Lizenzen haben nun automatisch einen Erstellungszeitstempel
-- ✅ Batch-Generierung funktioniert wieder
-- ✅ Konsistente Zeitstempel für Audit-Zwecke
-
-## 2025-06-06: Status "Deaktiviert" für manuell abgeschaltete Lizenzen
-
-**Problem:**
-- Dashboard zeigte nur "aktiv" und "abgelaufen" als Status
-- Manuell deaktivierte Lizenzen (is_active = FALSE) wurden nicht korrekt angezeigt
-- Filter für "inactive" existierte, aber Status wurde nicht richtig berechnet
-
-**Lösung:**
-1. **Status-Berechnung erweitert:**
-   - CASE-Statement prüft zuerst `is_active = FALSE`
-   - Status "deaktiviert" wird vor anderen Status geprüft
-   - Reihenfolge: deaktiviert → abgelaufen → läuft bald ab → aktiv
-2. **Dashboard-Statistik erweitert:**
-   - Neue Zählung für deaktivierte Lizenzen
-   - Variable `inactive_licenses` im stats Dictionary
-
-**Änderungen:**
-- `app.py`: Dashboard - Status-Berechnung für letzte 5 Lizenzen
-- `app.py`: Lizenzübersicht - Status-Berechnung in der Hauptabfrage
-- `app.py`: Export - Status-Berechnung für CSV/Excel Export
-- `app.py`: Dashboard - Neue Statistik für deaktivierte Lizenzen
-
-**Status:**
-- ✅ "Deaktiviert" wird korrekt als Status angezeigt
-- ✅ Dashboard zeigt Anzahl deaktivierter Lizenzen
-- ✅ Export enthält korrekten Status
-- ✅ Konsistente Status-Anzeige überall
-
-## 2025-06-08: SSL-Sicherheit verbessert - Chrome Warnung behoben
-
-**Problem:**
-- Chrome zeigte Warnung "Die Verbindung zu dieser Website ist nicht sicher"
-- Nginx erlaubte schwache Cipher Suites (WEAK) ohne Perfect Forward Secrecy
-- Veraltete SSL-Konfiguration mit `ssl_ciphers HIGH:!aNULL:!MD5;`
-
-**Lösung:**
-1. **Moderne Cipher Suite Konfiguration:**
-   - Nur sichere ECDHE und DHE Cipher Suites
-   - Entfernung aller RSA-only Cipher Suites
-   - Perfect Forward Secrecy für alle Verbindungen
-2. **SSL-Optimierungen:**
-   - Session Cache aktiviert (1 Tag Timeout)
-   - OCSP Stapling für bessere Performance
-   - DH Parameters (2048 bit) für zusätzliche Sicherheit
-3. **Resolver-Konfiguration:**
-   - Google DNS Server für OCSP Stapling
-
-**Änderungen:**
-- `v2_nginx/nginx.conf`: Komplett überarbeitete SSL-Konfiguration
-- `v2_nginx/ssl/dhparam.pem`: Neue 2048-bit DH Parameters generiert
-- `v2_nginx/Dockerfile`: COPY Befehl für dhparam.pem hinzugefügt
-
-**Status:**
-- ✅ Nur noch sichere Cipher Suites aktiv
-- ✅ Perfect Forward Secrecy gewährleistet
-- ✅ OCSP Stapling aktiviert
-- ✅ Chrome Sicherheitswarnung behoben
-
-**Hinweis:** Nach dem Rebuild des nginx Containers wird die Verbindung als sicher angezeigt.
-
-## 2025-06-08: CAPTCHA-Login-Bug behoben
-
-**Problem:**
-- Nach 2 fehlgeschlagenen Login-Versuchen wurde CAPTCHA angezeigt
-- Da keine CAPTCHA-Keys konfiguriert waren (für PoC), konnte man sich nicht mehr einloggen
-- Selbst mit korrektem Passwort war Login blockiert
-- Fehlermeldung "CAPTCHA ERFORDERLICH!" erschien immer
-
-**Lösung:**
-1. **CAPTCHA-Prüfung nur wenn Keys vorhanden:**
-   - `recaptcha_site_key` wird vor CAPTCHA-Prüfung geprüft
-   - Wenn keine Keys konfiguriert → kein CAPTCHA-Check
-   - CAPTCHA wird nur angezeigt wenn Keys existieren
-2. **Template-Anpassungen:**
-   - login.html zeigt CAPTCHA nur wenn `recaptcha_site_key` vorhanden
-   - Kein Test-Key mehr als Fallback
-3. **Konsistente Logik:**
-   - show_captcha prüft jetzt auch ob Keys vorhanden sind
-   - Bei GET und POST Requests gleiche Logik
-
-**Änderungen:**
-- `v2_adminpanel/app.py`: CAPTCHA-Check nur wenn `RECAPTCHA_SITE_KEY` existiert
-- `v2_adminpanel/templates/login.html`: CAPTCHA nur anzeigen wenn Keys vorhanden
-
-**Status:**
-- ✅ Login funktioniert wieder nach 2+ Fehlversuchen
-- ✅ CAPTCHA wird nur angezeigt wenn Keys konfiguriert sind
-- ✅ Für PoC-Phase ohne CAPTCHA nutzbar
-- ✅ Produktiv-ready wenn CAPTCHA-Keys eingetragen werden
-
-### 2025-06-08: Zeitzone auf Europe/Berlin umgestellt
-
-**Problem:**
-- Alle Zeitstempel wurden in UTC gespeichert und angezeigt
-- Backup-Dateinamen zeigten UTC-Zeit statt deutsche Zeit
-- Verwirrung bei Zeitangaben im Admin Panel und Logs
-
-**Lösung:**
-1. **Docker Container Zeitzone konfiguriert:**
-   - Alle Dockerfiles mit `TZ=Europe/Berlin` und tzdata Installation
-   - PostgreSQL mit `PGTZ=Europe/Berlin` für Datenbank-Zeitzone
-   - Explizite Zeitzone-Dateien in /etc/localtime und /etc/timezone
-
-2. **Python Code angepasst:**
-   - Import von `zoneinfo.ZoneInfo` für Zeitzonenunterstützung
-   - Alle `datetime.now()` Aufrufe mit `ZoneInfo("Europe/Berlin")`
-   - `.replace(tzinfo=None)` für Kompatibilität mit timezone-unaware Timestamps
-
-3. **PostgreSQL Konfiguration:**
-   - `SET timezone = 'Europe/Berlin';` in init.sql
-   - Umgebungsvariablen TZ und PGTZ in docker-compose.yaml
-
-4. **docker-compose.yaml erweitert:**
-   - `TZ: Europe/Berlin` für alle Services
-
-**Geänderte Dateien:**
-- `v2_adminpanel/Dockerfile`: Zeitzone und tzdata hinzugefügt
-- `v2_postgres/Dockerfile`: Zeitzone und tzdata hinzugefügt
-- `v2_nginx/Dockerfile`: Zeitzone und tzdata hinzugefügt
-- `v2_lizenzserver/Dockerfile`: Zeitzone und tzdata hinzugefügt
-- `v2_adminpanel/app.py`: 14 datetime.now() Aufrufe mit Zeitzone versehen
-- `v2_adminpanel/init.sql`: PostgreSQL Zeitzone gesetzt
-- `v2/docker-compose.yaml`: TZ Environment-Variable für alle Services
-
-**Ergebnis:**
-- ✅ Alle neuen Zeitstempel werden in deutscher Zeit (Europe/Berlin) gespeichert
-- ✅ Backup-Dateinamen zeigen korrekte deutsche Zeit
-- ✅ Admin Panel zeigt alle Zeiten in deutscher Zeitzone
-- ✅ Automatische Anpassung bei Sommer-/Winterzeit
-- ✅ Konsistente Zeitangaben über alle Komponenten
-
-**Hinweis:** Nach diesen Änderungen müssen die Docker Container neu gebaut werden:
-```bash
-docker-compose down
-docker-compose build
-docker-compose up -d
-```
-
-### 2025-06-08: Zeitzone-Fix - PostgreSQL Timestamps
-
-**Problem nach erster Implementierung:**
-- Trotz Zeitzoneneinstellung wurden Zeiten immer noch in UTC angezeigt
-- Grund: PostgreSQL Tabellen verwendeten `TIMESTAMP WITHOUT TIME ZONE`
-
-**Zusätzliche Lösung:**
-1. **Datenbankschema angepasst:**
-   - Alle `TIMESTAMP` Spalten auf `TIMESTAMP WITH TIME ZONE` geändert
-   - Betrifft: created_at, timestamp, started_at, ended_at, last_heartbeat, etc.
-   - Migration für bestehende Datenbanken berücksichtigt
-
-2. **SQL-Abfragen vereinfacht:**
-   - `AT TIME ZONE 'Europe/Berlin'` nicht mehr nötig
-   - PostgreSQL handhabt Zeitzonenkonvertierung automatisch
-
-**Geänderte Datei:**
-- `v2_adminpanel/init.sql`: Alle TIMESTAMP Felder mit WITH TIME ZONE
-
-**Wichtig:** Bei bestehenden Installationen muss die Datenbank neu initialisiert oder manuell migriert werden:
-```sql
-ALTER TABLE customers ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE licenses ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE sessions ALTER COLUMN started_at TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE sessions ALTER COLUMN last_heartbeat TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE sessions ALTER COLUMN ended_at TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE audit_log ALTER COLUMN timestamp TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE backup_history ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE login_attempts ALTER COLUMN first_attempt TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE login_attempts ALTER COLUMN last_attempt TYPE TIMESTAMP WITH TIME ZONE;
-ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME ZONE;
-```
-
-### 2025-06-08: UI/UX Überarbeitung - Phase 1 (Navigation)
-
-**Problem:**
-- Inkonsistente Navigation zwischen verschiedenen Seiten
-- Zu viele Navigationspunkte im Dashboard
-- Verwirrende Benutzerführung
-
-**Lösung:**
-1. **Dashboard vereinfacht:**
-   - Nur noch 3 Buttons: Neue Lizenz, Batch-Lizenzen, Log
-   - Statistik-Karten wurden klickbar gemacht (verlinken zu jeweiligen Seiten)
-   - "Audit" wurde zu "Log" umbenannt
-
-2. **Navigation konsistent gemacht:**
-   - Navbar-Brand "AccountForger - Admin Panel" ist jetzt klickbar und führt zum Dashboard
-   - Keine Log-Links mehr in Unterseiten
-   - Konsistente "Dashboard" Buttons in allen Unterseiten
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/base.html`: Navbar-Brand klickbar gemacht
-- `v2_adminpanel/templates/dashboard.html`: Navigation reduziert, Karten klickbar
-- `v2_adminpanel/templates/*.html`: Konsistente Dashboard-Links
-
-### 2025-06-08: UI/UX Überarbeitung - Phase 2 (Visuelle Verbesserungen)
-
-**Implementierte Verbesserungen:**
-1. **Größere Icons in Statistik-Karten:**
-   - Icon-Größe auf 3rem erhöht
-   - Bessere visuelle Hierarchie
-
-2. **Donut-Chart für Lizenzen:**
-   - Chart.js Integration für Lizenzstatistik
-   - Zeigt Verhältnis Aktiv/Abgelaufen
-   - UPDATE: Später wieder entfernt auf Benutzerwunsch
-
-3. **Pulse-Effekt für aktive Sessions:**
-   - CSS-Animation für aktive Sessions
-   - Visueller Indikator für Live-Aktivität
-
-4. **Progress-Bar für Backup-Status:**
-   - Zeigt visuell den Erfolg des letzten Backups
-   - Inkl. Dateigröße und Dauer
-
-5. **Konsistente Farbcodierung:**
-   - CSS-Variablen für Statusfarben
-   - Globale Klassen für konsistente Darstellung
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/base.html`: Globale CSS-Variablen und Statusklassen
-- `v2_adminpanel/templates/dashboard.html`: Visuelle Verbesserungen implementiert
-
-### 2025-06-08: UI/UX Überarbeitung - Phase 3 (Tabellen-Optimierungen)
-
-**Problem:**
-- Tabellen waren schwer zu navigieren bei vielen Einträgen
-- Keine Möglichkeit für Bulk-Operationen
-- Umständliches Kopieren von Lizenzschlüsseln
-
-**Lösung:**
-1. **Sticky Headers:**
-   - Tabellenköpfe bleiben beim Scrollen sichtbar
-   - CSS-Klasse `.table-sticky` mit `position: sticky`
-
-2. **Inline-Actions:**
-   - Copy-Button direkt neben Lizenzschlüsseln
-   - Toggle-Switches für Aktiv/Inaktiv-Status
-   - Visuelles Feedback bei Aktionen
-
-3. **Bulk-Actions:**
-   - Checkboxen für Mehrfachauswahl
-   - "Select All" Funktionalität
-   - Bulk-Actions Bar mit Aktivieren/Deaktivieren/Löschen
-   - JavaScript für dynamische Anzeige
-
-4. **API-Endpoints hinzugefügt:**
-   - `/api/license/<id>/toggle` - Toggle einzelner Lizenzstatus
-   - `/api/licenses/bulk-activate` - Mehrere Lizenzen aktivieren
-   - `/api/licenses/bulk-deactivate` - Mehrere Lizenzen deaktivieren
-   - `/api/licenses/bulk-delete` - Mehrere Lizenzen löschen
-
-5. **Beispieldaten eingefügt:**
-   - 15 Testkunden
-   - 18 Lizenzen (verschiedene Status)
-   - Sessions, Audit-Logs, Login-Attempts
-   - Backup-Historie
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/base.html`: CSS für Sticky-Tables und Bulk-Actions
-- `v2_adminpanel/templates/licenses.html`: Komplette Tabellen-Überarbeitung
-- `v2_adminpanel/app.py`: 4 neue API-Endpoints für Toggle und Bulk-Operationen
-- `v2_adminpanel/sample_data.sql`: Umfangreiche Testdaten erstellt
-
-**Bugfix:**
-- API-Endpoints versuchten `updated_at` zu setzen, obwohl die Spalte nicht existiert
-- Entfernt aus allen 3 betroffenen Endpoints
-
-**Status:**
-- ✅ Sticky Headers funktionieren
-- ✅ Copy-Buttons mit Clipboard-API
-- ✅ Toggle-Switches ändern Lizenzstatus
-- ✅ Bulk-Operationen vollständig implementiert
-- ✅ 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
-
-### 2025-06-08: Bugfix - Sortierlogik korrigiert
-
-**Problem:**
-- Sortierung funktionierte nicht korrekt
-- Beim Klick auf Spaltenköpfe wurde immer absteigend sortiert
-- Toggle zwischen ASC/DESC funktionierte nicht
-
-**Ursachen:**
-1. **Falsche Bedingungslogik**: Die ursprüngliche Implementierung verwendete eine fehlerhafte Ternär-Bedingung
-2. **Berechnete Felder**: Das 'status' Feld in der Lizenztabelle konnte nicht direkt sortiert werden
-
-**Lösung:**
-1. **Sortierlogik korrigiert:**
-   - Bei neuer Spalte: Immer aufsteigend (ASC) beginnen
-   - Bei gleicher Spalte: Toggle zwischen ASC und DESC
-   - Implementiert durch bedingte Links in den Makros
-
-2. **Spezialbehandlung für berechnete Felder:**
-   - Status-Feld verwendet CASE-Statement in ORDER BY
-   - Wiederholt die gleiche Logik wie im SELECT
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/licenses.html`: Sortierlogik korrigiert
-- `v2_adminpanel/templates/customers.html`: Sortierlogik korrigiert
-- `v2_adminpanel/templates/audit_log.html`: Sortierlogik korrigiert
-- `v2_adminpanel/templates/sessions.html`: Sortierlogik für beide Tabellen korrigiert
-- `v2_adminpanel/app.py`: Spezialbehandlung für Status-Feld in licenses Route
-
-**Verhalten nach Fix:**
-- ✅ Erster Klick auf Spalte: Aufsteigend sortieren
-- ✅ Zweiter Klick: Absteigend sortieren
-- ✅ Weitere Klicks: Toggle zwischen ASC/DESC
-- ✅ Sortierung funktioniert korrekt mit Pagination und Filtern
-
-### 2025-06-09: Port 8443 geschlossen - API nur noch über Nginx
-
-**Problem:**
-- Doppelte Verfügbarkeit des License Servers (Port 8443 + Nginx) machte keinen Sinn
-- Inkonsistente Sicherheitskonfiguration (Nginx hatte Security Headers, Port 8443 nicht)
-- Doppelte SSL-Konfiguration nötig
-- Verwirrung welcher Zugangsweg genutzt werden soll
-
-**Lösung:**
-- Port-Mapping für License Server in docker-compose.yaml entfernt
-- API nur noch über Nginx erreichbar: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com
-- Interne Kommunikation zwischen Nginx und License Server bleibt bestehen
-
-**Vorteile:**
-- ✅ Einheitliche Sicherheitskonfiguration (Security Headers, HSTS)
-- ✅ Zentrale SSL-Verwaltung nur in Nginx
-- ✅ Möglichkeit für Rate Limiting und zentrales Logging
-- ✅ Keine zusätzlichen offenen Ports (nur 80/443)
-- ✅ Professionellere API-URL ohne Port-Angabe
-
-**Geänderte Dateien:**
-- `v2/docker-compose.yaml`: Port-Mapping "8443:8443" entfernt
-
-**Hinweis für Client-Software:**
-- API-Endpunkte sind weiterhin unter https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com erreichbar
-- Keine Änderung der API-URLs nötig, nur Port 8443 ist nicht mehr direkt zugänglich
-
-**Status:**
-- ✅ Port 8443 geschlossen
-- ✅ API nur noch über Nginx Reverse Proxy erreichbar
-- ✅ Sicherheit erhöht durch zentrale Verwaltung
-
-### 2025-06-09: Live-Filtering implementiert
-
-**Problem:**
-- Benutzer mussten immer auf "Filter anwenden" klicken
-- Umständliche Bedienung, besonders bei mehreren Filterkriterien
-- Nicht zeitgemäße User Experience
-
-**Lösung:**
-- JavaScript Event-Listener für automatisches Filtern
-- Text-Eingaben: 300ms Debouncing (verzögerte Suche nach Tipp-Pause)
-- Dropdowns: Sofortiges Filtern bei Änderung
-- "Filter anwenden" Button entfernt, nur "Zurücksetzen" bleibt
-
-**Implementierte Live-Filter:**
-1. **Lizenzübersicht** (licenses.html):
-   - Suchfeld mit Debouncing
-   - Typ-Dropdown (Vollversion/Testversion)
-   - Status-Dropdown (Aktiv/Ablaufend/Abgelaufen/Deaktiviert)
-
-2. **Kundenübersicht** (customers.html):
-   - Suchfeld mit Debouncing
-   - "Suchen" Button entfernt
-
-3. **Audit-Log** (audit_log.html):
-   - Benutzer-Textfeld mit Debouncing
-   - Aktion-Dropdown
-   - Entität-Dropdown
-
-**Technische Details:**
-- `addEventListener('input')` für Textfelder
-- `addEventListener('change')` für Select-Elemente
-- `setTimeout()` mit 300ms für Debouncing
-- Automatisches `form.submit()` bei Änderungen
-
-**Vorteile:**
-- ✅ Schnellere und intuitivere Bedienung
-- ✅ Weniger Klicks erforderlich
-- ✅ Moderne User Experience
-- ✅ Besonders hilfreich bei komplexen Filterkriterien
-
-**Status:**
-- ✅ Live-Filtering auf allen Hauptseiten implementiert
-- ✅ Debouncing verhindert zu viele Server-Requests
-- ✅ Zurücksetzen-Button bleibt für schnelles Löschen aller Filter
-
-### 2025-06-09: Resource Pool System implementiert (Phase 1 & 2)
-
-**Ziel:**
-Ein Pool-System für Domains, IPv4-Adressen und Telefonnummern, wobei bei jeder Lizenzerstellung 1-10 Ressourcen pro Typ zugewiesen werden. Ressourcen haben 3 Status: available, allocated, quarantine.
-
-**Phase 1 - Datenbank-Schema (✅ Abgeschlossen):**
-1. **Neue Tabellen erstellt:**
-   - `resource_pools` - Haupttabelle für alle Ressourcen
-   - `resource_history` - Vollständige Historie aller Aktionen
-   - `resource_metrics` - Performance-Tracking und ROI-Berechnung
-   - `license_resources` - Zuordnung zwischen Lizenzen und Ressourcen
-
-2. **Erweiterte licenses Tabelle:**
-   - `domain_count`, `ipv4_count`, `phone_count` Spalten hinzugefügt
-   - Constraints: 0-10 pro Resource-Typ
-
-3. **Indizes für Performance:**
-   - Status, Type, Allocated License, Quarantine Date
-
-**Phase 2 - Backend-Implementierung (✅ Abgeschlossen):**
-1. **Resource Management Routes:**
-   - `/resources` - Hauptübersicht mit Statistiken
-   - `/resources/add` - Bulk-Import von Ressourcen
-   - `/resources/quarantine/<id>` - Ressourcen sperren
-   - `/resources/release` - Quarantäne aufheben
-   - `/resources/history/<id>` - Komplette Historie
-   - `/resources/metrics` - Performance Dashboard
-   - `/resources/report` - Report-Generator
-
-2. **API-Endpunkte:**
-   - `/api/resources/allocate` - Ressourcen-Zuweisung
-   - `/api/resources/check-availability` - Verfügbarkeit prüfen
-
-3. **Integration in Lizenzerstellung:**
-   - `create_license()` erweitert um Resource-Allocation
-   - `batch_licenses()` mit Ressourcen-Prüfung für gesamten Batch
-   - Transaktionale Sicherheit bei Zuweisung
-
-4. **Dashboard-Integration:**
-   - Resource-Statistiken in Dashboard eingebaut
-   - Warning-Level basierend auf Verfügbarkeit
-
-5. **Navigation erweitert:**
-   - Resources-Link in Navbar hinzugefügt
-
-**Was noch zu tun ist:**
-
-### Phase 3 - UI-Komponenten (🔄 Ausstehend):
-1. **Templates erstellen:**
-   - `resources.html` - Hauptübersicht mit Drag&Drop
-   - `add_resources.html` - Formular für Bulk-Import
-   - `resource_history.html` - Historie-Anzeige
-   - `resource_metrics.html` - Performance Dashboard
-
-2. **Formulare erweitern:**
-   - `index.html` - Resource-Dropdowns hinzufügen
-   - `batch_form.html` - Resource-Dropdowns hinzufügen
-
-3. **Dashboard-Widget:**
-   - Resource Pool Statistik mit Ampelsystem
-   - Warnung bei niedrigem Bestand
-
-### Phase 4 - Erweiterte Features (🔄 Ausstehend):
-1. **Quarantäne-Workflow:**
-   - Gründe: abuse, defect, maintenance, blacklisted, expired
-   - Automatische Tests vor Freigabe
-   - Genehmigungsprozess
-
-2. **Performance-Metrics:**
-   - Täglicher Cronjob für Metriken
-   - ROI-Berechnung
-   - Issue-Tracking
-
-3. **Report-Generator:**
-   - Auslastungsreport
-   - Performance-Report
-   - Compliance-Report
-
-### Phase 5 - Backup erweitern (🔄 Ausstehend):
-- Neue Tabellen in Backup einbeziehen:
-  - resource_pools
-  - resource_history
-  - resource_metrics
-  - license_resources
-
-### Phase 6 - Testing & Migration (🔄 Ausstehend):
-1. **Test-Daten generieren:**
-   - 500 Test-Domains
-   - 200 Test-IPs
-   - 100 Test-Telefonnummern
-
-2. **Migrations-Script:**
-   - Bestehende Lizenzen auf default resource_count setzen
-
-### Phase 7 - Dokumentation (🔄 Ausstehend):
-- API-Dokumentation für License Server
-- Admin-Handbuch für Resource Management
-
-**Technische Details:**
-- 3-Status-System: available/allocated/quarantine
-- Transaktionale Ressourcen-Zuweisung mit FOR UPDATE Lock
-- Vollständige Historie mit IP-Tracking
-- Drag&Drop UI für Resource-Management geplant
-- Automatische Warnung bei < 50 verfügbaren Ressourcen
-
-**Status:**
-- ✅ Datenbank-Schema komplett
-- ✅ Backend-Routen implementiert
-- ✅ Integration in Lizenzerstellung
-- ❌ UI-Templates fehlen noch
-- ❌ Erweiterte Features ausstehend
-- ❌ Testing und Migration offen
-
-### 2025-06-09: Resource Pool System UI-Implementierung (Phase 3 & 4)
-
-**Phase 3 - UI-Komponenten (✅ Abgeschlossen):**
-
-1. **Neue Templates erstellt:**
-   - `resources.html` - Hauptübersicht mit Statistiken, Filter, Live-Suche, Pagination
-   - `add_resources.html` - Bulk-Import Formular mit Validierung
-   - `resource_history.html` - Timeline-Ansicht der Historie mit Details
-   - `resource_metrics.html` - Performance Dashboard mit Charts
-   - `resource_report.html` - Report-Generator UI
-
-2. **Erweiterte Formulare:**
-   - `index.html` - Resource-Count Dropdowns (0-10) mit Live-Verfügbarkeitsprüfung
-   - `batch_form.html` - Resource-Count mit Batch-Berechnung (zeigt Gesamtbedarf)
-   
-3. **Dashboard-Widget:**
-   - Resource Pool Statistik mit Ampelsystem implementiert
-   - Zeigt verfügbare/zugeteilte/quarantäne Ressourcen
-   - Warnung bei niedrigem Bestand (<50)
-   - Fortschrittsbalken für visuelle Darstellung
-
-4. **Backend-Anpassungen:**
-   - `resource_history` Route korrigiert für Object-Style Template-Zugriff
-   - `resources_metrics` Route vollständig implementiert mit Charts-Daten
-   - `resources_report` Route erweitert für Template-Anzeige und Downloads
-   - Dashboard erweitert um Resource-Statistiken
-
-**Phase 4 - Erweiterte Features (✅ Teilweise):**
-1. **Report-Generator:**
-   - Template für Report-Auswahl erstellt
-   - 4 Report-Typen: Usage, Performance, Compliance, Inventory
-   - Export als Excel, CSV oder PDF-Vorschau
-   - Zeitraum-Auswahl mit Validierung
-
-**Technische Details der Implementierung:**
-- Live-Filtering ohne Reload durch JavaScript
-- AJAX-basierte Verfügbarkeitsprüfung
-- Bootstrap 5 für konsistentes Design
-- Chart.js für Metriken-Visualisierung
-- Responsives Design für alle Templates
-- Copy-to-Clipboard für Resource-Werte
-- Modal-Dialoge für Quarantäne-Aktionen
-
-**Was noch fehlt:**
-
-### Phase 5 - Backup erweitern (🔄 Ausstehend):
-- Resource-Tabellen in pg_dump einbeziehen:
-  - resource_pools
-  - resource_history  
-  - resource_metrics
-  - license_resources
-
-### Phase 6 - Testing & Migration (🔄 Ausstehend):
-1. **Test-Daten generieren:**
-   - Script für 500 Test-Domains
-   - 200 Test-IPv4-Adressen
-   - 100 Test-Telefonnummern
-   - Realistische Verteilung über Status
-
-2. **Migrations-Script:**
-   - Bestehende Lizenzen auf Default resource_count setzen
-   - UPDATE licenses SET domain_count=1, ipv4_count=1, phone_count=1 WHERE ...
-
-### Phase 7 - Dokumentation (🔄 Ausstehend):
-- API-Dokumentation für Resource-Endpunkte
-- Admin-Handbuch für Resource Management
-- Troubleshooting-Guide
-
-**Offene Punkte für Produktion:**
-1. Drag&Drop für Resource-Verwaltung (Nice-to-have)
-2. Automatische Quarantäne-Aufhebung nach Zeitablauf
-3. E-Mail-Benachrichtigungen bei niedrigem Bestand
-4. API für externe Resource-Prüfung
-5. Bulk-Delete für Ressourcen
-6. Resource-Import aus CSV/Excel
-
-### 2025-06-09: Resource Pool System finalisiert
-
-**Problem:**
-- Resource Pool System war nur teilweise implementiert
-- UI-Templates waren vorhanden, aber nicht dokumentiert
-- Test-Daten und Migration fehlten
-- Backup-Integration unklar
-
-**Analyse und Lösung:**
-1. **Status-Überprüfung durchgeführt:**
-   - Alle 5 UI-Templates existierten bereits (resources.html, add_resources.html, etc.)
-   - Resource-Dropdowns waren bereits in index.html und batch_form.html integriert
-   - Dashboard-Widget war bereits implementiert
-   - Backup-System inkludiert bereits alle Tabellen (pg_dump ohne -t Parameter)
-
-2. **Fehlende Komponenten erstellt:**
-   - Test-Daten Script: `test_data_resources.sql`
-     - 500 Test-Domains (400 verfügbar, 50 zugeteilt, 50 in Quarantäne)
-     - 200 Test-IPv4-Adressen (150 verfügbar, 30 zugeteilt, 20 in Quarantäne)
-     - 100 Test-Telefonnummern (70 verfügbar, 20 zugeteilt, 10 in Quarantäne)
-     - Resource History und Metrics für realistische Daten
-   
-   - Migration Script: `migrate_existing_licenses.sql`
-     - Setzt Default Resource Counts (Vollversion: 2, Testversion: 1, Inaktiv: 0)
-     - Weist automatisch verfügbare Ressourcen zu
-     - Erstellt Audit-Log Einträge
-     - Gibt detaillierten Migrationsbericht aus
-
-**Neue Dateien:**
-- `v2_adminpanel/test_data_resources.sql` - Testdaten für Resource Pool
-- `v2_adminpanel/migrate_existing_licenses.sql` - Migration für bestehende Lizenzen
-
-**Status:**
-- ✅ Resource Pool System vollständig implementiert und dokumentiert
-- ✅ Alle UI-Komponenten vorhanden und funktionsfähig
-- ✅ Integration in Lizenz-Formulare abgeschlossen
-- ✅ Dashboard-Widget zeigt Resource-Statistiken
-- ✅ Backup-System inkludiert Resource-Tabellen
-- ✅ Test-Daten und Migration bereitgestellt
-
-**Nächste Schritte:**
-1. Test-Daten einspielen: `psql -U adminuser -d meinedatenbank -f test_data_resources.sql`
-2. Migration ausführen: `psql -U adminuser -d meinedatenbank -f migrate_existing_licenses.sql`
-3. License Server API implementieren (Hauptaufgabe)
-
-### 2025-06-09: Bugfix - Resource Pool Tabellen fehlten
-
-**Problem:**
-- Admin Panel zeigte "Internal Server Error"
-- Dashboard Route versuchte auf `resource_pools` Tabelle zuzugreifen
-- Tabelle existierte nicht in der Datenbank
-
-**Ursache:**
-- Bei bereits existierender Datenbank wird init.sql nicht erneut ausgeführt
-- Resource Pool Tabellen wurden erst später zum init.sql hinzugefügt
-- Docker Container verwendeten noch die alte Datenbankstruktur
-
-**Lösung:**
-1. Separates Script `create_resource_tables.sql` erstellt
-2. Script manuell in der Datenbank ausgeführt
-3. Alle 4 Resource-Tabellen erfolgreich erstellt:
-   - resource_pools
-   - resource_history
-   - resource_metrics
-   - license_resources
-
-**Status:**
-- ✅ Admin Panel funktioniert wieder
-- ✅ Dashboard zeigt Resource Pool Statistiken
-- ✅ Alle Resource-Funktionen verfügbar
-
-**Empfehlung für Neuinstallationen:**
-- Bei frischer Installation funktioniert alles automatisch
-- Bei bestehenden Installationen: `create_resource_tables.sql` ausführen
-
-### 2025-06-09: Navigation vereinfacht
-
-**Änderung:**
-- Navigationspunkte aus der schwarzen Navbar entfernt
-- Links zu Lizenzen, Kunden, Ressourcen, Sessions, Backups und Log entfernt
-
-**Grund:**
-- Cleaner Look mit nur Logo, Timer und Logout
-- Alle Funktionen sind weiterhin über das Dashboard erreichbar
-- Bessere Übersichtlichkeit und weniger Ablenkung
-
-**Geänderte Datei:**
-- `v2_adminpanel/templates/base.html` - Navbar-Links auskommentiert
-
-**Status:**
-- ✅ Navbar zeigt nur noch Logo, Session-Timer und Logout
-- ✅ Navigation erfolgt über Dashboard und Buttons auf den jeweiligen Seiten
-- ✅ Alle Funktionen bleiben erreichbar
-
-### 2025-06-09: Bugfix - Resource Report Einrückungsfehler
-
-**Problem:**
-- Resource Report Route zeigte "Internal Server Error"
-- UnboundLocalError: `report_type` wurde verwendet bevor es definiert war
-
-**Ursache:**
-- Fehlerhafte Einrückung in der `resources_report()` Funktion
-- `elif` und `else` Blöcke waren falsch eingerückt
-- Variablen wurden außerhalb ihres Gültigkeitsbereichs verwendet
-
-**Lösung:**
-- Korrekte Einrückung für alle Conditional-Blöcke wiederhergestellt
-- Alle Report-Typen (usage, performance, compliance, inventory) richtig strukturiert
-- Excel und CSV Export-Code korrekt eingerückt
-
-**Geänderte Datei:**
-- `v2_adminpanel/app.py` - resources_report() Funktion korrigiert
-
-**Status:**
-- ✅ Resource Report funktioniert wieder
-- ✅ Alle 4 Report-Typen verfügbar
-- ✅ Export als Excel und CSV möglich
-
----
-
-## Zusammenfassung der heutigen Arbeiten (2025-06-09)
-
-### 1. Resource Pool System Finalisierung
-- **Ausgangslage**: Resource Pool war nur teilweise dokumentiert
-- **Überraschung**: UI-Templates waren bereits vorhanden (nicht dokumentiert)
-- **Ergänzt**: 
-  - Test-Daten Script (`test_data_resources.sql`)
-  - Migration Script (`migrate_existing_licenses.sql`)
-- **Status**: ✅ Vollständig implementiert
-
-### 2. Database Migration Bug
-- **Problem**: Admin Panel zeigte "Internal Server Error"
-- **Ursache**: Resource Pool Tabellen fehlten in bestehender DB
-- **Lösung**: Separates Script `create_resource_tables.sql` erstellt
-- **Status**: ✅ Behoben
-
-### 3. UI Cleanup
-- **Änderung**: Navigation aus Navbar entfernt
-- **Effekt**: Cleaner Look, Navigation nur über Dashboard
-- **Status**: ✅ Implementiert
-
-### 4. Resource Report Bug
-- **Problem**: Einrückungsfehler in `resources_report()` Funktion
-- **Lösung**: Korrekte Einrückung wiederhergestellt
-- **Status**: ✅ Behoben
-
-### Neue Dateien erstellt heute:
-1. `v2_adminpanel/test_data_resources.sql` - 800 Test-Ressourcen
-
-### 2025-06-09: Bugfix - Resource Quarantäne Modal
-
-**Problem:**
-- Quarantäne-Button funktionierte nicht
-- Modal öffnete sich nicht beim Klick
-
-**Ursache:**
-- Bootstrap 5 vs Bootstrap 4 API-Inkompatibilität
-- Modal wurde mit Bootstrap 4 Syntax (`modal.modal('show')`) aufgerufen
-- jQuery wurde nach Bootstrap geladen
-
-**Lösung:**
-1. **JavaScript angepasst:**
-   - Von jQuery Modal-API zu nativer Bootstrap 5 Modal-API gewechselt
-   - `new bootstrap.Modal(element).show()` statt `$(element).modal('show')`
-
-2. **HTML-Struktur aktualisiert:**
-   - Modal-Close-Button: `data-bs-dismiss="modal"` statt `data-dismiss="modal"`
-   - `btn-close` Klasse statt custom close button
-   - Form-Klassen: `mb-3` statt `form-group`, `form-select` statt `form-control` für Select
-
-3. **Script-Reihenfolge korrigiert:**
-   - jQuery vor Bootstrap laden für korrekte Initialisierung
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/resources.html`
-- `v2_adminpanel/templates/base.html`
-
-**Status:** ✅ Behoben
-
-### 2025-06-09: Resource Pool UI Redesign
-
-**Ziel:**
-- Komplettes Redesign des Resource Pool Managements für bessere Benutzerfreundlichkeit
-- Konsistentes Design mit dem Rest der Anwendung
-
-**Durchgeführte Änderungen:**
-
-1. **resources.html - Hauptübersicht:**
-   - Moderne Statistik-Karten mit Hover-Effekten
-   - Farbcodierte Progress-Bars mit Tooltips
-   - Verbesserte Tabelle mit Icons und Status-Badges
-   - Live-Filter mit sofortiger Suche
-   - Überarbeitete Quarantäne-Modal für Bootstrap 5
-   - Responsive Design mit Grid-Layout
-
-2. **add_resources.html - Ressourcen hinzufügen:**
-   - 3-Schritt Wizard-ähnliches Interface
-   - Visueller Ressourcentyp-Selector mit Icons
-   - Live-Validierung mit Echtzeit-Feedback
-   - Statistik-Anzeige (Gültig/Duplikate/Ungültig)
-   - Formatierte Beispiele mit Erklärungen
-   - Verbesserte Fehlerbehandlung
-
-3. **resource_history.html - Historie:**
-   - Zentrierte Resource-Anzeige mit großen Icons
-   - Info-Grid Layout für Details
-   - Modernisierte Timeline mit Hover-Effekten
-   - Farbcodierte Action-Icons
-   - Verbesserte Darstellung von Details
-
-4. **resource_metrics.html - Metriken:**
-   - Dashboard-Style Metrik-Karten mit Icon-Badges
-   - Modernisierte Charts mit besseren Farben
-   - Performance-Tabellen mit Progress-Bars
-   - Trend-Indikatoren für Performance
-   - Responsives Grid-Layout
-
-**Design-Verbesserungen:**
-- Konsistente Emoji-Icons für bessere visuelle Kommunikation
-- Einheitliche Farbgebung (Blau/Lila/Grün für Ressourcentypen)
-- Card-basiertes Layout mit Schatten und Hover-Effekten
-- Bootstrap 5 kompatible Komponenten
-- Verbesserte Typografie und Spacing
-
-**Technische Details:**
-- Bootstrap 5 Modal-API statt jQuery
-- CSS Grid für responsive Layouts
-- Moderne Chart.js Konfiguration
-- Optimierte JavaScript-Validierung
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/resources.html`
-- `v2_adminpanel/templates/add_resources.html`
-- `v2_adminpanel/templates/resource_history.html`
-- `v2_adminpanel/templates/resource_metrics.html`
-
-**Status:** ✅ Abgeschlossen
-
-### 2025-06-09: Zusammenfassung der heutigen Arbeiten
-
-**Durchgeführte Aufgaben:**
-
-1. **Quarantäne-Funktion repariert:**
-   - Bootstrap 5 Modal-API implementiert
-   - data-bs-dismiss statt data-dismiss
-   - jQuery vor Bootstrap laden
-
-2. **Resource Pool UI komplett überarbeitet:**
-   - Alle 4 Templates modernisiert (resources, add_resources, resource_history, resource_metrics)
-   - Konsistentes Design mit Emoji-Icons
-   - Einheitliche Farbgebung (Blau/Lila/Grün)
-   - Bootstrap 5 kompatible Komponenten
-   - Responsive Grid-Layouts
-
-**Aktuelle Projekt-Status:**
-- ✅ Admin Panel voll funktionsfähig
-- ✅ Resource Pool Management mit modernem UI
-- ✅ PostgreSQL mit allen Tabellen
-- ✅ Nginx Reverse Proxy mit SSL
-- ❌ Lizenzserver noch nicht implementiert (nur Platzhalter)
-
-**Nächste Schritte:**
-- Lizenzserver implementieren
-- API-Endpunkte für Lizenzvalidierung
-- Heartbeat-System für Sessions
-- Versionsprüfung implementieren
-1. `v2_adminpanel/templates/base.html` - Navigation entfernt
-2. `v2_adminpanel/app.py` - Resource Report Einrückung korrigiert
-3. `JOURNAL.md` - Alle Änderungen dokumentiert
-
-### Offene Hauptaufgabe:
-- **License Server API** - Noch komplett zu implementieren
-  - `/api/version` - Versionscheck
-  - `/api/validate` - Lizenzvalidierung  
-  - `/api/heartbeat` - Session-Management
-
-### 2025-06-09: Resource Pool Internal Error behoben
-
-**Problem:**
-- Internal Server Error beim Zugriff auf `/resources`
-- NameError: name 'datetime' is not defined in Template
-
-**Ursache:**
-- Fehlende `datetime` und `timedelta` Objekte im Template-Kontext
-- Falsche Array-Indizes in resources.html für activity-Daten
-
-**Lösung:**
-1. **app.py (Zeile 2797-2798):**
-   - `datetime=datetime` und `timedelta=timedelta` zu render_template hinzugefügt
-   
-2. **resources.html (Zeile 484-490):**
-   - Array-Indizes korrigiert:
-     - activity[0] = action
-     - activity[1] = action_by
-     - activity[2] = action_at
-     - activity[3] = resource_type
-     - activity[4] = resource_value
-     - activity[5] = details
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py`
-- `v2_adminpanel/templates/resources.html`
-
-**Status:** ✅ Behoben - Resource Pool funktioniert wieder einwandfrei
-
-### 2025-06-09: Passwort-Änderung und 2FA implementiert
-
-**Ziel:**
-- Benutzer können ihr Passwort ändern
-- Zwei-Faktor-Authentifizierung (2FA) mit TOTP
-- Komplett kostenlose Lösung ohne externe Services
-
-**Implementierte Features:**
-
-1. **Datenbank-Erweiterung:**
-   - Neue `users` Tabelle mit Passwort-Hash und 2FA-Feldern
-   - Unterstützung für TOTP-Secrets und Backup-Codes
-   - Migration von Environment-Variablen zu Datenbank
-
-2. **Passwort-Management:**
-   - Sichere Passwort-Hashes mit bcrypt
-   - Passwort-Änderung mit Verifikation des alten Passworts
-   - Passwort-Stärke-Indikator im Frontend
-
-3. **2FA-Implementation:**
-   - TOTP-basierte 2FA (Google Authenticator, Authy kompatibel)
-   - QR-Code-Generierung für einfaches Setup
-   - 8 Backup-Codes für Notfallzugriff
-   - Backup-Codes als Textdatei downloadbar
-
-4. **Neue Routen:**
-   - `/profile` - Benutzerprofil mit Passwort und 2FA-Verwaltung
-   - `/verify-2fa` - 2FA-Verifizierung beim Login
-   - `/profile/setup-2fa` - 2FA-Einrichtung mit QR-Code
-   - `/profile/enable-2fa` - 2FA-Aktivierung
-   - `/profile/disable-2fa` - 2FA-Deaktivierung
-   - `/profile/change-password` - Passwort ändern
-
-5. **Sicherheits-Features:**
-   - Fallback zu Environment-Variablen für Rückwärtskompatibilität
-   - Session-Management für 2FA-Verifizierung
-   - Fehlgeschlagene 2FA-Versuche werden protokolliert
-   - Verwendete Backup-Codes werden entfernt
-
-**Verwendete Libraries (alle kostenlos):**
-- `bcrypt` - Passwort-Hashing
-- `pyotp` - TOTP-Generierung und Verifizierung
-- `qrcode[pil]` - QR-Code-Generierung
-
-**Migration:**
-- Script `migrate_users.py` erstellt für Migration existierender Benutzer
-- Erhält bestehende Credentials aus Environment-Variablen
-- Erstellt Datenbank-Einträge mit gehashten Passwörtern
-
-**Geänderte Dateien:**
-- `v2_adminpanel/init.sql` - Users-Tabelle hinzugefügt
-- `v2_adminpanel/requirements.txt` - Neue Dependencies
-- `v2_adminpanel/app.py` - Auth-Funktionen und neue Routen
-- `v2_adminpanel/migrate_users.py` - Migrations-Script (neu)
-- `v2_adminpanel/templates/base.html` - Profil-Link hinzugefügt
-- `v2_adminpanel/templates/profile.html` - Profil-Seite (neu)
-- `v2_adminpanel/templates/verify_2fa.html` - 2FA-Verifizierung (neu)
-- `v2_adminpanel/templates/setup_2fa.html` - 2FA-Setup (neu)
-- `v2_adminpanel/templates/backup_codes.html` - Backup-Codes Anzeige (neu)
-
-**Status:** ✅ Vollständig implementiert
-
-### 2025-06-09: Internal Server Error behoben und UI-Design angepasst
-
-### 2025-06-09: Journal-Bereinigung und Projekt-Cleanup
-
-**Durchgeführte Aufgaben:**
-
-1. **Überflüssige SQL-Dateien gelöscht:**
-   - `create_resource_tables.sql` - War nur für Migrations nötig
-   - `migrate_existing_licenses.sql` - Keine alten Installationen vorhanden
-   - `sample_data.sql` - Testdaten nicht mehr benötigt
-   - `test_data_resources.sql` - Testdaten nicht mehr benötigt
-
-2. **Journal aktualisiert:**
-   - Veraltete Todo-Liste korrigiert (viele Features bereits implementiert)
-   - Passwörter aus Zugangsdaten entfernt (Sicherheit)
-   - "Bekannte Probleme" auf aktuellen Stand gebracht
-   - Neuer Abschnitt "Best Practices für Produktiv-Migration" hinzugefügt
-
-3. **Status-Klärungen:**
-   - Alle Daten sind Testdaten (PoC-Phase)
-   - 2FA ist implementiert und funktionsfähig
-   - Resource Pool System ist vollständig implementiert
-   - Port 8443 ist geschlossen (nur über Nginx erreichbar)
-
-**Noch zu erledigen:**
-- Nginx Config anpassen (proxy_pass von https:// auf http://)
-- License Server API implementieren (Hauptaufgabe)
-
-**Problem:**
-- Internal Server Error nach Login wegen fehlender `users` Tabelle
-- UI-Design der neuen 2FA-Seiten passte nicht zum Rest der Anwendung
-
-**Lösung:**
-
-1. **Datenbank-Fix:**
-   - Users-Tabelle wurde nicht automatisch erstellt
-   - Manuell mit SQL-Script nachgeholt
-   - Migration erfolgreich durchgeführt
-   - Beide Admin-User (rac00n, w@rh@mm3r) migriert
-
-2. **UI-Design Überarbeitung:**
-   - Profile-Seite im Dashboard-Stil mit Cards und Hover-Effekten
-   - 2FA-Setup mit nummerierten Schritten und modernem Card-Design
-   - Backup-Codes Seite mit Animation und verbessertem Layout
-   - Konsistente Farbgebung und Icons
-   - Verbesserte Benutzerführung mit visuellen Hinweisen
-
-**Design-Features:**
-- Card-basiertes Layout mit Schatten-Effekten
-- Hover-Animationen für bessere Interaktivität
-- Farbcodierte Sicherheitsstatus-Anzeigen
-- Passwort-Stärke-Indikator mit visueller Rückmeldung
-- Responsive Design für alle Bildschirmgrößen
-- Print-optimiertes Layout für Backup-Codes
-
-**Geänderte Dateien:**
-- `v2_adminpanel/create_users_table.sql` - SQL für Users-Tabelle (temporär)
-
-### 2025-06-09: Journal-Umstrukturierung
-
-**Durchgeführte Änderungen:**
-
-1. **Dokumentation aufgeteilt:**
-   - `JOURNAL.md` - Enthält nur noch chronologische Änderungen (wie ein Tagebuch)
-   - `THE_ROAD_SO_FAR.md` - Neues Dokument mit aktuellem Status und Roadmap
-   
-2. **THE_ROAD_SO_FAR.md erstellt mit:**
-   - Aktueller Status (was läuft bereits)
-   - Nächste Schritte (Priorität Hoch)
-   - Offene Aufgaben (Priorität Mittel)
-   - Nice-to-have Features
-   - Bekannte Probleme
-   - Deployment-Hinweise
-
-3. **JOURNAL.md bereinigt:**
-   - Todo-Listen entfernt (jetzt in THE_ROAD_SO_FAR.md)
-   - Nur noch chronologische Einträge
-   - Fokus auf "Was wurde gemacht" statt "Was muss gemacht werden"
-
-**Vorteile der Aufteilung:**
-- Journal bleibt übersichtlich und wächst linear
-- Status und Todos sind immer aktuell an einem Ort
-- Klare Trennung zwischen Historie und Planung
-- Einfacher für neue Entwickler einzusteigen
-
-### 2025-06-09: Nginx Config angepasst
-
-**Änderung:**
-- proxy_pass für License Server von `https://license-server:8443` auf `http://license-server:8443` geändert
-- `proxy_ssl_verify off` entfernt (nicht mehr nötig bei HTTP)
-- WebSocket-Support hinzugefügt (für zukünftige Features)
-
-**Grund:**
-- License Server läuft intern auf HTTP (wie Admin Panel)
-- SSL-Termination erfolgt nur am Nginx
-- Vereinfachte Konfiguration ohne doppelte SSL-Verschlüsselung
-
-**Hinweis:** 
-Docker-Container müssen neu gestartet werden, damit die Änderung wirksam wird:
-```bash
-docker-compose down
-docker-compose up -d
-```
-- `v2_adminpanel/templates/profile.html` - Komplett überarbeitet
-- `v2_adminpanel/templates/setup_2fa.html` - Neues Step-by-Step Design
-- `v2_adminpanel/templates/backup_codes.html` - Modernisiertes Layout
-
-**Status:** ✅ Abgeschlossen - Login funktioniert, UI im konsistenten Design
-
-### 2025-06-09: Lizenzschlüssel-Format geändert
-
-**Änderung:**
-- Altes Format: `AF-YYYYMMFT-XXXX-YYYY-ZZZZ` (z.B. AF-202506F-V55Y-9DWE-GL5G)
-- Neues Format: `AF-F-YYYYMM-XXXX-YYYY-ZZZZ` (z.B. AF-F-202506-V55Y-9DWE-GL5G)
-
-**Vorteile:**
-- Klarere Struktur mit separatem Typ-Indikator
-- Einfacher zu lesen und zu verstehen
-- Typ (F/T) sofort im zweiten Block erkennbar
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py`:
-  - `generate_license_key()` - Generiert Keys im neuen Format
-  - `validate_license_key()` - Validiert Keys mit neuem Regex-Pattern
-- `v2_adminpanel/templates/index.html`:
-  - Placeholder und Pattern für Input-Feld angepasst
-  - JavaScript charAt() Position für Typ-Prüfung korrigiert
-- `v2_adminpanel/templates/batch_form.html`:
-  - Vorschau-Format für Batch-Generierung angepasst
-
-**Hinweis:** Alte Keys im bisherigen Format bleiben ungültig. Bei Bedarf könnte eine Migration oder Dual-Support implementiert werden.
-
-**Status:** ✅ Implementiert
-
-### 2025-06-09: Datenbank-Migration der Lizenzschlüssel
-
-**Durchgeführt:**
-- Alle bestehenden Lizenzschlüssel in der Datenbank auf das neue Format migriert
-- 18 Lizenzschlüssel erfolgreich konvertiert (16 Full, 2 Test)
-
-**Migration:**
-- Von: `AF-YYYYMMFT-XXXX-YYYY-ZZZZ`
-- Nach: `AF-F-YYYYMM-XXXX-YYYY-ZZZZ`
-
-**Beispiele:**
-- Alt: `AF-202506F-V55Y-9DWE-GL5G`
-- Neu: `AF-F-202506-V55Y-9DWE-GL5G`
-
-**Geänderte Dateien:**
-- `v2_adminpanel/migrate_license_keys.sql` - Migrations-Script (temporär)
-- `v2_adminpanel/fix_license_keys.sql` - Korrektur-Script (temporär)
-
-**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert
-
-### 2025-06-09: Kombinierte Kunden-Lizenz-Ansicht implementiert
-
-**Problem:**
-- Umständliche Navigation zwischen Kunden- und Lizenzseiten
-- Viel Hin-und-Her-Springen bei der Verwaltung
-- Kontext-Verlust beim Wechseln zwischen Ansichten
-
-**Lösung:**
-Master-Detail View mit 2-Spalten Layout implementiert
-
-**Phase 1-3 abgeschlossen:**
-1. **Backend-Implementierung:**
-   - Neue Route `/customers-licenses` für kombinierte Ansicht
-   - API-Endpoints für AJAX: `/api/customer/<id>/licenses`, `/api/customer/<id>/quick-stats`
-   - API-Endpoint `/api/license/<id>/quick-edit` für Inline-Bearbeitung
-   - Optimierte SQL-Queries mit JOIN für Performance
-
-2. **Template-Erstellung:**
-   - Neues Template `customers_licenses.html` mit Master-Detail Layout
-   - Links: Kundenliste (30%) mit Suchfeld
-   - Rechts: Lizenzen des ausgewählten Kunden (70%)
-   - Responsive Design (Mobile: untereinander)
-   - JavaScript für dynamisches Laden ohne Seitenreload
-   - Keyboard-Navigation (↑↓ für Kundenwechsel)
-
-3. **Integration:**
-   - Dashboard: Neuer Button "Kunden & Lizenzen"
-   - Customers-Seite: Link zur kombinierten Ansicht
-   - Licenses-Seite: Link zur kombinierten Ansicht
-   - Lizenz-Erstellung: Unterstützung für vorausgewählten Kunden
-   - API /api/customers erweitert für Einzelabruf per ID
-
-**Features:**
-- Live-Suche in Kundenliste
-- Quick-Actions: Copy License Key, Toggle Status
-- Modal für neue Lizenz direkt aus Kundenansicht
-- URL-Update ohne Reload für Bookmarking
-- Loading-States während AJAX-Calls
-- Visuelles Feedback (aktiver Kunde hervorgehoben)
-
-**Noch ausstehend:**
-- Phase 4: Inline-Edit für Lizenzdetails
-- Phase 5: Erweiterte Error-Handling und Polish
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py` - Neue Routen und API-Endpoints
-- `v2_adminpanel/templates/customers_licenses.html` - Neues Template
-- `v2_adminpanel/templates/dashboard.html` - Neuer Button
-- `v2_adminpanel/templates/customers.html` - Link zur kombinierten Ansicht
-- `v2_adminpanel/templates/licenses.html` - Link zur kombinierten Ansicht
-- `v2_adminpanel/templates/index.html` - Unterstützung für preselected_customer_id
-
-**Status:** ✅ Grundfunktionalität implementiert und funktionsfähig
-
-### 2025-06-09: Kombinierte Ansicht - Fertigstellung und TODOs aktualisiert
-
-**Abgeschlossen:**
-- Phase 1-3 der kombinierten Kunden-Lizenz-Ansicht vollständig implementiert
-- Master-Detail Layout funktioniert einwandfrei
-- AJAX-basiertes Laden ohne Seitenreload
-- Keyboard-Navigation mit Pfeiltasten
-- Quick-Actions für Copy und Toggle Status
-- Integration in alle relevanten Seiten
-
-**THE_ROAD_SO_FAR.md aktualisiert:**
-- Kombinierte Ansicht als "Erledigt" markiert
-- Von "In Arbeit" zu "Abgeschlossen" verschoben
-- Status dokumentiert
-
-**Verbesserung gegenüber vorher:**
-- Kein Hin-und-Her-Springen mehr zwischen Seiten
-- Kontext bleibt erhalten beim Arbeiten mit Kunden
-- Schnellere Navigation und bessere Übersicht
-- Deutlich verbesserte User Experience
-
-**Optional für später (Phase 4-5):**
-- Inline-Edit für weitere Felder
-- Erweiterte Quick-Actions
-- Session-basierte Filter-Persistenz
-
-Die Hauptproblematik der umständlichen Navigation ist damit gelöst!
-
-### 2025-06-09: Test-Flag für Lizenzen implementiert
-
-**Ziel:**
-- Klare Trennung zwischen Testdaten und echten Produktivdaten
-- Testdaten sollen von der Software ignoriert werden können
-- Bessere Übersicht im Admin Panel
-
-**Durchgeführte Änderungen:**
-
-1. **Datenbank-Schema (init.sql):**
-   - Neue Spalte `is_test BOOLEAN DEFAULT FALSE` zur `licenses` Tabelle hinzugefügt
-   - Migration für bestehende Daten: Alle werden als `is_test = TRUE` markiert
-   - Index `idx_licenses_is_test` für bessere Performance
-
-2. **Backend (app.py):**
-   - Dashboard-Queries filtern Testdaten mit `WHERE is_test = FALSE` aus
-   - Lizenz-Erstellung: Neues Checkbox-Feld für Test-Markierung
-   - Lizenz-Bearbeitung: Test-Status kann geändert werden
-   - Export: Optional mit/ohne Testdaten (`?include_test=true`)
-   - Bulk-Operationen: Nur auf Live-Daten anwendbar
-   - Neue Filter in Lizenzliste: "🧪 Testdaten" und "🚀 Live-Daten"
-
-3. **Frontend Templates:**
-   - **index.html**: Checkbox "Als Testdaten markieren" bei Lizenzerstellung
-   - **edit_license.html**: Checkbox zum Ändern des Test-Status
-   - **licenses.html**: Badge 🧪 für Testdaten, neue Filteroptionen
-   - **dashboard.html**: Info-Box zeigt Anzahl der Testdaten
-   - **batch_form.html**: Option für Batch-Test-Lizenzen
-
-4. **Audit-Log Integration:**
-   - `is_test` Feld wird bei CREATE/UPDATE geloggt
-   - Nachvollziehbarkeit von Test/Live-Status-Änderungen
-
-**Technische Details:**
-- Testdaten werden in allen Statistiken ausgefiltert
-- License Server API wird Lizenzen mit `is_test = TRUE` ignorieren
-- Resource Pool bleibt unverändert (kann Test- und Live-Ressourcen verwalten)
-
-**Migration der bestehenden Daten:**
-```sql
-UPDATE licenses SET is_test = TRUE;  -- Alle aktuellen Daten sind Testdaten
-```
-
-**Status:** ✅ Implementiert
-
-### 2025-06-09: Test-Flag für Kunden und Resource Pools erweitert
-
-**Ziel:**
-- Konsistentes Test-Daten-Management über alle Entitäten
-- Kunden und Resource Pools können ebenfalls als Testdaten markiert werden
-- Automatische Verknüpfung: Test-Kunde → Test-Lizenzen → Test-Ressourcen
-
-**Durchgeführte Änderungen:**
-
-1. **Datenbank-Schema erweitert:**
-   - `customers.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
-   - `resource_pools.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
-   - Indizes für bessere Performance erstellt
-   - Migrations in init.sql integriert
-
-2. **Backend (app.py) - Erweiterte Logik:**
-   - Dashboard: Separate Zähler für Test-Kunden und Test-Ressourcen
-   - Kunde-Erstellung: Erbt Test-Status von Lizenz
-   - Test-Kunde erzwingt Test-Lizenzen
-   - Resource-Zuweisung: Test-Lizenzen bekommen nur Test-Ressourcen
-   - Customer-Management mit is_test Filter
-
-3. **Frontend Updates:**
-   - **customers.html**: 🧪 Badge für Test-Kunden
-   - **edit_customer.html**: Checkbox für Test-Status
-   - **dashboard.html**: Erweiterte Test-Statistik (Lizenzen, Kunden, Ressourcen)
-
-4. **Geschäftslogik:**
-   - Wenn neuer Kunde bei Test-Lizenz erstellt wird → automatisch Test-Kunde
-   - Wenn Test-Kunde gewählt wird → Lizenz automatisch als Test markiert
-   - Resource Pool Allocation prüft Test-Status für korrekte Zuweisung
-
-**Migration der bestehenden Daten:**
-```sql
-UPDATE customers SET is_test = TRUE;       -- 5 Kunden
-UPDATE resource_pools SET is_test = TRUE;  -- 20 Ressourcen
-```
-
-**Technische Details:**
-- Konsistente Test/Live-Trennung über alle Ebenen
-- Dashboard-Statistiken zeigen nur Live-Daten
-- Test-Ressourcen werden nur Test-Lizenzen zugewiesen
-- Alle bestehenden Daten sind jetzt als Test markiert
-
-**Status:** ✅ Vollständig implementiert
-
-### 2025-06-09 (17:20 - 18:13): Kunden-Lizenz-Verwaltung konsolidiert
-
-**Problem:**
-- Kombinierte Ansicht `/customers-licenses` hatte Formatierungs- und Funktionsprobleme
-- Kunden wurden nicht angezeigt
-- Bootstrap Icons fehlten
-- JavaScript-Fehler beim Modal
-- Inkonsistentes Design im Vergleich zu anderen Seiten
-- Testkunden-Filter wurde beim Navigieren nicht beibehalten
-
-**Durchgeführte Änderungen:**
-
-1. **Frontend-Fixes (base.html):**
-   - Bootstrap Icons CSS hinzugefügt: `bootstrap-icons@1.11.3/font/bootstrap-icons.min.css`
-   - Bootstrap JavaScript Bundle bereits vorhanden, Reihenfolge optimiert
-
-2. **customers_licenses.html komplett überarbeitet:**
-   - Container-Klasse von `container-fluid` auf `container py-5` geändert
-   - Emojis und Button-Styling vereinheitlicht (👥 Kunden & Lizenzen)
-   - Export-Dropdown wie in anderen Ansichten implementiert
-   - Card-Styling mit Schatten für einheitliches Design
-   - Checkbox "Testkunden anzeigen" mit Status-Beibehaltung
-   - JavaScript-Funktionen korrigiert:
-     - copyToClipboard mit event.currentTarget
-     - showNewLicenseModal mit Bootstrap Modal
-     - Header-Update beim AJAX-Kundenwechsel
-   - URL-Parameter `show_test` wird überall beibehalten
-
-3. **Backend-Anpassungen (app.py):**
-   - customers_licenses Route: Optional Testkunden anzeigen mit `show_test` Parameter
-   - Redirects von `/customers` und `/licenses` auf `/customers-licenses` implementiert
-   - Alte Route-Funktionen entfernt (kein toter Code mehr)
-   - edit_license und edit_customer: Redirects behalten show_test Parameter bei
-   - Dashboard-Links zeigen jetzt auf kombinierte Ansicht
-
-4. **Navigation optimiert:**
-   - Dashboard: Klick auf Kunden/Lizenzen-Statistik führt zur kombinierten Ansicht
-   - Alle Edit-Links behalten den show_test Parameter bei
-   - Konsistente User Experience beim Navigieren
-
-**Technische Details:**
-- AJAX-Loading für dynamisches Laden der Lizenzen
-- Keyboard-Navigation (↑↓) für Kundenliste
-- Responsive Design mit Bootstrap Grid
-- Modal-Dialoge für Bestätigungen
-- Live-Suche in der Kundenliste
-
-**Resultat:**
-- ✅ Einheitliches Design mit anderen Admin-Panel-Seiten
-- ✅ Alle Funktionen arbeiten korrekt
-- ✅ Testkunden-Filter bleibt erhalten
-- ✅ Keine redundanten Views mehr
-- ✅ Zentrale Verwaltung für Kunden und Lizenzen
-
-**Status:** ✅ Vollständig implementiert
-
-### 2025-06-09: Test-Daten Checkbox Persistenz implementiert
-
-**Problem:**
-- Die "Testkunden anzeigen" Checkbox in `/customers-licenses` verlor ihren Status beim Navigieren zwischen Seiten
-- Wenn Benutzer zu anderen Seiten (Resources, Audit Log, etc.) wechselten und zurückkehrten, war die Checkbox wieder deaktiviert
-- Benutzer mussten die Checkbox jedes Mal neu aktivieren, was umständlich war
-
-**Lösung:**
-- Globale JavaScript-Funktion `preserveShowTestParameter()` in base.html implementiert
-- Die Funktion prüft beim Laden jeder Seite, ob `show_test=true` in der URL ist
-- Wenn ja, wird dieser Parameter automatisch an alle internen Links angehängt
-- Backend-Route `/create` wurde angepasst, um den Parameter bei Redirects beizubehalten
-
-**Technische Details:**
-1. **base.html** - JavaScript-Funktion hinzugefügt:
-   - Läuft beim `DOMContentLoaded` Event
-   - Findet alle Links die mit "/" beginnen
-   - Fügt `show_test=true` Parameter hinzu wenn nicht bereits vorhanden
-   - Überspringt Fragment-Links (#) und Links die bereits den Parameter haben
-
-2. **app.py** - Route-Anpassung:
-   - `/create` Route behält jetzt `show_test` Parameter bei Redirects bei
-   - Andere Routen (edit_license, edit_customer) behalten Parameter bereits bei
-
-**Vorteile:**
-- ✅ Konsistente User Experience beim Navigieren
-- ✅ Keine manuelle Anpassung aller Links nötig
-- ✅ Funktioniert automatisch für alle zukünftigen Links
-- ✅ Minimaler Code-Overhead
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/base.html`
-- `v2_adminpanel/app.py`
-
-**Status:** ✅ Vollständig implementiert
-
-### 2025-06-09: Bearbeiten-Button Fehler behoben
-
-**Problem:**
-- Der "Bearbeiten" Button neben dem Kundennamen in der `/customers-licenses` Ansicht verursachte einen Internal Server Error
-- Die URL-Konstruktion war fehlerhaft wenn kein `show_test` Parameter vorhanden war
-- Die edit_customer.html Template hatte falsche Array-Indizes und veraltete Links
-
-**Ursache:**
-1. Die href-Attribute wurden falsch konstruiert:
-   - Alt: `/customer/edit/ID{% if show_test %}?ref=customers-licenses&show_test=true{% endif %}`
-   - Problem: Ohne show_test fehlte das `?ref=customers-licenses` komplett
-
-2. Die SQL-Abfrage in edit_customer() holte nur 4 Felder, aber das Template erwartete 5:
-   - Query: `SELECT id, name, email, is_test`
-   - Template erwartete: `customer[3]` = created_at und `customer[4]` = is_test
-
-3. Veraltete Links zu `/customers` statt `/customers-licenses`
-
-**Lösung:**
-1. URL-Konstruktion korrigiert in beiden Fällen:
-   - Neu: `/customer/edit/ID?ref=customers-licenses{% if show_test %}&show_test=true{% endif %}`
-   
-2. SQL-Query erweitert um created_at:
-   - Neu: `SELECT id, name, email, created_at, is_test`
-   
-3. Template-Indizes korrigiert:
-   - is_test Checkbox nutzt jetzt `customer[4]`
-   
-4. Navigation-Links aktualisiert:
-   - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/customers_licenses.html` (Zeilen 103 und 295)
-- `v2_adminpanel/app.py` (edit_customer Route)
-- `v2_adminpanel/templates/edit_customer.html`
-
-**Status:** ✅ Behoben
-
-### 2025-06-09: Unnötigen Lizenz-Erstellungs-Popup entfernt
-
-**Änderung:**
-- Der Bestätigungs-Popup "Möchten Sie eine neue Lizenz für KUNDENNAME erstellen?" wurde entfernt
-- Klick auf "Neue Lizenz" Button führt jetzt direkt zur Lizenzerstellung
-
-**Technische Details:**
-- Modal-HTML komplett entfernt
-- `showNewLicenseModal()` Funktion vereinfacht - navigiert jetzt direkt zu `/create?customer_id=X`
-- URL-Parameter (wie `show_test`) werden dabei beibehalten
-
-**Vorteile:**
-- ✅ Ein Klick weniger für Benutzer
-- ✅ Schnellerer Workflow
-- ✅ Weniger Code zu warten
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/customers_licenses.html`
-
-**Status:** ✅ Implementiert
-
-### 2025-06-09: Testkunden-Checkbox bleibt jetzt bei Lizenz/Kunden-Bearbeitung erhalten
-
-**Problem:**
-- Bei "Lizenz bearbeiten" ging der "Testkunden anzeigen" Haken verloren beim Zurückkehren
-- Die Navigation-Links in edit_license.html zeigten auf `/licenses` statt `/customers-licenses`
-- Der show_test Parameter wurde nur über den unsicheren Referrer übertragen
-
-**Lösung:**
-1. **Navigation-Links korrigiert**:
-   - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter
-   - Betrifft: "Zurück zur Übersicht" und "Abbrechen" Buttons
-
-2. **Hidden Form Field hinzugefügt**:
-   - Sowohl in edit_license.html als auch edit_customer.html
-   - Überträgt den show_test Parameter sicher beim POST
-
-3. **Route-Logik verbessert**:
-   - Parameter wird aus Form-Daten ODER GET-Parametern gelesen
-   - Nicht mehr auf unsicheren Referrer angewiesen
-   - Funktioniert sowohl bei Speichern als auch Abbrechen
-
-**Technische Details:**
-- Templates prüfen `request.args.get('show_test')` für Navigation
-- Hidden Input: `<input type="hidden" name="show_test" value="true">`
-- Routes: `show_test = request.form.get('show_test') or request.args.get('show_test')`
-
-**Geänderte Dateien:**
-- `v2_adminpanel/templates/edit_license.html`
-- `v2_adminpanel/templates/edit_customer.html`
-- `v2_adminpanel/app.py` (edit_license und edit_customer Routen)
-
-**Status:** ✅ Vollständig implementiert
-
-### 2025-06-09 22:02: Konsistente Sortierung bei Status-Toggle
-
-**Problem:**
-- Beim Klicken auf den An/Aus-Knopf (Status-Toggle) in der Kunden & Lizenzen Ansicht änderte sich die Reihenfolge der Lizenzen
-- Dies war verwirrend für Benutzer, da die Position der gerade bearbeiteten Lizenz springen konnte
-
-**Ursache:**
-- Die Sortierung `ORDER BY l.created_at DESC` war nicht stabil genug
-- Bei gleichem Erstellungszeitpunkt konnte die Datenbank die Reihenfolge inkonsistent zurückgeben
-
-**Lösung:**
-- Sekundäres Sortierkriterium hinzugefügt: `ORDER BY l.created_at DESC, l.id DESC`
-- Dies stellt sicher, dass bei gleichem Erstellungsdatum nach ID sortiert wird
-- Die Reihenfolge bleibt jetzt konsistent, auch nach Status-Änderungen
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py`:
-  - Zeile 2278: `/customers-licenses` Route
-  - Zeile 2319: `/api/customer/<int:customer_id>/licenses` API-Route
-
-### 2025-06-10 00:01: Verbesserte Integration zwischen Kunden & Lizenzen und Resource Pool
-
-**Problem:**
-- Umständliche Navigation zwischen Kunden & Lizenzen und Resource Pool Bereichen
-- Keine direkte Verbindung zwischen beiden Ansichten
-- Benutzer mussten ständig zwischen verschiedenen Seiten hin- und herspringen
-
-**Implementierte Lösung - 5 Phasen:**
-
-1. **Phase 1: Ressourcen-Details in Kunden & Lizenzen Ansicht**
-   - API `/api/customer/{id}/licenses` erweitert um konkrete Ressourcen-Informationen
-   - Neue API `/api/license/{id}/resources` für detaillierte Ressourcen einer Lizenz
-   - Anzeige der zugewiesenen Ressourcen mit Info-Buttons und Modal-Dialogen
-   - Klickbare Links zu Ressourcen-Details im Resource Pool
-
-2. **Phase 2: Quick-Actions für Ressourcenverwaltung**
-   - "Ressourcen verwalten" Button (Zahnrad-Icon) bei jeder Lizenz
-   - Modal mit Übersicht aller zugewiesenen Ressourcen
-   - Vorbereitung für Quarantäne-Funktionen und Ressourcen-Austausch
-
-3. **Phase 3: Ressourcen-Preview bei Lizenzerstellung**
-   - Live-Anzeige verfügbarer Ressourcen beim Ändern der Anzahl
-   - Erweiterte Verfügbarkeitsanzeige mit Badges (OK/Niedrig/Kritisch)
-   - Warnungen bei niedrigem Bestand mit visuellen Hinweisen
-   - Fortschrittsbalken zur Visualisierung der Verfügbarkeit
-
-4. **Phase 4: Dashboard-Integration**
-   - Resource Pool Widget mit erweiterten Links
-   - Kritische Warnungen bei < 50 Ressourcen mit "Auffüllen" Button
-   - Direkte Navigation zu gefilterten Ansichten (nach Typ/Status)
-   - Verbesserte visuelle Darstellung mit Tooltips
-
-5. **Phase 5: Bidirektionale Navigation**
-   - Von Resource Pool: Links zu Kunden/Lizenzen bei zugewiesenen Ressourcen
-   - "Zurück zu Kunden" Button wenn von Kunden & Lizenzen kommend
-   - Navigation-Links im Dashboard für schnellen Zugriff
-   - SQL-Query erweitert um customer_id für direkte Verlinkung
-
-**Technische Details:**
-- JavaScript-Funktionen für Modal-Dialoge und Ressourcen-Details
-- Erweiterte SQL-Queries mit JOINs für Ressourcen-Informationen
-- Bootstrap 5 Tooltips und Modals für bessere UX
-- Globale Variable `currentLicenses` für Caching der Lizenzdaten
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py` - Neue APIs und erweiterte Queries
-- `v2_adminpanel/templates/customers_licenses.html` - Ressourcen-Details und Modals
-- `v2_adminpanel/templates/index.html` - Erweiterte Verfügbarkeitsanzeige
-- `v2_adminpanel/templates/dashboard.html` - Verbesserte Resource Pool Integration
-- `v2_adminpanel/templates/resources.html` - Bidirektionale Navigation
-
-**Status:** ✅ Alle 5 Phasen erfolgreich implementiert
-
-### 2025-06-10 00:15: IP-Adressen-Erfassung hinter Reverse Proxy korrigiert
-
-**Problem:**
-- Flask-App erfasste nur die Docker-interne IP-Adresse von Nginx (172.19.0.5)
-- Echte Client-IPs wurden nicht in Audit-Logs und Login-Attempts gespeichert
-- Nginx setzte die Header korrekt, aber Flask las sie nicht aus
-
-**Ursache:**
-- Flask verwendet standardmäßig nur `request.remote_addr`
-- Dies gibt bei einem Reverse Proxy nur die Proxy-IP zurück
-- Die Header `X-Real-IP` und `X-Forwarded-For` wurden ignoriert
-
-**Lösung:**
-1. **ProxyFix Middleware** hinzugefügt für korrekte Header-Verarbeitung
-2. **get_client_ip() Funktion** angepasst:
-   - Prüft zuerst `X-Real-IP` Header
-   - Dann `X-Forwarded-For` Header (nimmt erste IP bei mehreren)
-   - Fallback auf `request.remote_addr`
-3. **Debug-Logging** für IP-Erfassung hinzugefügt
-4. **Alle `request.remote_addr` Aufrufe** durch `get_client_ip()` ersetzt
-
-**Technische Details:**
-```python
-# ProxyFix für korrekte IP-Adressen
-app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
-
-# Verbesserte IP-Erfassung
-def get_client_ip():
-    if request.headers.get('X-Real-IP'):
-        return request.headers.get('X-Real-IP')
-    elif request.headers.get('X-Forwarded-For'):
-        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
-    else:
-        return request.remote_addr
-```
-
-**Geänderte Dateien:**
-- `v2_adminpanel/app.py` - ProxyFix und verbesserte IP-Erfassung
-
-**Status:** ✅ Implementiert - Neue Aktionen erfassen jetzt echte Client-IPs
-
-### 2025-06-10 00:30: Docker ENV Legacy-Format Warnungen behoben
-
-**Problem:**
-- Docker Build zeigte Warnungen: "LegacyKeyValueFormat: ENV key=value should be used"
-- Veraltetes Format `ENV KEY VALUE` wurde in Dockerfiles verwendet
-
-**Lösung:**
-- Alle ENV-Anweisungen auf neues Format `ENV KEY=VALUE` umgestellt
-- Betraf hauptsächlich v2_postgres/Dockerfile mit 3 ENV-Zeilen
-
-**Geänderte Dateien:**
-- `v2_postgres/Dockerfile` - ENV-Format modernisiert
-
-**Beispiel der Änderung:**
-```dockerfile
-# Alt (Legacy):
-ENV LANG de_DE.UTF-8
-ENV LANGUAGE de_DE:de
-
-# Neu (Modern):
-ENV LANG=de_DE.UTF-8
-ENV LANGUAGE=de_DE:de
-```
-
-**Status:** ✅ Alle Dockerfiles verwenden jetzt das moderne ENV-Format
-
+# v2-Docker Projekt Journal
+
+## Letzte Änderungen (06.01.2025)
+
+### Gerätelimit-Feature implementiert
+- **Datenbank-Schema erweitert**:
+  - Neue Spalte `device_limit` in `licenses` Tabelle (Standard: 3, Range: 1-10)
+  - Neue Tabelle `device_registrations` für Hardware-ID Tracking
+  - Indizes für Performance-Optimierung hinzugefügt
+  
+- **UI-Anpassungen**:
+  - Einzellizenz-Formular: Dropdown für Gerätelimit (1-10 Geräte)
+  - Batch-Formular: Gerätelimit pro Lizenz auswählbar
+  - Lizenz-Bearbeitung: Gerätelimit änderbar
+  - Lizenz-Anzeige: Zeigt aktive Geräte (z.B. "💻 2/3")
+  
+- **Backend-Änderungen**:
+  - Lizenz-Erstellung speichert device_limit
+  - Batch-Erstellung berücksichtigt device_limit
+  - Lizenz-Update kann device_limit ändern
+  - API-Endpoints liefern Geräteinformationen
+  
+- **Migration**:
+  - Skript `migrate_device_limit.sql` erstellt
+  - Setzt device_limit = 3 für alle bestehenden Lizenzen
+
+### Vollständig implementiert:
+✅ Device Management UI (Geräte pro Lizenz anzeigen/verwalten)
+✅ Device Validation Logic (Prüfung bei Geräte-Registrierung)
+✅ API-Endpoints für Geräte-Registrierung/Deregistrierung
+
+### API-Endpoints:
+- `GET /api/license/<id>/devices` - Listet alle Geräte einer Lizenz
+- `POST /api/license/<id>/register-device` - Registriert ein neues Gerät
+- `POST /api/license/<id>/deactivate-device/<device_id>` - Deaktiviert ein Gerät
+
+### Features:
+- Geräte-Registrierung mit Hardware-ID Validierung
+- Automatische Prüfung des Gerätelimits
+- Reaktivierung deaktivierter Geräte möglich
+- Geräte-Verwaltung UI mit Modal-Dialog
+- Anzeige von Gerätename, OS, IP, Registrierungsdatum
+- Admin kann Geräte manuell deaktivieren
+
+---
+
+## Projektübersicht
+Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker-basierter Architektur.
+
+### Technische Anforderungen
+- **Lokaler Betrieb**: Docker mit 4GB RAM und 40GB Speicher
+- **Internet-Zugriff**: 
+  - Admin Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
+  - API Server: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com
+- **Datenbank**: PostgreSQL mit 2 Admin-Usern
+- **Ziel**: PoC für spätere VPS-Migration
+
+---
+
+## Best Practices für Produktiv-Migration
+
+### Passwort-Management
+Für die Migration auf Hetzner/VPS müssen die Credentials sicher verwaltet werden:
+
+1. **Environment Variables erstellen:**
+   ```bash
+   # .env.example (ins Git Repository)
+   POSTGRES_USER=changeme
+   POSTGRES_PASSWORD=changeme
+   POSTGRES_DB=changeme
+   SECRET_KEY=generate-a-secure-key
+   ADMIN_USER_1=changeme
+   ADMIN_PASS_1=changeme
+   ADMIN_USER_2=changeme
+   ADMIN_PASS_2=changeme
+   
+   # .env (NICHT ins Git, auf Server erstellen)
+   POSTGRES_USER=produktiv_user
+   POSTGRES_PASSWORD=sicheres_passwort_min_20_zeichen
+   POSTGRES_DB=v2docker_prod
+   SECRET_KEY=generierter_64_zeichen_key
+   # etc.
+   ```
+
+2. **Sichere Passwörter generieren:**
+   - Mindestens 20 Zeichen
+   - Mix aus Groß-/Kleinbuchstaben, Zahlen, Sonderzeichen
+   - Verschiedene Passwörter für Dev/Staging/Prod
+   - Password-Generator verwenden (z.B. `openssl rand -base64 32`)
+
+3. **Erweiterte Sicherheit (Optional):**
+   - HashiCorp Vault für zentrale Secret-Verwaltung
+   - Docker Secrets (für Docker Swarm)
+   - Cloud-Lösungen: AWS Secrets Manager, Azure Key Vault
+
+4. **Wichtige Checkliste:**
+   - [ ] `.env` in `.gitignore` aufnehmen
+   - [ ] Neue Credentials für Produktion generieren
+   - [ ] Backup der Credentials an sicherem Ort
+   - [ ] Regelmäßige Passwort-Rotation planen
+   - [ ] Keine Default-Passwörter verwenden
+
+---
+
+## Änderungsprotokoll
+
+### 2025-06-06 - Journal erstellt
+- Initialer Projektstand dokumentiert
+- Aufgabenliste priorisiert
+- Technische Anforderungen festgehalten
+
+### 2025-06-06 - UTF-8 Support implementiert
+- Flask App Konfiguration für UTF-8 hinzugefügt (JSON_AS_ASCII=False)
+- PostgreSQL Verbindung mit UTF-8 client_encoding
+- HTML Forms mit accept-charset="UTF-8"
+- Dockerfile mit deutschen Locale-Einstellungen (de_DE.UTF-8)
+- PostgreSQL Container mit UTF-8 Initialisierung
+- init.sql mit SET client_encoding = 'UTF8'
+
+**Geänderte Dateien:**
+- v2_adminpanel/app.py
+- v2_adminpanel/templates/index.html
+- v2_adminpanel/init.sql
+- v2_adminpanel/Dockerfile
+- v2/docker-compose.yaml
+
+**Nächster Test:**
+- Container neu bauen und starten
+- Kundennamen mit Umlauten testen (z.B. "Müller GmbH", "Björn Schäfer")
+- Email mit Umlauten testen
+
+### 2025-06-06 - Lizenzübersicht implementiert
+- Neue Route `/licenses` für Lizenzübersicht
+- SQL-Query mit JOIN zwischen licenses und customers
+- Status-Berechnung (aktiv, läuft bald ab, abgelaufen)
+- Farbcodierung für verschiedene Status
+- Navigation zwischen Lizenz erstellen und Übersicht
+
+**Neue Features:**
+- Anzeige aller Lizenzen mit Kundeninformationen
+- Status-Anzeige basierend auf Ablaufdatum
+- Unterscheidung zwischen Voll- und Testversion
+- Responsive Tabelle mit Bootstrap
+- Link von Dashboard zur Übersicht und zurück
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/app.py (neue Route hinzugefügt)
+- v2_adminpanel/templates/licenses.html (neu erstellt)
+- v2_adminpanel/templates/index.html (Navigation ergänzt)
+
+**Nächster Test:**
+- Container neu starten
+- Mehrere Lizenzen mit verschiedenen Ablaufdaten erstellen
+- Lizenzübersicht unter /licenses aufrufen
+
+### 2025-06-06 - Lizenz bearbeiten/löschen implementiert
+- Neue Routen für Bearbeiten und Löschen von Lizenzen
+- Bearbeitungsformular mit vorausgefüllten Werten
+- Aktiv/Inaktiv-Status kann geändert werden
+- Lösch-Bestätigung per JavaScript confirm()
+- Kunde kann nicht geändert werden (nur Lizenzdetails)
+
+**Neue Features:**
+- `/license/edit/<id>` - Bearbeitungsformular
+- `/license/delete/<id>` - Lizenz löschen (POST)
+- Aktionen-Spalte in der Lizenzübersicht
+- Buttons für Bearbeiten und Löschen
+- Checkbox für Aktiv-Status
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/app.py (edit_license und delete_license Routen)
+- v2_adminpanel/templates/licenses.html (Aktionen-Spalte hinzugefügt)
+- v2_adminpanel/templates/edit_license.html (neu erstellt)
+
+**Sicherheit:**
+- Login-Required für alle Aktionen
+- POST-only für Löschvorgänge
+- Bestätigungsdialog vor dem Löschen
+
+### 2025-06-06 - Kundenverwaltung implementiert
+- Komplette CRUD-Funktionalität für Kunden
+- Übersicht zeigt Anzahl aktiver/gesamter Lizenzen pro Kunde
+- Kunden können nur gelöscht werden, wenn sie keine Lizenzen haben
+- Bearbeitungsseite zeigt alle Lizenzen des Kunden
+
+**Neue Features:**
+- `/customers` - Kundenübersicht mit Statistiken
+- `/customer/edit/<id>` - Kunde bearbeiten (Name, E-Mail)
+- `/customer/delete/<id>` - Kunde löschen (nur ohne Lizenzen)
+- Navigation zwischen allen drei Hauptbereichen
+- Anzeige der Kundenlizenzen beim Bearbeiten
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/app.py (customers, edit_customer, delete_customer Routen)
+- v2_adminpanel/templates/customers.html (neu erstellt)
+- v2_adminpanel/templates/edit_customer.html (neu erstellt)
+- v2_adminpanel/templates/index.html (Navigation erweitert)
+- v2_adminpanel/templates/licenses.html (Navigation erweitert)
+
+**Besonderheiten:**
+- Lösch-Button ist deaktiviert, wenn Kunde Lizenzen hat
+- Aktive Lizenzen werden separat gezählt (nicht abgelaufen + aktiv)
+- UTF-8 Support für Kundennamen mit Umlauten
+
+### 2025-06-06 - Dashboard mit Statistiken implementiert
+- Übersichtliches Dashboard als neue Startseite
+- Statistik-Karten mit wichtigen Kennzahlen
+- Listen für bald ablaufende und zuletzt erstellte Lizenzen
+- Routing angepasst: Dashboard (/) und Lizenz erstellen (/create)
+
+**Neue Features:**
+- Statistik-Karten: Kunden, Lizenzen gesamt, Aktive, Ablaufende
+- Aufteilung nach Lizenztypen (Vollversion/Testversion)
+- Aufteilung nach Status (Aktiv/Abgelaufen)
+- Top 10 bald ablaufende Lizenzen mit Restlaufzeit
+- Letzte 5 erstellte Lizenzen mit Status
+- Hover-Effekt auf Statistik-Karten
+- Einheitliche Navigation mit Dashboard-Link
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/app.py (dashboard() komplett überarbeitet, create_license() Route)
+- v2_adminpanel/templates/dashboard.html (neu erstellt)
+- v2_adminpanel/templates/index.html (Navigation erweitert)
+- v2_adminpanel/templates/licenses.html (Navigation angepasst)
+- v2_adminpanel/templates/customers.html (Navigation angepasst)
+
+**Dashboard-Inhalte:**
+- 4 Hauptstatistiken als Karten
+- Lizenztyp-Verteilung
+- Status-Verteilung
+- Warnung für bald ablaufende Lizenzen
+- Übersicht der neuesten Aktivitäten
+
+### 2025-06-06 - Suchfunktion implementiert
+- Volltextsuche für Lizenzen und Kunden
+- Case-insensitive Suche mit LIKE-Operator
+- Suchergebnisse mit Hervorhebung des Suchbegriffs
+- Suche zurücksetzen Button
+
+**Neue Features:**
+- **Lizenzsuche**: Sucht in Lizenzschlüssel, Kundenname und E-Mail
+- **Kundensuche**: Sucht in Kundenname und E-Mail
+- Suchformular mit autofocus für schnelle Eingabe
+- Anzeige des aktiven Suchbegriffs
+- Unterschiedliche Meldungen für leere Ergebnisse
+
+**Geänderte Dateien:**
+- v2_adminpanel/app.py (licenses() und customers() mit Suchlogik erweitert)
+- v2_adminpanel/templates/licenses.html (Suchformular hinzugefügt)
+- v2_adminpanel/templates/customers.html (Suchformular hinzugefügt)
+
+**Technische Details:**
+- GET-Parameter für Suche
+- SQL LIKE mit LOWER() für Case-Insensitive Suche
+- Wildcards (%) für Teilstring-Suche
+- UTF-8 kompatibel für deutsche Umlaute
+
+### 2025-06-06 - Filter und Pagination implementiert
+- Erweiterte Filteroptionen für Lizenzübersicht
+- Pagination für große Datenmengen (20 Einträge pro Seite)
+- Filter bleiben bei Seitenwechsel erhalten
+
+**Neue Features für Lizenzen:**
+- **Filter nach Typ**: Alle, Vollversion, Testversion
+- **Filter nach Status**: Alle, Aktiv, Läuft bald ab, Abgelaufen, Deaktiviert
+- **Kombinierbar mit Suche**: Filter und Suche funktionieren zusammen
+- **Pagination**: Navigation durch mehrere Seiten
+- **Ergebnisanzeige**: Zeigt Anzahl gefilterter Ergebnisse
+
+**Neue Features für Kunden:**
+- **Pagination**: 20 Kunden pro Seite
+- **Seitennavigation**: Erste, Letzte, Vor, Zurück
+- **Kombiniert mit Suche**: Suchparameter bleiben erhalten
+
+**Geänderte Dateien:**
+- v2_adminpanel/app.py (licenses() und customers() mit Filter/Pagination erweitert)
+- v2_adminpanel/templates/licenses.html (Filter-Formular und Pagination hinzugefügt)
+- v2_adminpanel/templates/customers.html (Pagination hinzugefügt)
+
+**Technische Details:**
+- SQL WHERE-Klauseln für Filter
+- LIMIT/OFFSET für Pagination
+- URL-Parameter bleiben bei Navigation erhalten
+- Responsive Bootstrap-Komponenten
+
+### 2025-06-06 - Session-Tracking implementiert
+- Neue Tabelle für Session-Verwaltung
+- Anzeige aktiver und beendeter Sessions
+- Manuelles Beenden von Sessions möglich
+- Dashboard zeigt Anzahl aktiver Sessions
+
+**Neue Features:**
+- **Sessions-Tabelle**: Speichert Session-ID, IP, User-Agent, Zeitstempel
+- **Aktive Sessions**: Zeigt alle laufenden Sessions mit Inaktivitätszeit
+- **Session-Historie**: Letzte 24 Stunden beendeter Sessions
+- **Session beenden**: Admins können Sessions manuell beenden
+- **Farbcodierung**: Grün (aktiv), Gelb (>5 Min inaktiv), Rot (lange inaktiv)
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/init.sql (sessions Tabelle hinzugefügt)
+- v2_adminpanel/app.py (sessions() und end_session() Routen)
+- v2_adminpanel/templates/sessions.html (neu erstellt)
+- v2_adminpanel/templates/dashboard.html (Session-Statistik)
+- Alle Templates (Session-Navigation hinzugefügt)
+
+**Technische Details:**
+- Heartbeat-basiertes Tracking (last_heartbeat)
+- Automatische Inaktivitätsberechnung
+- Session-Dauer Berechnung
+- Responsive Tabellen mit Bootstrap
+
+**Hinweis:**
+Die Session-Daten werden erst gefüllt, wenn der License Server API implementiert ist und Clients sich verbinden.
+
+### 2025-06-06 - Export-Funktion implementiert
+- CSV und Excel Export für Lizenzen und Kunden
+- Formatierte Ausgabe mit deutschen Datumsformaten
+- UTF-8 Unterstützung für Sonderzeichen
+
+**Neue Features:**
+- **Lizenz-Export**: Alle Lizenzen mit Kundeninformationen
+- **Kunden-Export**: Alle Kunden mit Lizenzstatistiken
+- **Format-Optionen**: Excel (.xlsx) und CSV (.csv)
+- **Deutsche Formatierung**: Datum als dd.mm.yyyy, Status auf Deutsch
+- **UTF-8 Export**: Korrekte Kodierung für Umlaute
+- **Export-Buttons**: Dropdown-Menüs in Lizenz- und Kundenübersicht
+
+**Geänderte Dateien:**
+- v2_adminpanel/app.py (export_licenses() und export_customers() Routen)
+- v2_adminpanel/requirements.txt (pandas und openpyxl hinzugefügt)
+- v2_adminpanel/templates/licenses.html (Export-Dropdown hinzugefügt)
+- v2_adminpanel/templates/customers.html (Export-Dropdown hinzugefügt)
+
+**Technische Details:**
+- Pandas für Datenverarbeitung
+- OpenPyXL für Excel-Export
+- CSV mit Semikolon-Trennung für deutsche Excel-Kompatibilität
+- Automatische Spaltenbreite in Excel
+- BOM für UTF-8 CSV (Excel-Kompatibilität)
+
+### 2025-06-06 - Audit-Log implementiert
+- Vollständiges Änderungsprotokoll für alle Aktionen
+- Filterbare Übersicht mit Pagination
+- Detaillierte Anzeige von Änderungen
+
+**Neue Features:**
+- **Audit-Log-Tabelle**: Speichert alle Änderungen mit Zeitstempel, Benutzer, IP
+- **Protokollierte Aktionen**: CREATE, UPDATE, DELETE, LOGIN, LOGOUT, EXPORT
+- **JSON-Speicherung**: Alte und neue Werte als JSONB für flexible Abfragen
+- **Filter-Optionen**: Nach Benutzer, Aktion und Entität
+- **Detail-Anzeige**: Aufklappbare Änderungsdetails
+- **Navigation**: Audit-Link in allen Templates
+
+**Geänderte/Neue Dateien:**
+- v2_adminpanel/init.sql (audit_log Tabelle mit Indizes)
+- v2_adminpanel/app.py (log_audit() Funktion und audit_log() Route)
+- v2_adminpanel/templates/audit_log.html (neu erstellt)
+- Alle Templates (Audit-Navigation hinzugefügt)
+
+**Technische Details:**
+- JSONB für strukturierte Datenspeicherung
+- Performance-Indizes auf timestamp, username und entity
+- Farbcodierung für verschiedene Aktionen
+- 50 Einträge pro Seite mit Pagination
+- IP-Adresse und User-Agent Tracking
+
+### 2025-06-06 - PostgreSQL UTF-8 Locale konfiguriert
+- Eigenes PostgreSQL Dockerfile für deutsche Locale
+- Sicherstellung der UTF-8 Unterstützung auf Datenbankebene
+
+**Neue Features:**
+- **PostgreSQL Dockerfile**: Installiert deutsche Locale (de_DE.UTF-8)
+- **Locale-Umgebungsvariablen**: LANG, LANGUAGE, LC_ALL gesetzt
+- **Docker Compose Update**: Verwendet jetzt eigenes PostgreSQL-Image
+
+**Neue Dateien:**
+- v2_postgres/Dockerfile (neu erstellt)
+
+**Geänderte Dateien:**
+- v2/docker-compose.yaml (postgres Service nutzt jetzt build statt image)
+
+**Technische Details:**
+- Basis-Image: postgres:14
+- Locale-Installation über apt-get
+- locale-gen für de_DE.UTF-8
+- Vollständige UTF-8 Unterstützung für deutsche Sonderzeichen
+
+### 2025-06-07 - Backup-Funktionalität implementiert
+- Verschlüsselte Backups mit manueller und automatischer Ausführung
+- Backup-Historie mit Download und Wiederherstellung
+- Dashboard-Integration für Backup-Status
+
+**Neue Features:**
+- **Backup-Erstellung**: Manuell und automatisch (täglich 3:00 Uhr)
+- **Verschlüsselung**: AES-256 mit Fernet, Key aus ENV oder automatisch generiert
+- **Komprimierung**: GZIP-Komprimierung vor Verschlüsselung
+- **Backup-Historie**: Vollständige Übersicht aller Backups
+- **Wiederherstellung**: Mit optionalem Verschlüsselungs-Passwort
+- **Download-Funktion**: Backups können heruntergeladen werden
+- **Dashboard-Widget**: Zeigt letztes Backup-Status
+- **E-Mail-Vorbereitung**: Struktur für Benachrichtigungen (deaktiviert)
+
+**Neue/Geänderte Dateien:**
+- v2_adminpanel/init.sql (backup_history Tabelle hinzugefügt)
+- v2_adminpanel/requirements.txt (cryptography, apscheduler hinzugefügt)
+- v2_adminpanel/app.py (Backup-Funktionen und Routen)
+- v2_adminpanel/templates/backups.html (neu erstellt)
+- v2_adminpanel/templates/dashboard.html (Backup-Status-Widget)
+- v2_adminpanel/Dockerfile (PostgreSQL-Client installiert)
+- v2/.env (EMAIL_ENABLED und BACKUP_ENCRYPTION_KEY)
+- Alle Templates (Backup-Navigation hinzugefügt)
+
+**Technische Details:**
+- Speicherort: C:\Users\Administrator\Documents\GitHub\v2-Docker\backups\
+- Dateiformat: backup_v2docker_YYYYMMDD_HHMMSS_encrypted.sql.gz.enc
+- APScheduler für automatische Backups
+- pg_dump/psql für Datenbank-Operationen
+- Audit-Log für alle Backup-Aktionen
+- Sicherheitsabfrage bei Wiederherstellung
+
+### 2025-06-07 - HTTPS/SSL und Internet-Zugriff implementiert
+- Nginx Reverse Proxy für externe Erreichbarkeit eingerichtet
+- SSL-Zertifikate von IONOS mit vollständiger Certificate Chain integriert
+- Netzwerkkonfiguration für feste IP-Adresse
+- DynDNS und Port-Forwarding konfiguriert
+
+**Neue Features:**
+- **Nginx Reverse Proxy**: Leitet HTTPS-Anfragen an Container weiter
+- **SSL-Zertifikate**: Wildcard-Zertifikat von IONOS für *.z5m7q9dk3ah2v1plx6ju.com
+- **Certificate Chain**: Server-, Intermediate- und Root-Zertifikate kombiniert
+- **Subdomain-Routing**: admin-panel-undso und api-software-undso
+- **Port-Forwarding**: FRITZ!Box 443 → 192.168.178.88
+- **Feste IP**: Windows-PC auf 192.168.178.88 konfiguriert
+
+**Neue/Geänderte Dateien:**
+- v2_nginx/nginx.conf (Reverse Proxy Konfiguration)
+- v2_nginx/Dockerfile (Nginx Container mit SSL)
+- v2_nginx/ssl/fullchain.pem (Certificate Chain)
+- v2_nginx/ssl/privkey.pem (Private Key)
+- v2/docker-compose.yaml (nginx Service hinzugefügt)
+- set-static-ip.ps1 (PowerShell Script für feste IP)
+- reset-to-dhcp.ps1 (PowerShell Script für DHCP)
+
+**Technische Details:**
+- SSL-Termination am Nginx Reverse Proxy
+- Backend-Kommunikation über Docker-internes Netzwerk
+- Admin-Panel nur noch über Nginx erreichbar (Port 443 nicht mehr exposed)
+- License-Server behält externen Port 8443 für direkte API-Zugriffe
+- Intermediate Certificates aus ZIP extrahiert und korrekt verkettet
+
+**Zugangsdaten:**
+- Admin-Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
+- Benutzer 1: rac00n
+- Benutzer 2: w@rh@mm3r
+
+**Status:**
+- ✅ Admin-Panel extern erreichbar ohne SSL-Warnungen
+- ✅ Reverse Proxy funktioniert
+- ✅ SSL-Zertifikate korrekt konfiguriert
+- ✅ Netzwerk-Setup abgeschlossen
+
+### 2025-06-07 - Projekt-Cleanup durchgeführt
+- Redundante und überflüssige Dateien entfernt
+- Projektstruktur verbessert und organisiert
+
+**Durchgeführte Änderungen:**
+1. **Entfernte Dateien:**
+   - v2_adminpanel/templates/.env (Duplikat der Haupt-.env)
+   - v2_postgreSQL/ (leeres Verzeichnis)
+   - SSL-Zertifikate aus Root-Verzeichnis (7 Dateien)
+   - Ungenutzer `json` Import aus app.py
+
+2. **Organisatorische Verbesserungen:**
+   - PowerShell-Scripts in neuen `scripts/` Ordner verschoben
+   - SSL-Zertifikate nur noch in v2_nginx/ssl/
+   - Keine Konfigurationsdateien mehr in Template-Verzeichnissen
+
+**Technische Details:**
+- Docker-Container wurden gestoppt und nach Cleanup neu gestartet
+- Alle Services laufen wieder normal
+- Keine funktionalen Änderungen, nur Struktur-Verbesserungen
+
+**Ergebnis:**
+- Verbesserte Projektstruktur
+- Erhöhte Sicherheit (keine SSL-Zertifikate im Root)
+- Klarere Dateiorganisation
+
+### 2025-06-07 - SSL "Nicht sicher" Problem behoben
+- Chrome-Warnung trotz gültigem Zertifikat analysiert und behoben
+- Ursache: Selbstsigniertes Zertifikat in der Admin Panel Flask-App
+
+**Durchgeführte Änderungen:**
+1. **Admin Panel Konfiguration (app.py):**
+   - Von HTTPS mit selbstsigniertem Zertifikat auf HTTP Port 5000 umgestellt
+   - `ssl_context='adhoc'` entfernt
+   - Flask läuft jetzt auf `0.0.0.0:5000` statt HTTPS
+
+2. **Dockerfile Anpassung (v2_adminpanel/Dockerfile):**
+   - EXPOSE Port von 443 auf 5000 geändert
+   - Container exponiert jetzt HTTP statt HTTPS
+
+3. **Nginx Konfiguration (nginx.conf):**
+   - proxy_pass von `https://admin-panel:443` auf `http://admin-panel:5000` geändert
+   - `proxy_ssl_verify off` entfernt (nicht mehr benötigt)
+   - Sicherheits-Header für beide Domains hinzugefügt:
+     - Strict-Transport-Security (HSTS) - erzwingt HTTPS für 1 Jahr
+     - X-Content-Type-Options - verhindert MIME-Type Sniffing
+     - X-Frame-Options - Schutz vor Clickjacking
+     - X-XSS-Protection - aktiviert XSS-Filter
+     - Referrer-Policy - kontrolliert Referrer-Informationen
+
+**Technische Details:**
+- Externer Traffic nutzt weiterhin HTTPS mit gültigen IONOS-Zertifikaten
+- Interne Kommunikation zwischen Nginx und Admin Panel läuft über HTTP (sicher im Docker-Netzwerk)
+- Kein selbstsigniertes Zertifikat mehr in der Zertifikatskette
+- SSL-Termination erfolgt ausschließlich am Nginx Reverse Proxy
+
+**Docker Neustart:**
+- Container gestoppt (`docker-compose down`)
+- Images neu gebaut (`docker-compose build`)
+- Container neu gestartet (`docker-compose up -d`)
+- Alle Services laufen normal
+
+**Ergebnis:**
+- ✅ "Nicht sicher" Warnung in Chrome behoben
+- ✅ Saubere SSL-Konfiguration ohne Mixed Content
+- ✅ Verbesserte Sicherheits-Header implementiert
+- ✅ Admin Panel zeigt jetzt grünes Schloss-Symbol
+
+### 2025-06-07 - Sicherheitslücke geschlossen: License Server Port
+- Direkter Zugriff auf License Server Port 8443 entfernt
+- Sicherheitsanalyse der exponierten Ports durchgeführt
+
+**Identifiziertes Problem:**
+- License Server war direkt auf Port 8443 von außen erreichbar
+- Umging damit die Nginx-Sicherheitsschicht und Security Headers
+- Besonders kritisch, da nur Platzhalter ohne echte Sicherheit
+
+**Durchgeführte Änderung:**
+- Port-Mapping für License Server in docker-compose.yaml entfernt
+- Service ist jetzt nur noch über Nginx Reverse Proxy erreichbar
+- Gleiche Sicherheitskonfiguration wie Admin Panel
+
+**Aktuelle Port-Exposition:**
+- ✅ Nginx: Port 80/443 (benötigt für externen Zugriff)
+- ✅ PostgreSQL: Keine Ports exponiert (gut)
+- ✅ Admin Panel: Nur über Nginx erreichbar
+- ✅ License Server: Nur über Nginx erreichbar (vorher direkt auf 8443)
+
+**Weitere identifizierte Sicherheitsthemen:**
+1. Credentials im Klartext in .env Datei
+2. SSL-Zertifikate im Repository gespeichert
+3. License Server noch nicht implementiert
+
+**Empfehlung:** Docker-Container neu starten für Änderungsübernahme
+
+### 2025-06-07 - License Server Port 8443 wieder aktiviert
+- Port 8443 für direkten Zugriff auf License Server wieder geöffnet
+- Notwendig für Client-Software Lizenzprüfung
+
+**Begründung:**
+- Client-Software benötigt direkten Zugriff für Lizenzprüfung
+- Umgehung von möglichen Firewall-Blockaden auf Port 443
+- Weniger Latenz ohne Nginx-Proxy
+- Flexibilität für verschiedene Client-Implementierungen
+
+**Konfiguration:**
+- License Server erreichbar über:
+  - Direkt: Port 8443 (für Client-Software)
+  - Via Nginx: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com (für Browser/Tests)
+
+**Sicherheitshinweis:**
+- Port 8443 ist wieder direkt exponiert
+- License Server muss vor Produktivbetrieb implementiert werden mit:
+  - Eigener SSL-Konfiguration
+  - API-Key Authentifizierung
+  - Rate Limiting
+  - Input-Validierung
+
+**Status:**
+- Port-Mapping in docker-compose.yaml wiederhergestellt
+- Änderung erfordert Docker-Neustart
+
+### 2025-06-07 - Rate-Limiting und Brute-Force-Schutz implementiert
+- Umfassender Schutz vor Login-Angriffen mit IP-Sperre
+- Dashboard-Integration für Sicherheitsüberwachung
+
+**Implementierte Features:**
+1. **Rate-Limiting System:**
+   - 5 Login-Versuche erlaubt, danach 24h IP-Sperre
+   - Progressive Fehlermeldungen (zufällig aus 5 lustigen Varianten)
+   - CAPTCHA nach 2 Fehlversuchen (Google reCAPTCHA v2 vorbereitet)
+   - E-Mail-Benachrichtigung bei Sperrung (vorbereitet, deaktiviert für PoC)
+
+2. **Timing-Attack Schutz:**
+   - Mindestens 1 Sekunde Antwortzeit bei allen Login-Versuchen
+   - Gleiche Antwortzeit bei richtigem/falschem Username
+   - Verhindert Username-Enumeration
+
+3. **Lustige Fehlermeldungen (zufällig):**
+   - "NOPE!"
+   - "ACCESS DENIED, TRY HARDER"
+   - "WRONG! 🚫"
+   - "COMPUTER SAYS NO"
+   - "YOU FAILED"
+
+4. **Dashboard-Sicherheitswidget:**
+   - Sicherheitslevel-Anzeige (NORMAL/ERHÖHT/KRITISCH)
+   - Anzahl gesperrter IPs
+   - Fehlversuche heute
+   - Letzte 5 Sicherheitsereignisse mit Details
+
+5. **IP-Verwaltung:**
+   - Übersicht aller gesperrten IPs
+   - Manuelles Entsperren möglich
+   - Login-Versuche zurücksetzen
+   - Detaillierte Informationen pro IP
+
+6. **Audit-Log Erweiterungen:**
+   - LOGIN_SUCCESS - Erfolgreiche Anmeldung
+   - LOGIN_FAILED - Fehlgeschlagener Versuch
+   - LOGIN_BLOCKED - IP wurde gesperrt
+   - UNBLOCK_IP - IP manuell entsperrt
+   - CLEAR_ATTEMPTS - Versuche zurückgesetzt
+
+**Neue/Geänderte Dateien:**
+- v2_adminpanel/init.sql (login_attempts Tabelle)
+- v2_adminpanel/app.py (Rate-Limiting Logik, neue Routen)
+- v2_adminpanel/templates/login.html (Fehlermeldungs-Styling, CAPTCHA)
+- v2_adminpanel/templates/dashboard.html (Sicherheitswidget)
+- v2_adminpanel/templates/blocked_ips.html (neu - IP-Verwaltung)
+
+**Technische Details:**
+- IP-Ermittlung berücksichtigt Proxy-Header (X-Forwarded-For)
+- Fehlermeldungen mit Animation (shake-effect)
+- Farbcodierung: Rot für Fehler, Lila für Sperre, Orange für CAPTCHA
+- Automatische Bereinigung alter Einträge möglich
+
+**Sicherheitsverbesserungen:**
+- Schutz vor Brute-Force-Angriffen
+- Timing-Attack-Schutz implementiert
+- IP-basierte Sperrung für 24 Stunden
+- Audit-Trail für alle Sicherheitsereignisse
+
+**Hinweis für Produktion:**
+- CAPTCHA-Keys müssen in .env konfiguriert werden
+- E-Mail-Server für Benachrichtigungen einrichten
+- Rate-Limits können über Konstanten angepasst werden
+
+### 2025-06-07 - Session-Timeout mit Live-Timer implementiert
+- 5 Minuten Inaktivitäts-Timeout mit visueller Countdown-Anzeige
+- Automatische Session-Verlängerung bei Benutzeraktivität
+
+**Implementierte Features:**
+1. **Session-Timeout Backend:**
+   - Flask Session-Timeout auf 5 Minuten konfiguriert
+   - Heartbeat-Endpoint für Keep-Alive
+   - Automatisches Session-Update bei jeder Aktion
+
+2. **Live-Timer in der Navbar:**
+   - Countdown von 5:00 bis 0:00
+   - Position: Zwischen Logo und Username
+   - Farbwechsel nach verbleibender Zeit:
+     - Grün: > 2 Minuten
+     - Gelb: 1-2 Minuten
+     - Rot: < 1 Minute
+     - Blinkend: < 30 Sekunden
+
+3. **Benutzerinteraktion:**
+   - Timer wird bei jeder Aktivität zurückgesetzt
+   - Tracking von: Klicks, Tastatureingaben, Mausbewegungen
+   - Automatischer Heartbeat bei Aktivität
+   - Warnung bei < 1 Minute mit "Session verlängern" Button
+
+4. **Base-Template System:**
+   - Neue base.html als Basis für alle Admin-Seiten
+   - Alle Templates (außer login.html) nutzen jetzt base.html
+   - Einheitliches Layout und Timer auf allen Seiten
+
+**Neue/Geänderte Dateien:**
+- v2_adminpanel/app.py (Session-Konfiguration, Heartbeat-Endpoint)
+- v2_adminpanel/templates/base.html (neu - Base-Template mit Timer)
+- Alle anderen Templates aktualisiert für Template-Vererbung
+
+**Technische Details:**
+- JavaScript-basierter Countdown-Timer
+- AJAX-Heartbeat alle 5 Sekunden bei Aktivität
+- LocalStorage für Tab-Synchronisation möglich
+- Automatischer Logout bei 0:00
+- Fetch-Interceptor für automatische Session-Verlängerung
+
+**Sicherheitsverbesserung:**
+- Automatischer Logout nach 5 Minuten Inaktivität
+- Verhindert vergessene Sessions
+- Visuelles Feedback für Session-Status
+
+### 2025-06-07 - Session-Timeout Bug behoben
+- Problem: Session-Timeout funktionierte nicht korrekt - Session blieb länger als 5 Minuten aktiv
+- Ursache: login_required Decorator aktualisierte last_activity bei JEDEM Request
+
+**Durchgeführte Änderungen:**
+1. **login_required Decorator (app.py):**
+   - Prüft jetzt ob Session abgelaufen ist (5 Minuten seit last_activity)
+   - Aktualisiert last_activity NICHT mehr automatisch
+   - Führt AUTO_LOGOUT mit Audit-Log bei Timeout durch
+   - Speichert Username vor session.clear() für korrektes Logging
+
+2. **Heartbeat-Endpoint (app.py):**
+   - Geändert zu POST-only Endpoint
+   - Aktualisiert explizit last_activity wenn aufgerufen
+   - Wird nur bei aktiver Benutzerinteraktion aufgerufen
+
+3. **Frontend Timer (base.html):**
+   - Heartbeat wird als POST Request gesendet
+   - trackActivity() ruft extendSession() ohne vorheriges resetTimer() auf
+   - Timer wird erst nach erfolgreichem Heartbeat zurückgesetzt
+   - AJAX Interceptor ignoriert Heartbeat-Requests
+
+4. **Audit-Log Erweiterung:**
+   - Neue Aktion AUTO_LOGOUT hinzugefügt
+   - Orange Farbcodierung (#fd7e14)
+   - Zeigt Grund des Timeouts im Audit-Log
+
+**Ergebnis:**
+- ✅ Session läuft nach exakt 5 Minuten Inaktivität ab
+- ✅ Benutzeraktivität verlängert Session korrekt
+- ✅ AUTO_LOGOUT wird im Audit-Log protokolliert
+- ✅ Visueller Timer zeigt verbleibende Zeit
+
+### 2025-06-07 - Session-Timeout weitere Verbesserungen
+- Zusätzliche Fixes nach Test-Feedback implementiert
+
+**Weitere durchgeführte Änderungen:**
+1. **Fehlender Import behoben:**
+   - `flash` zu Flask-Imports hinzugefügt für Timeout-Warnmeldungen
+
+2. **Session-Cookie-Konfiguration erweitert (app.py):**
+   - SESSION_COOKIE_HTTPONLY = True (Sicherheit gegen XSS)
+   - SESSION_COOKIE_SECURE = False (intern HTTP, extern HTTPS via Nginx)
+   - SESSION_COOKIE_SAMESITE = 'Lax' (CSRF-Schutz)
+   - SESSION_COOKIE_NAME = 'admin_session' (eindeutiger Name)
+   - SESSION_REFRESH_EACH_REQUEST = False (verhindert automatische Verlängerung)
+
+3. **Session-Handling verbessert:**
+   - Entfernt: session.permanent = True aus login_required decorator
+   - Hinzugefügt: session.modified = True im Heartbeat für explizites Speichern
+   - Debug-Logging für Session-Timeout-Prüfung hinzugefügt
+
+4. **Nginx-Konfiguration:**
+   - Bereits korrekt konfiguriert für Heartbeat-Weiterleitung
+   - Proxy-Headers für korrekte IP-Weitergabe
+
+**Technische Details:**
+- Flask-Session mit Filesystem-Backend nutzt jetzt korrekte Cookie-Einstellungen
+- Session-Cookie wird nicht mehr automatisch bei jedem Request verlängert
+- Explizite Session-Modifikation nur bei Heartbeat-Requests
+- Debug-Logs zeigen Zeit seit letzter Aktivität für Troubleshooting
+
+**Status:**
+- ✅ Session-Timeout-Mechanismus vollständig implementiert
+- ✅ Debug-Logging für Session-Überwachung aktiv
+- ✅ Cookie-Sicherheitseinstellungen optimiert
+
+### 2025-06-07 - CAPTCHA Backend-Validierung implementiert
+- Google reCAPTCHA v2 Backend-Verifizierung hinzugefügt
+
+**Implementierte Features:**
+1. **verify_recaptcha() Funktion (app.py):**
+   - Validiert CAPTCHA-Response mit Google API
+   - Fallback: Wenn RECAPTCHA_SECRET_KEY nicht konfiguriert, wird CAPTCHA übersprungen (für PoC)
+   - Timeout von 5 Sekunden für API-Request
+   - Error-Handling für Netzwerkfehler
+   - Logging für Debugging und Fehleranalyse
+
+2. **Login-Route Erweiterungen:**
+   - CAPTCHA wird nach 2 Fehlversuchen angezeigt
+   - Prüfung ob CAPTCHA-Response vorhanden
+   - Validierung der CAPTCHA-Response gegen Google API
+   - Unterschiedliche Fehlermeldungen für fehlende/ungültige CAPTCHA
+   - Site Key wird aus Environment-Variable an Template übergeben
+
+3. **Environment-Konfiguration (.env):**
+   - RECAPTCHA_SITE_KEY (für Frontend)
+   - RECAPTCHA_SECRET_KEY (für Backend-Validierung)
+   - Beide auskommentiert für PoC-Phase
+
+4. **Dependencies:**
+   - requests Library zu requirements.txt hinzugefügt
+
+**Sicherheitsaspekte:**
+- CAPTCHA verhindert automatisierte Brute-Force-Angriffe
+- Timing-Attack-Schutz bleibt auch bei CAPTCHA-Prüfung aktiv
+- Bei Netzwerkfehlern wird CAPTCHA als bestanden gewertet (Verfügbarkeit vor Sicherheit)
+- Secret Key wird niemals im Frontend exponiert
+
+**Verwendung:**
+1. Google reCAPTCHA v2 Keys erstellen: https://www.google.com/recaptcha/admin
+2. Keys in .env eintragen:
+   ```
+   RECAPTCHA_SITE_KEY=your-site-key
+   RECAPTCHA_SECRET_KEY=your-secret-key
+   ```
+3. Container neu starten
+
+**Status:**
+- ✅ CAPTCHA-Frontend bereits vorhanden (login.html)
+- ✅ Backend-Validierung vollständig implementiert
+- ✅ Fallback für PoC-Betrieb ohne Google-Keys
+- ✅ Integration in Rate-Limiting-System
+- ⚠️ CAPTCHA-Keys noch nicht konfiguriert (für PoC deaktiviert)
+
+**Anleitung für Google reCAPTCHA Keys:**
+
+1. **Registrierung bei Google reCAPTCHA:**
+   - Gehe zu: https://www.google.com/recaptcha/admin/create
+   - Melde dich mit Google-Konto an
+   - Label eingeben: "v2-Docker Admin Panel"
+   - Typ wählen: "reCAPTCHA v2" → "Ich bin kein Roboter"-Kästchen
+   - Domains hinzufügen:
+     ```
+     admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
+     localhost
+     ```
+   - Nutzungsbedingungen akzeptieren
+   - Senden klicken
+
+2. **Keys erhalten:**
+   - Site Key (öffentlich für Frontend)
+   - Secret Key (geheim für Backend-Validierung)
+
+3. **Keys in .env eintragen:**
+   ```bash
+   RECAPTCHA_SITE_KEY=6Ld...
+   RECAPTCHA_SECRET_KEY=6Ld...
+   ```
+
+4. **Container neu starten:**
+   ```bash
+   docker-compose down
+   docker-compose up -d
+   ```
+
+**Kosten:**
+- Kostenlos bis 1 Million Anfragen pro Monat
+- Danach: $1.00 pro 1000 zusätzliche Anfragen
+- Für dieses Projekt reicht die kostenlose Version vollkommen aus
+
+**Test-Keys für Entwicklung:**
+- Site Key: `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI`
+- Secret Key: `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe`
+- ⚠️ Diese Keys nur für lokale Tests verwenden, niemals produktiv!
+
+**Aktueller Status:**
+- Code ist vollständig implementiert und getestet
+- CAPTCHA wird nach 2 Fehlversuchen angezeigt
+- Ohne konfigurierte Keys wird CAPTCHA-Prüfung übersprungen
+- Für Produktion müssen nur die Keys in .env eingetragen werden
+
+### 2025-06-07 - License Key Generator implementiert
+- Automatische Generierung von Lizenzschlüsseln mit definiertem Format
+
+**Implementiertes Format:**
+`AF-YYYYMMFT-XXXX-YYYY-ZZZZ`
+- **AF** = Account Factory (feste Produktkennung)
+- **YYYY** = Jahr (z.B. 2025)
+- **MM** = Monat (z.B. 06)
+- **FT** = Lizenztyp (F=Fullversion, T=Testversion)
+- **XXXX-YYYY-ZZZZ** = Zufällige alphanumerische Zeichen (ohne verwirrende wie 0/O, 1/I/l)
+
+**Beispiele:**
+- Vollversion: `AF-202506F-A7K9-M3P2-X8R4`
+- Testversion: `AF-202512T-B2N5-K8L3-Q9W7`
+
+**Implementierte Features:**
+
+1. **Backend-Funktionen (app.py):**
+   - `generate_license_key()` - Generiert Keys mit kryptografisch sicherem Zufallsgenerator
+   - `validate_license_key()` - Validiert das Key-Format mit Regex
+   - Verwendet `secrets` statt `random` für Sicherheit
+   - Erlaubte Zeichen: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (ohne verwirrende)
+
+2. **API-Endpoint:**
+   - POST `/api/generate-license-key` - JSON API für Key-Generierung
+   - Prüft auf Duplikate in der Datenbank (max. 10 Versuche)
+   - Audit-Log-Eintrag bei jeder Generierung
+   - Login-Required geschützt
+
+3. **Frontend-Verbesserungen (index.html):**
+   - Generate-Button neben License Key Input
+   - Placeholder und Pattern-Attribut für Format-Hinweis
+   - Auto-Uppercase bei manueller Eingabe
+   - Visuelles Feedback bei erfolgreicher Generierung
+   - Format-Hinweis unter dem Eingabefeld
+
+4. **JavaScript-Features:**
+   - AJAX-basierte Key-Generierung ohne Seiten-Reload
+   - Automatische Prüfung bei Lizenztyp-Änderung
+   - Ladeindikator während der Generierung
+   - Fehlerbehandlung mit Benutzer-Feedback
+   - Standard-Datum-Einstellungen (heute + 1 Jahr)
+
+5. **Validierung:**
+   - Server-seitige Format-Validierung beim Speichern
+   - Flash-Message bei ungültigem Format
+   - Automatische Großschreibung des Keys
+   - Pattern-Validierung im HTML-Formular
+
+6. **Weitere Fixes:**
+   - Form Action von "/" auf "/create" korrigiert
+   - Flash-Messages mit Bootstrap Toasts implementiert
+   - GENERATE_KEY Aktion zum Audit-Log hinzugefügt (Farbe: #20c997)
+
+**Technische Details:**
+- Keine vorhersagbaren Muster durch `secrets.choice()`
+- Datum im Key zeigt Erstellungszeitpunkt
+- Lizenztyp direkt im Key erkennbar
+- Kollisionsprüfung gegen Datenbank
+
+**Status:**
+- ✅ Backend-Generierung vollständig implementiert
+- ✅ Frontend mit Generate-Button und JavaScript
+- ✅ Validierung und Fehlerbehandlung
+- ✅ Audit-Log-Integration
+- ✅ Form-Action-Bug behoben
+
+### 2025-06-07 - Batch-Lizenzgenerierung implementiert
+- Mehrere Lizenzen auf einmal für einen Kunden erstellen
+
+**Implementierte Features:**
+
+1. **Batch-Formular (/batch):**
+   - Kunde und E-Mail eingeben
+   - Anzahl der Lizenzen (1-100)
+   - Lizenztyp (Vollversion/Testversion)
+   - Gültigkeitszeitraum für alle Lizenzen
+   - Vorschau-Modal zeigt Key-Format
+   - Standard-Datum-Einstellungen (heute + 1 Jahr)
+
+2. **Backend-Verarbeitung:**
+   - Route `/batch` für GET (Formular) und POST (Generierung)
+   - Generiert die angegebene Anzahl eindeutiger Keys
+   - Speichert alle in einer Transaktion
+   - Kunde wird automatisch angelegt (falls nicht vorhanden)
+   - ON CONFLICT für existierende Kunden
+   - Audit-Log-Eintrag mit CREATE_BATCH Aktion
+
+3. **Ergebnis-Seite:**
+   - Zeigt alle generierten Lizenzen in Tabellenform
+   - Kundeninformationen und Gültigkeitszeitraum
+   - Einzelne Keys können kopiert werden (📋 Button)
+   - Alle Keys auf einmal kopieren
+   - Druckfunktion für physische Ausgabe
+   - Link zur Lizenzübersicht mit Kundenfilter
+
+4. **Export-Funktionalität:**
+   - Route `/batch/export` für CSV-Download
+   - Speichert Batch-Daten in Session für Export
+   - CSV mit UTF-8 BOM für Excel-Kompatibilität
+   - Enthält Kundeninfo, Generierungsdatum und alle Keys
+   - Format: Nr;Lizenzschlüssel;Typ
+   - Dateiname: batch_licenses_KUNDE_TIMESTAMP.csv
+
+5. **Integration:**
+   - Batch-Button in Navigation (Dashboard, Einzellizenz-Seite)
+   - CREATE_BATCH Aktion im Audit-Log (Farbe: #6610f2)
+   - Session-basierte Export-Daten
+   - Flash-Messages für Feedback
+
+**Sicherheit:**
+- Limit von 100 Lizenzen pro Batch
+- Login-Required für alle Routen
+- Transaktionale Datenbank-Operationen
+- Validierung der Eingaben
+
+**Beispiel-Workflow:**
+1. Admin geht zu `/batch`
+2. Gibt Kunde "Firma GmbH", Anzahl "25", Typ "Vollversion" ein
+3. System generiert 25 eindeutige Keys
+4. Ergebnis-Seite zeigt alle Keys
+5. Admin kann CSV exportieren oder Keys kopieren
+6. Kunde erhält die Lizenzen
+
+**Status:**
+- ✅ Batch-Formular vollständig implementiert
+- ✅ Backend-Generierung mit Transaktionen
+- ✅ Export als CSV
+- ✅ Copy-to-Clipboard Funktionalität
+- ✅ Audit-Log-Integration
+- ✅ Navigation aktualisiert
+
+## 2025-06-06: Implementierung Searchable Dropdown für Kundenauswahl
+
+**Problem:**
+- Bei der Lizenzerstellung wurde immer ein neuer Kunde angelegt
+- Keine Möglichkeit, Lizenzen für bestehende Kunden zu erstellen
+- Bei vielen Kunden wäre ein normales Dropdown unübersichtlich
+
+**Lösung:**
+1. **Select2 Library** für searchable Dropdown integriert
+2. **API-Endpoint `/api/customers`** für die Kundensuche erstellt
+3. **Frontend angepasst:**
+   - Searchable Dropdown mit Live-Suche
+   - Option "Neuer Kunde" im Dropdown
+   - Eingabefelder erscheinen nur bei "Neuer Kunde"
+4. **Backend-Logik verbessert:**
+   - Prüfung ob neuer oder bestehender Kunde
+   - E-Mail-Duplikatsprüfung vor Kundenerstellung
+   - Separate Audit-Logs für Kunde und Lizenz
+5. **Datenbank:**
+   - UNIQUE Constraint auf E-Mail-Spalte hinzugefügt
+
+**Änderungen:**
+- `app.py`: Neuer API-Endpoint `/api/customers`, angepasste Routes `/create` und `/batch`
+- `base.html`: Select2 CSS und JS eingebunden
+- `index.html`: Kundenauswahl mit Select2 implementiert
+- `batch_form.html`: Kundenauswahl mit Select2 implementiert
+- `init.sql`: UNIQUE Constraint für E-Mail
+
+**Status:**
+- ✅ API-Endpoint funktioniert mit Pagination
+- ✅ Select2 Dropdown mit Suchfunktion
+- ✅ Neue/bestehende Kunden können ausgewählt werden
+- ✅ E-Mail-Duplikate werden verhindert
+- ✅ Sowohl Einzellizenz als auch Batch unterstützt
+
+## 2025-06-06: Automatische Ablaufdatum-Berechnung
+
+**Problem:**
+- Manuelles Eingeben von Start- und Enddatum war umständlich
+- Fehleranfällig bei der Datumseingabe
+- Nicht intuitiv für Standard-Laufzeiten
+
+**Lösung:**
+1. **Frontend-Änderungen:**
+   - Startdatum + Laufzeit (Zahl) + Einheit (Tage/Monate/Jahre)
+   - Ablaufdatum wird automatisch berechnet und angezeigt (read-only)
+   - Standard: 1 Jahr Laufzeit voreingestellt
+2. **Backend-Validierung:**
+   - Server-seitige Berechnung zur Sicherheit
+   - Verwendung von `python-dateutil` für korrekte Monats-/Jahresberechnungen
+3. **Benutzerfreundlichkeit:**
+   - Sofortige Neuberechnung bei Änderungen
+   - Visuelle Hervorhebung des berechneten Datums
+
+**Änderungen:**
+- `index.html`: Laufzeit-Eingabe statt Ablaufdatum
+- `batch_form.html`: Laufzeit-Eingabe statt Ablaufdatum
+- `app.py`: Datum-Berechnung in `/create` und `/batch` Routes
+- `requirements.txt`: `python-dateutil` hinzugefügt
+
+**Status:**
+- ✅ Automatische Berechnung funktioniert
+- ✅ Frontend zeigt berechnetes Datum sofort an
+- ✅ Backend validiert die Berechnung
+- ✅ Standardwert (1 Jahr) voreingestellt
+
+## 2025-06-06: Bugfix - created_at für licenses Tabelle
+
+**Problem:**
+- Batch-Generierung schlug fehl mit "Fehler bei der Batch-Generierung!"
+- INSERT Statement versuchte `created_at` zu setzen, aber Spalte existierte nicht
+- Inkonsistenz: Einzellizenzen hatten kein created_at, Batch-Lizenzen versuchten es zu setzen
+
+**Lösung:**
+1. **Datenbank-Schema erweitert:**
+   - `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` zur licenses Tabelle hinzugefügt
+   - Migration für bestehende Datenbanken implementiert
+   - Konsistent mit customers Tabelle
+2. **Code bereinigt:**
+   - Explizites `created_at` aus Batch-INSERT entfernt
+   - Datenbank setzt nun automatisch den Zeitstempel bei ALLEN Lizenzen
+
+**Änderungen:**
+- `init.sql`: created_at Spalte zur licenses Tabelle mit DEFAULT-Wert
+- `init.sql`: Migration für bestehende Datenbanken
+- `app.py`: Entfernt explizites created_at aus batch_licenses()
+
+**Status:**
+- ✅ Alle Lizenzen haben nun automatisch einen Erstellungszeitstempel
+- ✅ Batch-Generierung funktioniert wieder
+- ✅ Konsistente Zeitstempel für Audit-Zwecke
+
+## 2025-06-06: Status "Deaktiviert" für manuell abgeschaltete Lizenzen
+
+**Problem:**
+- Dashboard zeigte nur "aktiv" und "abgelaufen" als Status
+- Manuell deaktivierte Lizenzen (is_active = FALSE) wurden nicht korrekt angezeigt
+- Filter für "inactive" existierte, aber Status wurde nicht richtig berechnet
+
+**Lösung:**
+1. **Status-Berechnung erweitert:**
+   - CASE-Statement prüft zuerst `is_active = FALSE`
+   - Status "deaktiviert" wird vor anderen Status geprüft
+   - Reihenfolge: deaktiviert → abgelaufen → läuft bald ab → aktiv
+2. **Dashboard-Statistik erweitert:**
+   - Neue Zählung für deaktivierte Lizenzen
+   - Variable `inactive_licenses` im stats Dictionary
+
+**Änderungen:**
+- `app.py`: Dashboard - Status-Berechnung für letzte 5 Lizenzen
+- `app.py`: Lizenzübersicht - Status-Berechnung in der Hauptabfrage
+- `app.py`: Export - Status-Berechnung für CSV/Excel Export
+- `app.py`: Dashboard - Neue Statistik für deaktivierte Lizenzen
+
+**Status:**
+- ✅ "Deaktiviert" wird korrekt als Status angezeigt
+- ✅ Dashboard zeigt Anzahl deaktivierter Lizenzen
+- ✅ Export enthält korrekten Status
+- ✅ Konsistente Status-Anzeige überall
+
+## 2025-06-08: SSL-Sicherheit verbessert - Chrome Warnung behoben
+
+**Problem:**
+- Chrome zeigte Warnung "Die Verbindung zu dieser Website ist nicht sicher"
+- Nginx erlaubte schwache Cipher Suites (WEAK) ohne Perfect Forward Secrecy
+- Veraltete SSL-Konfiguration mit `ssl_ciphers HIGH:!aNULL:!MD5;`
+
+**Lösung:**
+1. **Moderne Cipher Suite Konfiguration:**
+   - Nur sichere ECDHE und DHE Cipher Suites
+   - Entfernung aller RSA-only Cipher Suites
+   - Perfect Forward Secrecy für alle Verbindungen
+2. **SSL-Optimierungen:**
+   - Session Cache aktiviert (1 Tag Timeout)
+   - OCSP Stapling für bessere Performance
+   - DH Parameters (2048 bit) für zusätzliche Sicherheit
+3. **Resolver-Konfiguration:**
+   - Google DNS Server für OCSP Stapling
+
+**Änderungen:**
+- `v2_nginx/nginx.conf`: Komplett überarbeitete SSL-Konfiguration
+- `v2_nginx/ssl/dhparam.pem`: Neue 2048-bit DH Parameters generiert
+- `v2_nginx/Dockerfile`: COPY Befehl für dhparam.pem hinzugefügt
+
+**Status:**
+- ✅ Nur noch sichere Cipher Suites aktiv
+- ✅ Perfect Forward Secrecy gewährleistet
+- ✅ OCSP Stapling aktiviert
+- ✅ Chrome Sicherheitswarnung behoben
+
+**Hinweis:** Nach dem Rebuild des nginx Containers wird die Verbindung als sicher angezeigt.
+
+## 2025-06-08: CAPTCHA-Login-Bug behoben
+
+**Problem:**
+- Nach 2 fehlgeschlagenen Login-Versuchen wurde CAPTCHA angezeigt
+- Da keine CAPTCHA-Keys konfiguriert waren (für PoC), konnte man sich nicht mehr einloggen
+- Selbst mit korrektem Passwort war Login blockiert
+- Fehlermeldung "CAPTCHA ERFORDERLICH!" erschien immer
+
+**Lösung:**
+1. **CAPTCHA-Prüfung nur wenn Keys vorhanden:**
+   - `recaptcha_site_key` wird vor CAPTCHA-Prüfung geprüft
+   - Wenn keine Keys konfiguriert → kein CAPTCHA-Check
+   - CAPTCHA wird nur angezeigt wenn Keys existieren
+2. **Template-Anpassungen:**
+   - login.html zeigt CAPTCHA nur wenn `recaptcha_site_key` vorhanden
+   - Kein Test-Key mehr als Fallback
+3. **Konsistente Logik:**
+   - show_captcha prüft jetzt auch ob Keys vorhanden sind
+   - Bei GET und POST Requests gleiche Logik
+
+**Änderungen:**
+- `v2_adminpanel/app.py`: CAPTCHA-Check nur wenn `RECAPTCHA_SITE_KEY` existiert
+- `v2_adminpanel/templates/login.html`: CAPTCHA nur anzeigen wenn Keys vorhanden
+
+**Status:**
+- ✅ Login funktioniert wieder nach 2+ Fehlversuchen
+- ✅ CAPTCHA wird nur angezeigt wenn Keys konfiguriert sind
+- ✅ Für PoC-Phase ohne CAPTCHA nutzbar
+- ✅ Produktiv-ready wenn CAPTCHA-Keys eingetragen werden
+
+### 2025-06-08: Zeitzone auf Europe/Berlin umgestellt
+
+**Problem:**
+- Alle Zeitstempel wurden in UTC gespeichert und angezeigt
+- Backup-Dateinamen zeigten UTC-Zeit statt deutsche Zeit
+- Verwirrung bei Zeitangaben im Admin Panel und Logs
+
+**Lösung:**
+1. **Docker Container Zeitzone konfiguriert:**
+   - Alle Dockerfiles mit `TZ=Europe/Berlin` und tzdata Installation
+   - PostgreSQL mit `PGTZ=Europe/Berlin` für Datenbank-Zeitzone
+   - Explizite Zeitzone-Dateien in /etc/localtime und /etc/timezone
+
+2. **Python Code angepasst:**
+   - Import von `zoneinfo.ZoneInfo` für Zeitzonenunterstützung
+   - Alle `datetime.now()` Aufrufe mit `ZoneInfo("Europe/Berlin")`
+   - `.replace(tzinfo=None)` für Kompatibilität mit timezone-unaware Timestamps
+
+3. **PostgreSQL Konfiguration:**
+   - `SET timezone = 'Europe/Berlin';` in init.sql
+   - Umgebungsvariablen TZ und PGTZ in docker-compose.yaml
+
+4. **docker-compose.yaml erweitert:**
+   - `TZ: Europe/Berlin` für alle Services
+
+**Geänderte Dateien:**
+- `v2_adminpanel/Dockerfile`: Zeitzone und tzdata hinzugefügt
+- `v2_postgres/Dockerfile`: Zeitzone und tzdata hinzugefügt
+- `v2_nginx/Dockerfile`: Zeitzone und tzdata hinzugefügt
+- `v2_lizenzserver/Dockerfile`: Zeitzone und tzdata hinzugefügt
+- `v2_adminpanel/app.py`: 14 datetime.now() Aufrufe mit Zeitzone versehen
+- `v2_adminpanel/init.sql`: PostgreSQL Zeitzone gesetzt
+- `v2/docker-compose.yaml`: TZ Environment-Variable für alle Services
+
+**Ergebnis:**
+- ✅ Alle neuen Zeitstempel werden in deutscher Zeit (Europe/Berlin) gespeichert
+- ✅ Backup-Dateinamen zeigen korrekte deutsche Zeit
+- ✅ Admin Panel zeigt alle Zeiten in deutscher Zeitzone
+- ✅ Automatische Anpassung bei Sommer-/Winterzeit
+- ✅ Konsistente Zeitangaben über alle Komponenten
+
+**Hinweis:** Nach diesen Änderungen müssen die Docker Container neu gebaut werden:
+```bash
+docker-compose down
+docker-compose build
+docker-compose up -d
+```
+
+### 2025-06-08: Zeitzone-Fix - PostgreSQL Timestamps
+
+**Problem nach erster Implementierung:**
+- Trotz Zeitzoneneinstellung wurden Zeiten immer noch in UTC angezeigt
+- Grund: PostgreSQL Tabellen verwendeten `TIMESTAMP WITHOUT TIME ZONE`
+
+**Zusätzliche Lösung:**
+1. **Datenbankschema angepasst:**
+   - Alle `TIMESTAMP` Spalten auf `TIMESTAMP WITH TIME ZONE` geändert
+   - Betrifft: created_at, timestamp, started_at, ended_at, last_heartbeat, etc.
+   - Migration für bestehende Datenbanken berücksichtigt
+
+2. **SQL-Abfragen vereinfacht:**
+   - `AT TIME ZONE 'Europe/Berlin'` nicht mehr nötig
+   - PostgreSQL handhabt Zeitzonenkonvertierung automatisch
+
+**Geänderte Datei:**
+- `v2_adminpanel/init.sql`: Alle TIMESTAMP Felder mit WITH TIME ZONE
+
+**Wichtig:** Bei bestehenden Installationen muss die Datenbank neu initialisiert oder manuell migriert werden:
+```sql
+ALTER TABLE customers ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE licenses ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE sessions ALTER COLUMN started_at TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE sessions ALTER COLUMN last_heartbeat TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE sessions ALTER COLUMN ended_at TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE audit_log ALTER COLUMN timestamp TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE backup_history ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE login_attempts ALTER COLUMN first_attempt TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE login_attempts ALTER COLUMN last_attempt TYPE TIMESTAMP WITH TIME ZONE;
+ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME ZONE;
+```
+
+### 2025-06-08: UI/UX Überarbeitung - Phase 1 (Navigation)
+
+**Problem:**
+- Inkonsistente Navigation zwischen verschiedenen Seiten
+- Zu viele Navigationspunkte im Dashboard
+- Verwirrende Benutzerführung
+
+**Lösung:**
+1. **Dashboard vereinfacht:**
+   - Nur noch 3 Buttons: Neue Lizenz, Batch-Lizenzen, Log
+   - Statistik-Karten wurden klickbar gemacht (verlinken zu jeweiligen Seiten)
+   - "Audit" wurde zu "Log" umbenannt
+
+2. **Navigation konsistent gemacht:**
+   - Navbar-Brand "AccountForger - Admin Panel" ist jetzt klickbar und führt zum Dashboard
+   - Keine Log-Links mehr in Unterseiten
+   - Konsistente "Dashboard" Buttons in allen Unterseiten
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/base.html`: Navbar-Brand klickbar gemacht
+- `v2_adminpanel/templates/dashboard.html`: Navigation reduziert, Karten klickbar
+- `v2_adminpanel/templates/*.html`: Konsistente Dashboard-Links
+
+### 2025-06-08: UI/UX Überarbeitung - Phase 2 (Visuelle Verbesserungen)
+
+**Implementierte Verbesserungen:**
+1. **Größere Icons in Statistik-Karten:**
+   - Icon-Größe auf 3rem erhöht
+   - Bessere visuelle Hierarchie
+
+2. **Donut-Chart für Lizenzen:**
+   - Chart.js Integration für Lizenzstatistik
+   - Zeigt Verhältnis Aktiv/Abgelaufen
+   - UPDATE: Später wieder entfernt auf Benutzerwunsch
+
+3. **Pulse-Effekt für aktive Sessions:**
+   - CSS-Animation für aktive Sessions
+   - Visueller Indikator für Live-Aktivität
+
+4. **Progress-Bar für Backup-Status:**
+   - Zeigt visuell den Erfolg des letzten Backups
+   - Inkl. Dateigröße und Dauer
+
+5. **Konsistente Farbcodierung:**
+   - CSS-Variablen für Statusfarben
+   - Globale Klassen für konsistente Darstellung
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/base.html`: Globale CSS-Variablen und Statusklassen
+- `v2_adminpanel/templates/dashboard.html`: Visuelle Verbesserungen implementiert
+
+### 2025-06-08: UI/UX Überarbeitung - Phase 3 (Tabellen-Optimierungen)
+
+**Problem:**
+- Tabellen waren schwer zu navigieren bei vielen Einträgen
+- Keine Möglichkeit für Bulk-Operationen
+- Umständliches Kopieren von Lizenzschlüsseln
+
+**Lösung:**
+1. **Sticky Headers:**
+   - Tabellenköpfe bleiben beim Scrollen sichtbar
+   - CSS-Klasse `.table-sticky` mit `position: sticky`
+
+2. **Inline-Actions:**
+   - Copy-Button direkt neben Lizenzschlüsseln
+   - Toggle-Switches für Aktiv/Inaktiv-Status
+   - Visuelles Feedback bei Aktionen
+
+3. **Bulk-Actions:**
+   - Checkboxen für Mehrfachauswahl
+   - "Select All" Funktionalität
+   - Bulk-Actions Bar mit Aktivieren/Deaktivieren/Löschen
+   - JavaScript für dynamische Anzeige
+
+4. **API-Endpoints hinzugefügt:**
+   - `/api/license/<id>/toggle` - Toggle einzelner Lizenzstatus
+   - `/api/licenses/bulk-activate` - Mehrere Lizenzen aktivieren
+   - `/api/licenses/bulk-deactivate` - Mehrere Lizenzen deaktivieren
+   - `/api/licenses/bulk-delete` - Mehrere Lizenzen löschen
+
+5. **Beispieldaten eingefügt:**
+   - 15 Testkunden
+   - 18 Lizenzen (verschiedene Status)
+   - Sessions, Audit-Logs, Login-Attempts
+   - Backup-Historie
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/base.html`: CSS für Sticky-Tables und Bulk-Actions
+- `v2_adminpanel/templates/licenses.html`: Komplette Tabellen-Überarbeitung
+- `v2_adminpanel/app.py`: 4 neue API-Endpoints für Toggle und Bulk-Operationen
+- `v2_adminpanel/sample_data.sql`: Umfangreiche Testdaten erstellt
+
+**Bugfix:**
+- API-Endpoints versuchten `updated_at` zu setzen, obwohl die Spalte nicht existiert
+- Entfernt aus allen 3 betroffenen Endpoints
+
+**Status:**
+- ✅ Sticky Headers funktionieren
+- ✅ Copy-Buttons mit Clipboard-API
+- ✅ Toggle-Switches ändern Lizenzstatus
+- ✅ Bulk-Operationen vollständig implementiert
+- ✅ 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
+
+### 2025-06-08: Bugfix - Sortierlogik korrigiert
+
+**Problem:**
+- Sortierung funktionierte nicht korrekt
+- Beim Klick auf Spaltenköpfe wurde immer absteigend sortiert
+- Toggle zwischen ASC/DESC funktionierte nicht
+
+**Ursachen:**
+1. **Falsche Bedingungslogik**: Die ursprüngliche Implementierung verwendete eine fehlerhafte Ternär-Bedingung
+2. **Berechnete Felder**: Das 'status' Feld in der Lizenztabelle konnte nicht direkt sortiert werden
+
+**Lösung:**
+1. **Sortierlogik korrigiert:**
+   - Bei neuer Spalte: Immer aufsteigend (ASC) beginnen
+   - Bei gleicher Spalte: Toggle zwischen ASC und DESC
+   - Implementiert durch bedingte Links in den Makros
+
+2. **Spezialbehandlung für berechnete Felder:**
+   - Status-Feld verwendet CASE-Statement in ORDER BY
+   - Wiederholt die gleiche Logik wie im SELECT
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/licenses.html`: Sortierlogik korrigiert
+- `v2_adminpanel/templates/customers.html`: Sortierlogik korrigiert
+- `v2_adminpanel/templates/audit_log.html`: Sortierlogik korrigiert
+- `v2_adminpanel/templates/sessions.html`: Sortierlogik für beide Tabellen korrigiert
+- `v2_adminpanel/app.py`: Spezialbehandlung für Status-Feld in licenses Route
+
+**Verhalten nach Fix:**
+- ✅ Erster Klick auf Spalte: Aufsteigend sortieren
+- ✅ Zweiter Klick: Absteigend sortieren
+- ✅ Weitere Klicks: Toggle zwischen ASC/DESC
+- ✅ Sortierung funktioniert korrekt mit Pagination und Filtern
+
+### 2025-06-09: Port 8443 geschlossen - API nur noch über Nginx
+
+**Problem:**
+- Doppelte Verfügbarkeit des License Servers (Port 8443 + Nginx) machte keinen Sinn
+- Inkonsistente Sicherheitskonfiguration (Nginx hatte Security Headers, Port 8443 nicht)
+- Doppelte SSL-Konfiguration nötig
+- Verwirrung welcher Zugangsweg genutzt werden soll
+
+**Lösung:**
+- Port-Mapping für License Server in docker-compose.yaml entfernt
+- API nur noch über Nginx erreichbar: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com
+- Interne Kommunikation zwischen Nginx und License Server bleibt bestehen
+
+**Vorteile:**
+- ✅ Einheitliche Sicherheitskonfiguration (Security Headers, HSTS)
+- ✅ Zentrale SSL-Verwaltung nur in Nginx
+- ✅ Möglichkeit für Rate Limiting und zentrales Logging
+- ✅ Keine zusätzlichen offenen Ports (nur 80/443)
+- ✅ Professionellere API-URL ohne Port-Angabe
+
+**Geänderte Dateien:**
+- `v2/docker-compose.yaml`: Port-Mapping "8443:8443" entfernt
+
+**Hinweis für Client-Software:**
+- API-Endpunkte sind weiterhin unter https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com erreichbar
+- Keine Änderung der API-URLs nötig, nur Port 8443 ist nicht mehr direkt zugänglich
+
+**Status:**
+- ✅ Port 8443 geschlossen
+- ✅ API nur noch über Nginx Reverse Proxy erreichbar
+- ✅ Sicherheit erhöht durch zentrale Verwaltung
+
+### 2025-06-09: Live-Filtering implementiert
+
+**Problem:**
+- Benutzer mussten immer auf "Filter anwenden" klicken
+- Umständliche Bedienung, besonders bei mehreren Filterkriterien
+- Nicht zeitgemäße User Experience
+
+**Lösung:**
+- JavaScript Event-Listener für automatisches Filtern
+- Text-Eingaben: 300ms Debouncing (verzögerte Suche nach Tipp-Pause)
+- Dropdowns: Sofortiges Filtern bei Änderung
+- "Filter anwenden" Button entfernt, nur "Zurücksetzen" bleibt
+
+**Implementierte Live-Filter:**
+1. **Lizenzübersicht** (licenses.html):
+   - Suchfeld mit Debouncing
+   - Typ-Dropdown (Vollversion/Testversion)
+   - Status-Dropdown (Aktiv/Ablaufend/Abgelaufen/Deaktiviert)
+
+2. **Kundenübersicht** (customers.html):
+   - Suchfeld mit Debouncing
+   - "Suchen" Button entfernt
+
+3. **Audit-Log** (audit_log.html):
+   - Benutzer-Textfeld mit Debouncing
+   - Aktion-Dropdown
+   - Entität-Dropdown
+
+**Technische Details:**
+- `addEventListener('input')` für Textfelder
+- `addEventListener('change')` für Select-Elemente
+- `setTimeout()` mit 300ms für Debouncing
+- Automatisches `form.submit()` bei Änderungen
+
+**Vorteile:**
+- ✅ Schnellere und intuitivere Bedienung
+- ✅ Weniger Klicks erforderlich
+- ✅ Moderne User Experience
+- ✅ Besonders hilfreich bei komplexen Filterkriterien
+
+**Status:**
+- ✅ Live-Filtering auf allen Hauptseiten implementiert
+- ✅ Debouncing verhindert zu viele Server-Requests
+- ✅ Zurücksetzen-Button bleibt für schnelles Löschen aller Filter
+
+### 2025-06-09: Resource Pool System implementiert (Phase 1 & 2)
+
+**Ziel:**
+Ein Pool-System für Domains, IPv4-Adressen und Telefonnummern, wobei bei jeder Lizenzerstellung 1-10 Ressourcen pro Typ zugewiesen werden. Ressourcen haben 3 Status: available, allocated, quarantine.
+
+**Phase 1 - Datenbank-Schema (✅ Abgeschlossen):**
+1. **Neue Tabellen erstellt:**
+   - `resource_pools` - Haupttabelle für alle Ressourcen
+   - `resource_history` - Vollständige Historie aller Aktionen
+   - `resource_metrics` - Performance-Tracking und ROI-Berechnung
+   - `license_resources` - Zuordnung zwischen Lizenzen und Ressourcen
+
+2. **Erweiterte licenses Tabelle:**
+   - `domain_count`, `ipv4_count`, `phone_count` Spalten hinzugefügt
+   - Constraints: 0-10 pro Resource-Typ
+
+3. **Indizes für Performance:**
+   - Status, Type, Allocated License, Quarantine Date
+
+**Phase 2 - Backend-Implementierung (✅ Abgeschlossen):**
+1. **Resource Management Routes:**
+   - `/resources` - Hauptübersicht mit Statistiken
+   - `/resources/add` - Bulk-Import von Ressourcen
+   - `/resources/quarantine/<id>` - Ressourcen sperren
+   - `/resources/release` - Quarantäne aufheben
+   - `/resources/history/<id>` - Komplette Historie
+   - `/resources/metrics` - Performance Dashboard
+   - `/resources/report` - Report-Generator
+
+2. **API-Endpunkte:**
+   - `/api/resources/allocate` - Ressourcen-Zuweisung
+   - `/api/resources/check-availability` - Verfügbarkeit prüfen
+
+3. **Integration in Lizenzerstellung:**
+   - `create_license()` erweitert um Resource-Allocation
+   - `batch_licenses()` mit Ressourcen-Prüfung für gesamten Batch
+   - Transaktionale Sicherheit bei Zuweisung
+
+4. **Dashboard-Integration:**
+   - Resource-Statistiken in Dashboard eingebaut
+   - Warning-Level basierend auf Verfügbarkeit
+
+5. **Navigation erweitert:**
+   - Resources-Link in Navbar hinzugefügt
+
+**Was noch zu tun ist:**
+
+### Phase 3 - UI-Komponenten (🔄 Ausstehend):
+1. **Templates erstellen:**
+   - `resources.html` - Hauptübersicht mit Drag&Drop
+   - `add_resources.html` - Formular für Bulk-Import
+   - `resource_history.html` - Historie-Anzeige
+   - `resource_metrics.html` - Performance Dashboard
+
+2. **Formulare erweitern:**
+   - `index.html` - Resource-Dropdowns hinzufügen
+   - `batch_form.html` - Resource-Dropdowns hinzufügen
+
+3. **Dashboard-Widget:**
+   - Resource Pool Statistik mit Ampelsystem
+   - Warnung bei niedrigem Bestand
+
+### Phase 4 - Erweiterte Features (🔄 Ausstehend):
+1. **Quarantäne-Workflow:**
+   - Gründe: abuse, defect, maintenance, blacklisted, expired
+   - Automatische Tests vor Freigabe
+   - Genehmigungsprozess
+
+2. **Performance-Metrics:**
+   - Täglicher Cronjob für Metriken
+   - ROI-Berechnung
+   - Issue-Tracking
+
+3. **Report-Generator:**
+   - Auslastungsreport
+   - Performance-Report
+   - Compliance-Report
+
+### Phase 5 - Backup erweitern (🔄 Ausstehend):
+- Neue Tabellen in Backup einbeziehen:
+  - resource_pools
+  - resource_history
+  - resource_metrics
+  - license_resources
+
+### Phase 6 - Testing & Migration (🔄 Ausstehend):
+1. **Test-Daten generieren:**
+   - 500 Test-Domains
+   - 200 Test-IPs
+   - 100 Test-Telefonnummern
+
+2. **Migrations-Script:**
+   - Bestehende Lizenzen auf default resource_count setzen
+
+### Phase 7 - Dokumentation (🔄 Ausstehend):
+- API-Dokumentation für License Server
+- Admin-Handbuch für Resource Management
+
+**Technische Details:**
+- 3-Status-System: available/allocated/quarantine
+- Transaktionale Ressourcen-Zuweisung mit FOR UPDATE Lock
+- Vollständige Historie mit IP-Tracking
+- Drag&Drop UI für Resource-Management geplant
+- Automatische Warnung bei < 50 verfügbaren Ressourcen
+
+**Status:**
+- ✅ Datenbank-Schema komplett
+- ✅ Backend-Routen implementiert
+- ✅ Integration in Lizenzerstellung
+- ❌ UI-Templates fehlen noch
+- ❌ Erweiterte Features ausstehend
+- ❌ Testing und Migration offen
+
+### 2025-06-09: Resource Pool System UI-Implementierung (Phase 3 & 4)
+
+**Phase 3 - UI-Komponenten (✅ Abgeschlossen):**
+
+1. **Neue Templates erstellt:**
+   - `resources.html` - Hauptübersicht mit Statistiken, Filter, Live-Suche, Pagination
+   - `add_resources.html` - Bulk-Import Formular mit Validierung
+   - `resource_history.html` - Timeline-Ansicht der Historie mit Details
+   - `resource_metrics.html` - Performance Dashboard mit Charts
+   - `resource_report.html` - Report-Generator UI
+
+2. **Erweiterte Formulare:**
+   - `index.html` - Resource-Count Dropdowns (0-10) mit Live-Verfügbarkeitsprüfung
+   - `batch_form.html` - Resource-Count mit Batch-Berechnung (zeigt Gesamtbedarf)
+   
+3. **Dashboard-Widget:**
+   - Resource Pool Statistik mit Ampelsystem implementiert
+   - Zeigt verfügbare/zugeteilte/quarantäne Ressourcen
+   - Warnung bei niedrigem Bestand (<50)
+   - Fortschrittsbalken für visuelle Darstellung
+
+4. **Backend-Anpassungen:**
+   - `resource_history` Route korrigiert für Object-Style Template-Zugriff
+   - `resources_metrics` Route vollständig implementiert mit Charts-Daten
+   - `resources_report` Route erweitert für Template-Anzeige und Downloads
+   - Dashboard erweitert um Resource-Statistiken
+
+**Phase 4 - Erweiterte Features (✅ Teilweise):**
+1. **Report-Generator:**
+   - Template für Report-Auswahl erstellt
+   - 4 Report-Typen: Usage, Performance, Compliance, Inventory
+   - Export als Excel, CSV oder PDF-Vorschau
+   - Zeitraum-Auswahl mit Validierung
+
+**Technische Details der Implementierung:**
+- Live-Filtering ohne Reload durch JavaScript
+- AJAX-basierte Verfügbarkeitsprüfung
+- Bootstrap 5 für konsistentes Design
+- Chart.js für Metriken-Visualisierung
+- Responsives Design für alle Templates
+- Copy-to-Clipboard für Resource-Werte
+- Modal-Dialoge für Quarantäne-Aktionen
+
+**Was noch fehlt:**
+
+### Phase 5 - Backup erweitern (🔄 Ausstehend):
+- Resource-Tabellen in pg_dump einbeziehen:
+  - resource_pools
+  - resource_history  
+  - resource_metrics
+  - license_resources
+
+### Phase 6 - Testing & Migration (🔄 Ausstehend):
+1. **Test-Daten generieren:**
+   - Script für 500 Test-Domains
+   - 200 Test-IPv4-Adressen
+   - 100 Test-Telefonnummern
+   - Realistische Verteilung über Status
+
+2. **Migrations-Script:**
+   - Bestehende Lizenzen auf Default resource_count setzen
+   - UPDATE licenses SET domain_count=1, ipv4_count=1, phone_count=1 WHERE ...
+
+### Phase 7 - Dokumentation (🔄 Ausstehend):
+- API-Dokumentation für Resource-Endpunkte
+- Admin-Handbuch für Resource Management
+- Troubleshooting-Guide
+
+**Offene Punkte für Produktion:**
+1. Drag&Drop für Resource-Verwaltung (Nice-to-have)
+2. Automatische Quarantäne-Aufhebung nach Zeitablauf
+3. E-Mail-Benachrichtigungen bei niedrigem Bestand
+4. API für externe Resource-Prüfung
+5. Bulk-Delete für Ressourcen
+6. Resource-Import aus CSV/Excel
+
+### 2025-06-09: Resource Pool System finalisiert
+
+**Problem:**
+- Resource Pool System war nur teilweise implementiert
+- UI-Templates waren vorhanden, aber nicht dokumentiert
+- Test-Daten und Migration fehlten
+- Backup-Integration unklar
+
+**Analyse und Lösung:**
+1. **Status-Überprüfung durchgeführt:**
+   - Alle 5 UI-Templates existierten bereits (resources.html, add_resources.html, etc.)
+   - Resource-Dropdowns waren bereits in index.html und batch_form.html integriert
+   - Dashboard-Widget war bereits implementiert
+   - Backup-System inkludiert bereits alle Tabellen (pg_dump ohne -t Parameter)
+
+2. **Fehlende Komponenten erstellt:**
+   - Test-Daten Script: `test_data_resources.sql`
+     - 500 Test-Domains (400 verfügbar, 50 zugeteilt, 50 in Quarantäne)
+     - 200 Test-IPv4-Adressen (150 verfügbar, 30 zugeteilt, 20 in Quarantäne)
+     - 100 Test-Telefonnummern (70 verfügbar, 20 zugeteilt, 10 in Quarantäne)
+     - Resource History und Metrics für realistische Daten
+   
+   - Migration Script: `migrate_existing_licenses.sql`
+     - Setzt Default Resource Counts (Vollversion: 2, Testversion: 1, Inaktiv: 0)
+     - Weist automatisch verfügbare Ressourcen zu
+     - Erstellt Audit-Log Einträge
+     - Gibt detaillierten Migrationsbericht aus
+
+**Neue Dateien:**
+- `v2_adminpanel/test_data_resources.sql` - Testdaten für Resource Pool
+- `v2_adminpanel/migrate_existing_licenses.sql` - Migration für bestehende Lizenzen
+
+**Status:**
+- ✅ Resource Pool System vollständig implementiert und dokumentiert
+- ✅ Alle UI-Komponenten vorhanden und funktionsfähig
+- ✅ Integration in Lizenz-Formulare abgeschlossen
+- ✅ Dashboard-Widget zeigt Resource-Statistiken
+- ✅ Backup-System inkludiert Resource-Tabellen
+- ✅ Test-Daten und Migration bereitgestellt
+
+**Nächste Schritte:**
+1. Test-Daten einspielen: `psql -U adminuser -d meinedatenbank -f test_data_resources.sql`
+2. Migration ausführen: `psql -U adminuser -d meinedatenbank -f migrate_existing_licenses.sql`
+3. License Server API implementieren (Hauptaufgabe)
+
+### 2025-06-09: Bugfix - Resource Pool Tabellen fehlten
+
+**Problem:**
+- Admin Panel zeigte "Internal Server Error"
+- Dashboard Route versuchte auf `resource_pools` Tabelle zuzugreifen
+- Tabelle existierte nicht in der Datenbank
+
+**Ursache:**
+- Bei bereits existierender Datenbank wird init.sql nicht erneut ausgeführt
+- Resource Pool Tabellen wurden erst später zum init.sql hinzugefügt
+- Docker Container verwendeten noch die alte Datenbankstruktur
+
+**Lösung:**
+1. Separates Script `create_resource_tables.sql` erstellt
+2. Script manuell in der Datenbank ausgeführt
+3. Alle 4 Resource-Tabellen erfolgreich erstellt:
+   - resource_pools
+   - resource_history
+   - resource_metrics
+   - license_resources
+
+**Status:**
+- ✅ Admin Panel funktioniert wieder
+- ✅ Dashboard zeigt Resource Pool Statistiken
+- ✅ Alle Resource-Funktionen verfügbar
+
+**Empfehlung für Neuinstallationen:**
+- Bei frischer Installation funktioniert alles automatisch
+- Bei bestehenden Installationen: `create_resource_tables.sql` ausführen
+
+### 2025-06-09: Navigation vereinfacht
+
+**Änderung:**
+- Navigationspunkte aus der schwarzen Navbar entfernt
+- Links zu Lizenzen, Kunden, Ressourcen, Sessions, Backups und Log entfernt
+
+**Grund:**
+- Cleaner Look mit nur Logo, Timer und Logout
+- Alle Funktionen sind weiterhin über das Dashboard erreichbar
+- Bessere Übersichtlichkeit und weniger Ablenkung
+
+**Geänderte Datei:**
+- `v2_adminpanel/templates/base.html` - Navbar-Links auskommentiert
+
+**Status:**
+- ✅ Navbar zeigt nur noch Logo, Session-Timer und Logout
+- ✅ Navigation erfolgt über Dashboard und Buttons auf den jeweiligen Seiten
+- ✅ Alle Funktionen bleiben erreichbar
+
+### 2025-06-09: Bugfix - Resource Report Einrückungsfehler
+
+**Problem:**
+- Resource Report Route zeigte "Internal Server Error"
+- UnboundLocalError: `report_type` wurde verwendet bevor es definiert war
+
+**Ursache:**
+- Fehlerhafte Einrückung in der `resources_report()` Funktion
+- `elif` und `else` Blöcke waren falsch eingerückt
+- Variablen wurden außerhalb ihres Gültigkeitsbereichs verwendet
+
+**Lösung:**
+- Korrekte Einrückung für alle Conditional-Blöcke wiederhergestellt
+- Alle Report-Typen (usage, performance, compliance, inventory) richtig strukturiert
+- Excel und CSV Export-Code korrekt eingerückt
+
+**Geänderte Datei:**
+- `v2_adminpanel/app.py` - resources_report() Funktion korrigiert
+
+**Status:**
+- ✅ Resource Report funktioniert wieder
+- ✅ Alle 4 Report-Typen verfügbar
+- ✅ Export als Excel und CSV möglich
+
+---
+
+## Zusammenfassung der heutigen Arbeiten (2025-06-09)
+
+### 1. Resource Pool System Finalisierung
+- **Ausgangslage**: Resource Pool war nur teilweise dokumentiert
+- **Überraschung**: UI-Templates waren bereits vorhanden (nicht dokumentiert)
+- **Ergänzt**: 
+  - Test-Daten Script (`test_data_resources.sql`)
+  - Migration Script (`migrate_existing_licenses.sql`)
+- **Status**: ✅ Vollständig implementiert
+
+### 2. Database Migration Bug
+- **Problem**: Admin Panel zeigte "Internal Server Error"
+- **Ursache**: Resource Pool Tabellen fehlten in bestehender DB
+- **Lösung**: Separates Script `create_resource_tables.sql` erstellt
+- **Status**: ✅ Behoben
+
+### 3. UI Cleanup
+- **Änderung**: Navigation aus Navbar entfernt
+- **Effekt**: Cleaner Look, Navigation nur über Dashboard
+- **Status**: ✅ Implementiert
+
+### 4. Resource Report Bug
+- **Problem**: Einrückungsfehler in `resources_report()` Funktion
+- **Lösung**: Korrekte Einrückung wiederhergestellt
+- **Status**: ✅ Behoben
+
+### Neue Dateien erstellt heute:
+1. `v2_adminpanel/test_data_resources.sql` - 800 Test-Ressourcen
+
+### 2025-06-09: Bugfix - Resource Quarantäne Modal
+
+**Problem:**
+- Quarantäne-Button funktionierte nicht
+- Modal öffnete sich nicht beim Klick
+
+**Ursache:**
+- Bootstrap 5 vs Bootstrap 4 API-Inkompatibilität
+- Modal wurde mit Bootstrap 4 Syntax (`modal.modal('show')`) aufgerufen
+- jQuery wurde nach Bootstrap geladen
+
+**Lösung:**
+1. **JavaScript angepasst:**
+   - Von jQuery Modal-API zu nativer Bootstrap 5 Modal-API gewechselt
+   - `new bootstrap.Modal(element).show()` statt `$(element).modal('show')`
+
+2. **HTML-Struktur aktualisiert:**
+   - Modal-Close-Button: `data-bs-dismiss="modal"` statt `data-dismiss="modal"`
+   - `btn-close` Klasse statt custom close button
+   - Form-Klassen: `mb-3` statt `form-group`, `form-select` statt `form-control` für Select
+
+3. **Script-Reihenfolge korrigiert:**
+   - jQuery vor Bootstrap laden für korrekte Initialisierung
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/resources.html`
+- `v2_adminpanel/templates/base.html`
+
+**Status:** ✅ Behoben
+
+### 2025-06-09: Resource Pool UI Redesign
+
+**Ziel:**
+- Komplettes Redesign des Resource Pool Managements für bessere Benutzerfreundlichkeit
+- Konsistentes Design mit dem Rest der Anwendung
+
+**Durchgeführte Änderungen:**
+
+1. **resources.html - Hauptübersicht:**
+   - Moderne Statistik-Karten mit Hover-Effekten
+   - Farbcodierte Progress-Bars mit Tooltips
+   - Verbesserte Tabelle mit Icons und Status-Badges
+   - Live-Filter mit sofortiger Suche
+   - Überarbeitete Quarantäne-Modal für Bootstrap 5
+   - Responsive Design mit Grid-Layout
+
+2. **add_resources.html - Ressourcen hinzufügen:**
+   - 3-Schritt Wizard-ähnliches Interface
+   - Visueller Ressourcentyp-Selector mit Icons
+   - Live-Validierung mit Echtzeit-Feedback
+   - Statistik-Anzeige (Gültig/Duplikate/Ungültig)
+   - Formatierte Beispiele mit Erklärungen
+   - Verbesserte Fehlerbehandlung
+
+3. **resource_history.html - Historie:**
+   - Zentrierte Resource-Anzeige mit großen Icons
+   - Info-Grid Layout für Details
+   - Modernisierte Timeline mit Hover-Effekten
+   - Farbcodierte Action-Icons
+   - Verbesserte Darstellung von Details
+
+4. **resource_metrics.html - Metriken:**
+   - Dashboard-Style Metrik-Karten mit Icon-Badges
+   - Modernisierte Charts mit besseren Farben
+   - Performance-Tabellen mit Progress-Bars
+   - Trend-Indikatoren für Performance
+   - Responsives Grid-Layout
+
+**Design-Verbesserungen:**
+- Konsistente Emoji-Icons für bessere visuelle Kommunikation
+- Einheitliche Farbgebung (Blau/Lila/Grün für Ressourcentypen)
+- Card-basiertes Layout mit Schatten und Hover-Effekten
+- Bootstrap 5 kompatible Komponenten
+- Verbesserte Typografie und Spacing
+
+**Technische Details:**
+- Bootstrap 5 Modal-API statt jQuery
+- CSS Grid für responsive Layouts
+- Moderne Chart.js Konfiguration
+- Optimierte JavaScript-Validierung
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/resources.html`
+- `v2_adminpanel/templates/add_resources.html`
+- `v2_adminpanel/templates/resource_history.html`
+- `v2_adminpanel/templates/resource_metrics.html`
+
+**Status:** ✅ Abgeschlossen
+
+### 2025-06-09: Zusammenfassung der heutigen Arbeiten
+
+**Durchgeführte Aufgaben:**
+
+1. **Quarantäne-Funktion repariert:**
+   - Bootstrap 5 Modal-API implementiert
+   - data-bs-dismiss statt data-dismiss
+   - jQuery vor Bootstrap laden
+
+2. **Resource Pool UI komplett überarbeitet:**
+   - Alle 4 Templates modernisiert (resources, add_resources, resource_history, resource_metrics)
+   - Konsistentes Design mit Emoji-Icons
+   - Einheitliche Farbgebung (Blau/Lila/Grün)
+   - Bootstrap 5 kompatible Komponenten
+   - Responsive Grid-Layouts
+
+**Aktuelle Projekt-Status:**
+- ✅ Admin Panel voll funktionsfähig
+- ✅ Resource Pool Management mit modernem UI
+- ✅ PostgreSQL mit allen Tabellen
+- ✅ Nginx Reverse Proxy mit SSL
+- ❌ Lizenzserver noch nicht implementiert (nur Platzhalter)
+
+**Nächste Schritte:**
+- Lizenzserver implementieren
+- API-Endpunkte für Lizenzvalidierung
+- Heartbeat-System für Sessions
+- Versionsprüfung implementieren
+1. `v2_adminpanel/templates/base.html` - Navigation entfernt
+2. `v2_adminpanel/app.py` - Resource Report Einrückung korrigiert
+3. `JOURNAL.md` - Alle Änderungen dokumentiert
+
+### Offene Hauptaufgabe:
+- **License Server API** - Noch komplett zu implementieren
+  - `/api/version` - Versionscheck
+  - `/api/validate` - Lizenzvalidierung  
+  - `/api/heartbeat` - Session-Management
+
+### 2025-06-09: Resource Pool Internal Error behoben
+
+**Problem:**
+- Internal Server Error beim Zugriff auf `/resources`
+- NameError: name 'datetime' is not defined in Template
+
+**Ursache:**
+- Fehlende `datetime` und `timedelta` Objekte im Template-Kontext
+- Falsche Array-Indizes in resources.html für activity-Daten
+
+**Lösung:**
+1. **app.py (Zeile 2797-2798):**
+   - `datetime=datetime` und `timedelta=timedelta` zu render_template hinzugefügt
+   
+2. **resources.html (Zeile 484-490):**
+   - Array-Indizes korrigiert:
+     - activity[0] = action
+     - activity[1] = action_by
+     - activity[2] = action_at
+     - activity[3] = resource_type
+     - activity[4] = resource_value
+     - activity[5] = details
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py`
+- `v2_adminpanel/templates/resources.html`
+
+**Status:** ✅ Behoben - Resource Pool funktioniert wieder einwandfrei
+
+### 2025-06-09: Passwort-Änderung und 2FA implementiert
+
+**Ziel:**
+- Benutzer können ihr Passwort ändern
+- Zwei-Faktor-Authentifizierung (2FA) mit TOTP
+- Komplett kostenlose Lösung ohne externe Services
+
+**Implementierte Features:**
+
+1. **Datenbank-Erweiterung:**
+   - Neue `users` Tabelle mit Passwort-Hash und 2FA-Feldern
+   - Unterstützung für TOTP-Secrets und Backup-Codes
+   - Migration von Environment-Variablen zu Datenbank
+
+2. **Passwort-Management:**
+   - Sichere Passwort-Hashes mit bcrypt
+   - Passwort-Änderung mit Verifikation des alten Passworts
+   - Passwort-Stärke-Indikator im Frontend
+
+3. **2FA-Implementation:**
+   - TOTP-basierte 2FA (Google Authenticator, Authy kompatibel)
+   - QR-Code-Generierung für einfaches Setup
+   - 8 Backup-Codes für Notfallzugriff
+   - Backup-Codes als Textdatei downloadbar
+
+4. **Neue Routen:**
+   - `/profile` - Benutzerprofil mit Passwort und 2FA-Verwaltung
+   - `/verify-2fa` - 2FA-Verifizierung beim Login
+   - `/profile/setup-2fa` - 2FA-Einrichtung mit QR-Code
+   - `/profile/enable-2fa` - 2FA-Aktivierung
+   - `/profile/disable-2fa` - 2FA-Deaktivierung
+   - `/profile/change-password` - Passwort ändern
+
+5. **Sicherheits-Features:**
+   - Fallback zu Environment-Variablen für Rückwärtskompatibilität
+   - Session-Management für 2FA-Verifizierung
+   - Fehlgeschlagene 2FA-Versuche werden protokolliert
+   - Verwendete Backup-Codes werden entfernt
+
+**Verwendete Libraries (alle kostenlos):**
+- `bcrypt` - Passwort-Hashing
+- `pyotp` - TOTP-Generierung und Verifizierung
+- `qrcode[pil]` - QR-Code-Generierung
+
+**Migration:**
+- Script `migrate_users.py` erstellt für Migration existierender Benutzer
+- Erhält bestehende Credentials aus Environment-Variablen
+- Erstellt Datenbank-Einträge mit gehashten Passwörtern
+
+**Geänderte Dateien:**
+- `v2_adminpanel/init.sql` - Users-Tabelle hinzugefügt
+- `v2_adminpanel/requirements.txt` - Neue Dependencies
+- `v2_adminpanel/app.py` - Auth-Funktionen und neue Routen
+- `v2_adminpanel/migrate_users.py` - Migrations-Script (neu)
+- `v2_adminpanel/templates/base.html` - Profil-Link hinzugefügt
+- `v2_adminpanel/templates/profile.html` - Profil-Seite (neu)
+- `v2_adminpanel/templates/verify_2fa.html` - 2FA-Verifizierung (neu)
+- `v2_adminpanel/templates/setup_2fa.html` - 2FA-Setup (neu)
+- `v2_adminpanel/templates/backup_codes.html` - Backup-Codes Anzeige (neu)
+
+**Status:** ✅ Vollständig implementiert
+
+### 2025-06-09: Internal Server Error behoben und UI-Design angepasst
+
+### 2025-06-09: Journal-Bereinigung und Projekt-Cleanup
+
+**Durchgeführte Aufgaben:**
+
+1. **Überflüssige SQL-Dateien gelöscht:**
+   - `create_resource_tables.sql` - War nur für Migrations nötig
+   - `migrate_existing_licenses.sql` - Keine alten Installationen vorhanden
+   - `sample_data.sql` - Testdaten nicht mehr benötigt
+   - `test_data_resources.sql` - Testdaten nicht mehr benötigt
+
+2. **Journal aktualisiert:**
+   - Veraltete Todo-Liste korrigiert (viele Features bereits implementiert)
+   - Passwörter aus Zugangsdaten entfernt (Sicherheit)
+   - "Bekannte Probleme" auf aktuellen Stand gebracht
+   - Neuer Abschnitt "Best Practices für Produktiv-Migration" hinzugefügt
+
+3. **Status-Klärungen:**
+   - Alle Daten sind Testdaten (PoC-Phase)
+   - 2FA ist implementiert und funktionsfähig
+   - Resource Pool System ist vollständig implementiert
+   - Port 8443 ist geschlossen (nur über Nginx erreichbar)
+
+**Noch zu erledigen:**
+- Nginx Config anpassen (proxy_pass von https:// auf http://)
+- License Server API implementieren (Hauptaufgabe)
+
+**Problem:**
+- Internal Server Error nach Login wegen fehlender `users` Tabelle
+- UI-Design der neuen 2FA-Seiten passte nicht zum Rest der Anwendung
+
+**Lösung:**
+
+1. **Datenbank-Fix:**
+   - Users-Tabelle wurde nicht automatisch erstellt
+   - Manuell mit SQL-Script nachgeholt
+   - Migration erfolgreich durchgeführt
+   - Beide Admin-User (rac00n, w@rh@mm3r) migriert
+
+2. **UI-Design Überarbeitung:**
+   - Profile-Seite im Dashboard-Stil mit Cards und Hover-Effekten
+   - 2FA-Setup mit nummerierten Schritten und modernem Card-Design
+   - Backup-Codes Seite mit Animation und verbessertem Layout
+   - Konsistente Farbgebung und Icons
+   - Verbesserte Benutzerführung mit visuellen Hinweisen
+
+**Design-Features:**
+- Card-basiertes Layout mit Schatten-Effekten
+- Hover-Animationen für bessere Interaktivität
+- Farbcodierte Sicherheitsstatus-Anzeigen
+- Passwort-Stärke-Indikator mit visueller Rückmeldung
+- Responsive Design für alle Bildschirmgrößen
+- Print-optimiertes Layout für Backup-Codes
+
+**Geänderte Dateien:**
+- `v2_adminpanel/create_users_table.sql` - SQL für Users-Tabelle (temporär)
+
+### 2025-06-09: Journal-Umstrukturierung
+
+**Durchgeführte Änderungen:**
+
+1. **Dokumentation aufgeteilt:**
+   - `JOURNAL.md` - Enthält nur noch chronologische Änderungen (wie ein Tagebuch)
+   - `THE_ROAD_SO_FAR.md` - Neues Dokument mit aktuellem Status und Roadmap
+   
+2. **THE_ROAD_SO_FAR.md erstellt mit:**
+   - Aktueller Status (was läuft bereits)
+   - Nächste Schritte (Priorität Hoch)
+   - Offene Aufgaben (Priorität Mittel)
+   - Nice-to-have Features
+   - Bekannte Probleme
+   - Deployment-Hinweise
+
+3. **JOURNAL.md bereinigt:**
+   - Todo-Listen entfernt (jetzt in THE_ROAD_SO_FAR.md)
+   - Nur noch chronologische Einträge
+   - Fokus auf "Was wurde gemacht" statt "Was muss gemacht werden"
+
+**Vorteile der Aufteilung:**
+- Journal bleibt übersichtlich und wächst linear
+- Status und Todos sind immer aktuell an einem Ort
+- Klare Trennung zwischen Historie und Planung
+- Einfacher für neue Entwickler einzusteigen
+
+### 2025-06-09: Nginx Config angepasst
+
+**Änderung:**
+- proxy_pass für License Server von `https://license-server:8443` auf `http://license-server:8443` geändert
+- `proxy_ssl_verify off` entfernt (nicht mehr nötig bei HTTP)
+- WebSocket-Support hinzugefügt (für zukünftige Features)
+
+**Grund:**
+- License Server läuft intern auf HTTP (wie Admin Panel)
+- SSL-Termination erfolgt nur am Nginx
+- Vereinfachte Konfiguration ohne doppelte SSL-Verschlüsselung
+
+**Hinweis:** 
+Docker-Container müssen neu gestartet werden, damit die Änderung wirksam wird:
+```bash
+docker-compose down
+docker-compose up -d
+```
+- `v2_adminpanel/templates/profile.html` - Komplett überarbeitet
+- `v2_adminpanel/templates/setup_2fa.html` - Neues Step-by-Step Design
+- `v2_adminpanel/templates/backup_codes.html` - Modernisiertes Layout
+
+**Status:** ✅ Abgeschlossen - Login funktioniert, UI im konsistenten Design
+
+### 2025-06-09: Lizenzschlüssel-Format geändert
+
+**Änderung:**
+- Altes Format: `AF-YYYYMMFT-XXXX-YYYY-ZZZZ` (z.B. AF-202506F-V55Y-9DWE-GL5G)
+- Neues Format: `AF-F-YYYYMM-XXXX-YYYY-ZZZZ` (z.B. AF-F-202506-V55Y-9DWE-GL5G)
+
+**Vorteile:**
+- Klarere Struktur mit separatem Typ-Indikator
+- Einfacher zu lesen und zu verstehen
+- Typ (F/T) sofort im zweiten Block erkennbar
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py`:
+  - `generate_license_key()` - Generiert Keys im neuen Format
+  - `validate_license_key()` - Validiert Keys mit neuem Regex-Pattern
+- `v2_adminpanel/templates/index.html`:
+  - Placeholder und Pattern für Input-Feld angepasst
+  - JavaScript charAt() Position für Typ-Prüfung korrigiert
+- `v2_adminpanel/templates/batch_form.html`:
+  - Vorschau-Format für Batch-Generierung angepasst
+
+**Hinweis:** Alte Keys im bisherigen Format bleiben ungültig. Bei Bedarf könnte eine Migration oder Dual-Support implementiert werden.
+
+**Status:** ✅ Implementiert
+
+### 2025-06-09: Datenbank-Migration der Lizenzschlüssel
+
+**Durchgeführt:**
+- Alle bestehenden Lizenzschlüssel in der Datenbank auf das neue Format migriert
+- 18 Lizenzschlüssel erfolgreich konvertiert (16 Full, 2 Test)
+
+**Migration:**
+- Von: `AF-YYYYMMFT-XXXX-YYYY-ZZZZ`
+- Nach: `AF-F-YYYYMM-XXXX-YYYY-ZZZZ`
+
+**Beispiele:**
+- Alt: `AF-202506F-V55Y-9DWE-GL5G`
+- Neu: `AF-F-202506-V55Y-9DWE-GL5G`
+
+**Geänderte Dateien:**
+- `v2_adminpanel/migrate_license_keys.sql` - Migrations-Script (temporär)
+- `v2_adminpanel/fix_license_keys.sql` - Korrektur-Script (temporär)
+
+**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert
+
+### 2025-06-09: Kombinierte Kunden-Lizenz-Ansicht implementiert
+
+**Problem:**
+- Umständliche Navigation zwischen Kunden- und Lizenzseiten
+- Viel Hin-und-Her-Springen bei der Verwaltung
+- Kontext-Verlust beim Wechseln zwischen Ansichten
+
+**Lösung:**
+Master-Detail View mit 2-Spalten Layout implementiert
+
+**Phase 1-3 abgeschlossen:**
+1. **Backend-Implementierung:**
+   - Neue Route `/customers-licenses` für kombinierte Ansicht
+   - API-Endpoints für AJAX: `/api/customer/<id>/licenses`, `/api/customer/<id>/quick-stats`
+   - API-Endpoint `/api/license/<id>/quick-edit` für Inline-Bearbeitung
+   - Optimierte SQL-Queries mit JOIN für Performance
+
+2. **Template-Erstellung:**
+   - Neues Template `customers_licenses.html` mit Master-Detail Layout
+   - Links: Kundenliste (30%) mit Suchfeld
+   - Rechts: Lizenzen des ausgewählten Kunden (70%)
+   - Responsive Design (Mobile: untereinander)
+   - JavaScript für dynamisches Laden ohne Seitenreload
+   - Keyboard-Navigation (↑↓ für Kundenwechsel)
+
+3. **Integration:**
+   - Dashboard: Neuer Button "Kunden & Lizenzen"
+   - Customers-Seite: Link zur kombinierten Ansicht
+   - Licenses-Seite: Link zur kombinierten Ansicht
+   - Lizenz-Erstellung: Unterstützung für vorausgewählten Kunden
+   - API /api/customers erweitert für Einzelabruf per ID
+
+**Features:**
+- Live-Suche in Kundenliste
+- Quick-Actions: Copy License Key, Toggle Status
+- Modal für neue Lizenz direkt aus Kundenansicht
+- URL-Update ohne Reload für Bookmarking
+- Loading-States während AJAX-Calls
+- Visuelles Feedback (aktiver Kunde hervorgehoben)
+
+**Noch ausstehend:**
+- Phase 4: Inline-Edit für Lizenzdetails
+- Phase 5: Erweiterte Error-Handling und Polish
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py` - Neue Routen und API-Endpoints
+- `v2_adminpanel/templates/customers_licenses.html` - Neues Template
+- `v2_adminpanel/templates/dashboard.html` - Neuer Button
+- `v2_adminpanel/templates/customers.html` - Link zur kombinierten Ansicht
+- `v2_adminpanel/templates/licenses.html` - Link zur kombinierten Ansicht
+- `v2_adminpanel/templates/index.html` - Unterstützung für preselected_customer_id
+
+**Status:** ✅ Grundfunktionalität implementiert und funktionsfähig
+
+### 2025-06-09: Kombinierte Ansicht - Fertigstellung und TODOs aktualisiert
+
+**Abgeschlossen:**
+- Phase 1-3 der kombinierten Kunden-Lizenz-Ansicht vollständig implementiert
+- Master-Detail Layout funktioniert einwandfrei
+- AJAX-basiertes Laden ohne Seitenreload
+- Keyboard-Navigation mit Pfeiltasten
+- Quick-Actions für Copy und Toggle Status
+- Integration in alle relevanten Seiten
+
+**THE_ROAD_SO_FAR.md aktualisiert:**
+- Kombinierte Ansicht als "Erledigt" markiert
+- Von "In Arbeit" zu "Abgeschlossen" verschoben
+- Status dokumentiert
+
+**Verbesserung gegenüber vorher:**
+- Kein Hin-und-Her-Springen mehr zwischen Seiten
+- Kontext bleibt erhalten beim Arbeiten mit Kunden
+- Schnellere Navigation und bessere Übersicht
+- Deutlich verbesserte User Experience
+
+**Optional für später (Phase 4-5):**
+- Inline-Edit für weitere Felder
+- Erweiterte Quick-Actions
+- Session-basierte Filter-Persistenz
+
+Die Hauptproblematik der umständlichen Navigation ist damit gelöst!
+
+### 2025-06-09: Test-Flag für Lizenzen implementiert
+
+**Ziel:**
+- Klare Trennung zwischen Testdaten und echten Produktivdaten
+- Testdaten sollen von der Software ignoriert werden können
+- Bessere Übersicht im Admin Panel
+
+**Durchgeführte Änderungen:**
+
+1. **Datenbank-Schema (init.sql):**
+   - Neue Spalte `is_test BOOLEAN DEFAULT FALSE` zur `licenses` Tabelle hinzugefügt
+   - Migration für bestehende Daten: Alle werden als `is_test = TRUE` markiert
+   - Index `idx_licenses_is_test` für bessere Performance
+
+2. **Backend (app.py):**
+   - Dashboard-Queries filtern Testdaten mit `WHERE is_test = FALSE` aus
+   - Lizenz-Erstellung: Neues Checkbox-Feld für Test-Markierung
+   - Lizenz-Bearbeitung: Test-Status kann geändert werden
+   - Export: Optional mit/ohne Testdaten (`?include_test=true`)
+   - Bulk-Operationen: Nur auf Live-Daten anwendbar
+   - Neue Filter in Lizenzliste: "🧪 Testdaten" und "🚀 Live-Daten"
+
+3. **Frontend Templates:**
+   - **index.html**: Checkbox "Als Testdaten markieren" bei Lizenzerstellung
+   - **edit_license.html**: Checkbox zum Ändern des Test-Status
+   - **licenses.html**: Badge 🧪 für Testdaten, neue Filteroptionen
+   - **dashboard.html**: Info-Box zeigt Anzahl der Testdaten
+   - **batch_form.html**: Option für Batch-Test-Lizenzen
+
+4. **Audit-Log Integration:**
+   - `is_test` Feld wird bei CREATE/UPDATE geloggt
+   - Nachvollziehbarkeit von Test/Live-Status-Änderungen
+
+**Technische Details:**
+- Testdaten werden in allen Statistiken ausgefiltert
+- License Server API wird Lizenzen mit `is_test = TRUE` ignorieren
+- Resource Pool bleibt unverändert (kann Test- und Live-Ressourcen verwalten)
+
+**Migration der bestehenden Daten:**
+```sql
+UPDATE licenses SET is_test = TRUE;  -- Alle aktuellen Daten sind Testdaten
+```
+
+**Status:** ✅ Implementiert
+
+### 2025-06-09: Test-Flag für Kunden und Resource Pools erweitert
+
+**Ziel:**
+- Konsistentes Test-Daten-Management über alle Entitäten
+- Kunden und Resource Pools können ebenfalls als Testdaten markiert werden
+- Automatische Verknüpfung: Test-Kunde → Test-Lizenzen → Test-Ressourcen
+
+**Durchgeführte Änderungen:**
+
+1. **Datenbank-Schema erweitert:**
+   - `customers.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
+   - `resource_pools.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
+   - Indizes für bessere Performance erstellt
+   - Migrations in init.sql integriert
+
+2. **Backend (app.py) - Erweiterte Logik:**
+   - Dashboard: Separate Zähler für Test-Kunden und Test-Ressourcen
+   - Kunde-Erstellung: Erbt Test-Status von Lizenz
+   - Test-Kunde erzwingt Test-Lizenzen
+   - Resource-Zuweisung: Test-Lizenzen bekommen nur Test-Ressourcen
+   - Customer-Management mit is_test Filter
+
+3. **Frontend Updates:**
+   - **customers.html**: 🧪 Badge für Test-Kunden
+   - **edit_customer.html**: Checkbox für Test-Status
+   - **dashboard.html**: Erweiterte Test-Statistik (Lizenzen, Kunden, Ressourcen)
+
+4. **Geschäftslogik:**
+   - Wenn neuer Kunde bei Test-Lizenz erstellt wird → automatisch Test-Kunde
+   - Wenn Test-Kunde gewählt wird → Lizenz automatisch als Test markiert
+   - Resource Pool Allocation prüft Test-Status für korrekte Zuweisung
+
+**Migration der bestehenden Daten:**
+```sql
+UPDATE customers SET is_test = TRUE;       -- 5 Kunden
+UPDATE resource_pools SET is_test = TRUE;  -- 20 Ressourcen
+```
+
+**Technische Details:**
+- Konsistente Test/Live-Trennung über alle Ebenen
+- Dashboard-Statistiken zeigen nur Live-Daten
+- Test-Ressourcen werden nur Test-Lizenzen zugewiesen
+- Alle bestehenden Daten sind jetzt als Test markiert
+
+**Status:** ✅ Vollständig implementiert
+
+### 2025-06-09 (17:20 - 18:13): Kunden-Lizenz-Verwaltung konsolidiert
+
+**Problem:**
+- Kombinierte Ansicht `/customers-licenses` hatte Formatierungs- und Funktionsprobleme
+- Kunden wurden nicht angezeigt
+- Bootstrap Icons fehlten
+- JavaScript-Fehler beim Modal
+- Inkonsistentes Design im Vergleich zu anderen Seiten
+- Testkunden-Filter wurde beim Navigieren nicht beibehalten
+
+**Durchgeführte Änderungen:**
+
+1. **Frontend-Fixes (base.html):**
+   - Bootstrap Icons CSS hinzugefügt: `bootstrap-icons@1.11.3/font/bootstrap-icons.min.css`
+   - Bootstrap JavaScript Bundle bereits vorhanden, Reihenfolge optimiert
+
+2. **customers_licenses.html komplett überarbeitet:**
+   - Container-Klasse von `container-fluid` auf `container py-5` geändert
+   - Emojis und Button-Styling vereinheitlicht (👥 Kunden & Lizenzen)
+   - Export-Dropdown wie in anderen Ansichten implementiert
+   - Card-Styling mit Schatten für einheitliches Design
+   - Checkbox "Testkunden anzeigen" mit Status-Beibehaltung
+   - JavaScript-Funktionen korrigiert:
+     - copyToClipboard mit event.currentTarget
+     - showNewLicenseModal mit Bootstrap Modal
+     - Header-Update beim AJAX-Kundenwechsel
+   - URL-Parameter `show_test` wird überall beibehalten
+
+3. **Backend-Anpassungen (app.py):**
+   - customers_licenses Route: Optional Testkunden anzeigen mit `show_test` Parameter
+   - Redirects von `/customers` und `/licenses` auf `/customers-licenses` implementiert
+   - Alte Route-Funktionen entfernt (kein toter Code mehr)
+   - edit_license und edit_customer: Redirects behalten show_test Parameter bei
+   - Dashboard-Links zeigen jetzt auf kombinierte Ansicht
+
+4. **Navigation optimiert:**
+   - Dashboard: Klick auf Kunden/Lizenzen-Statistik führt zur kombinierten Ansicht
+   - Alle Edit-Links behalten den show_test Parameter bei
+   - Konsistente User Experience beim Navigieren
+
+**Technische Details:**
+- AJAX-Loading für dynamisches Laden der Lizenzen
+- Keyboard-Navigation (↑↓) für Kundenliste
+- Responsive Design mit Bootstrap Grid
+- Modal-Dialoge für Bestätigungen
+- Live-Suche in der Kundenliste
+
+**Resultat:**
+- ✅ Einheitliches Design mit anderen Admin-Panel-Seiten
+- ✅ Alle Funktionen arbeiten korrekt
+- ✅ Testkunden-Filter bleibt erhalten
+- ✅ Keine redundanten Views mehr
+- ✅ Zentrale Verwaltung für Kunden und Lizenzen
+
+**Status:** ✅ Vollständig implementiert
+
+### 2025-06-09: Test-Daten Checkbox Persistenz implementiert
+
+**Problem:**
+- Die "Testkunden anzeigen" Checkbox in `/customers-licenses` verlor ihren Status beim Navigieren zwischen Seiten
+- Wenn Benutzer zu anderen Seiten (Resources, Audit Log, etc.) wechselten und zurückkehrten, war die Checkbox wieder deaktiviert
+- Benutzer mussten die Checkbox jedes Mal neu aktivieren, was umständlich war
+
+**Lösung:**
+- Globale JavaScript-Funktion `preserveShowTestParameter()` in base.html implementiert
+- Die Funktion prüft beim Laden jeder Seite, ob `show_test=true` in der URL ist
+- Wenn ja, wird dieser Parameter automatisch an alle internen Links angehängt
+- Backend-Route `/create` wurde angepasst, um den Parameter bei Redirects beizubehalten
+
+**Technische Details:**
+1. **base.html** - JavaScript-Funktion hinzugefügt:
+   - Läuft beim `DOMContentLoaded` Event
+   - Findet alle Links die mit "/" beginnen
+   - Fügt `show_test=true` Parameter hinzu wenn nicht bereits vorhanden
+   - Überspringt Fragment-Links (#) und Links die bereits den Parameter haben
+
+2. **app.py** - Route-Anpassung:
+   - `/create` Route behält jetzt `show_test` Parameter bei Redirects bei
+   - Andere Routen (edit_license, edit_customer) behalten Parameter bereits bei
+
+**Vorteile:**
+- ✅ Konsistente User Experience beim Navigieren
+- ✅ Keine manuelle Anpassung aller Links nötig
+- ✅ Funktioniert automatisch für alle zukünftigen Links
+- ✅ Minimaler Code-Overhead
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/base.html`
+- `v2_adminpanel/app.py`
+
+**Status:** ✅ Vollständig implementiert
+
+### 2025-06-09: Bearbeiten-Button Fehler behoben
+
+**Problem:**
+- Der "Bearbeiten" Button neben dem Kundennamen in der `/customers-licenses` Ansicht verursachte einen Internal Server Error
+- Die URL-Konstruktion war fehlerhaft wenn kein `show_test` Parameter vorhanden war
+- Die edit_customer.html Template hatte falsche Array-Indizes und veraltete Links
+
+**Ursache:**
+1. Die href-Attribute wurden falsch konstruiert:
+   - Alt: `/customer/edit/ID{% if show_test %}?ref=customers-licenses&show_test=true{% endif %}`
+   - Problem: Ohne show_test fehlte das `?ref=customers-licenses` komplett
+
+2. Die SQL-Abfrage in edit_customer() holte nur 4 Felder, aber das Template erwartete 5:
+   - Query: `SELECT id, name, email, is_test`
+   - Template erwartete: `customer[3]` = created_at und `customer[4]` = is_test
+
+3. Veraltete Links zu `/customers` statt `/customers-licenses`
+
+**Lösung:**
+1. URL-Konstruktion korrigiert in beiden Fällen:
+   - Neu: `/customer/edit/ID?ref=customers-licenses{% if show_test %}&show_test=true{% endif %}`
+   
+2. SQL-Query erweitert um created_at:
+   - Neu: `SELECT id, name, email, created_at, is_test`
+   
+3. Template-Indizes korrigiert:
+   - is_test Checkbox nutzt jetzt `customer[4]`
+   
+4. Navigation-Links aktualisiert:
+   - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/customers_licenses.html` (Zeilen 103 und 295)
+- `v2_adminpanel/app.py` (edit_customer Route)
+- `v2_adminpanel/templates/edit_customer.html`
+
+**Status:** ✅ Behoben
+
+### 2025-06-09: Unnötigen Lizenz-Erstellungs-Popup entfernt
+
+**Änderung:**
+- Der Bestätigungs-Popup "Möchten Sie eine neue Lizenz für KUNDENNAME erstellen?" wurde entfernt
+- Klick auf "Neue Lizenz" Button führt jetzt direkt zur Lizenzerstellung
+
+**Technische Details:**
+- Modal-HTML komplett entfernt
+- `showNewLicenseModal()` Funktion vereinfacht - navigiert jetzt direkt zu `/create?customer_id=X`
+- URL-Parameter (wie `show_test`) werden dabei beibehalten
+
+**Vorteile:**
+- ✅ Ein Klick weniger für Benutzer
+- ✅ Schnellerer Workflow
+- ✅ Weniger Code zu warten
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/customers_licenses.html`
+
+**Status:** ✅ Implementiert
+
+### 2025-06-09: Testkunden-Checkbox bleibt jetzt bei Lizenz/Kunden-Bearbeitung erhalten
+
+**Problem:**
+- Bei "Lizenz bearbeiten" ging der "Testkunden anzeigen" Haken verloren beim Zurückkehren
+- Die Navigation-Links in edit_license.html zeigten auf `/licenses` statt `/customers-licenses`
+- Der show_test Parameter wurde nur über den unsicheren Referrer übertragen
+
+**Lösung:**
+1. **Navigation-Links korrigiert**:
+   - Alle Links zeigen jetzt auf `/customers-licenses` mit show_test Parameter
+   - Betrifft: "Zurück zur Übersicht" und "Abbrechen" Buttons
+
+2. **Hidden Form Field hinzugefügt**:
+   - Sowohl in edit_license.html als auch edit_customer.html
+   - Überträgt den show_test Parameter sicher beim POST
+
+3. **Route-Logik verbessert**:
+   - Parameter wird aus Form-Daten ODER GET-Parametern gelesen
+   - Nicht mehr auf unsicheren Referrer angewiesen
+   - Funktioniert sowohl bei Speichern als auch Abbrechen
+
+**Technische Details:**
+- Templates prüfen `request.args.get('show_test')` für Navigation
+- Hidden Input: `<input type="hidden" name="show_test" value="true">`
+- Routes: `show_test = request.form.get('show_test') or request.args.get('show_test')`
+
+**Geänderte Dateien:**
+- `v2_adminpanel/templates/edit_license.html`
+- `v2_adminpanel/templates/edit_customer.html`
+- `v2_adminpanel/app.py` (edit_license und edit_customer Routen)
+
+**Status:** ✅ Vollständig implementiert
+
+### 2025-06-09 22:02: Konsistente Sortierung bei Status-Toggle
+
+**Problem:**
+- Beim Klicken auf den An/Aus-Knopf (Status-Toggle) in der Kunden & Lizenzen Ansicht änderte sich die Reihenfolge der Lizenzen
+- Dies war verwirrend für Benutzer, da die Position der gerade bearbeiteten Lizenz springen konnte
+
+**Ursache:**
+- Die Sortierung `ORDER BY l.created_at DESC` war nicht stabil genug
+- Bei gleichem Erstellungszeitpunkt konnte die Datenbank die Reihenfolge inkonsistent zurückgeben
+
+**Lösung:**
+- Sekundäres Sortierkriterium hinzugefügt: `ORDER BY l.created_at DESC, l.id DESC`
+- Dies stellt sicher, dass bei gleichem Erstellungsdatum nach ID sortiert wird
+- Die Reihenfolge bleibt jetzt konsistent, auch nach Status-Änderungen
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py`:
+  - Zeile 2278: `/customers-licenses` Route
+  - Zeile 2319: `/api/customer/<int:customer_id>/licenses` API-Route
+
+### 2025-06-10 00:01: Verbesserte Integration zwischen Kunden & Lizenzen und Resource Pool
+
+**Problem:**
+- Umständliche Navigation zwischen Kunden & Lizenzen und Resource Pool Bereichen
+- Keine direkte Verbindung zwischen beiden Ansichten
+- Benutzer mussten ständig zwischen verschiedenen Seiten hin- und herspringen
+
+**Implementierte Lösung - 5 Phasen:**
+
+1. **Phase 1: Ressourcen-Details in Kunden & Lizenzen Ansicht**
+   - API `/api/customer/{id}/licenses` erweitert um konkrete Ressourcen-Informationen
+   - Neue API `/api/license/{id}/resources` für detaillierte Ressourcen einer Lizenz
+   - Anzeige der zugewiesenen Ressourcen mit Info-Buttons und Modal-Dialogen
+   - Klickbare Links zu Ressourcen-Details im Resource Pool
+
+2. **Phase 2: Quick-Actions für Ressourcenverwaltung**
+   - "Ressourcen verwalten" Button (Zahnrad-Icon) bei jeder Lizenz
+   - Modal mit Übersicht aller zugewiesenen Ressourcen
+   - Vorbereitung für Quarantäne-Funktionen und Ressourcen-Austausch
+
+3. **Phase 3: Ressourcen-Preview bei Lizenzerstellung**
+   - Live-Anzeige verfügbarer Ressourcen beim Ändern der Anzahl
+   - Erweiterte Verfügbarkeitsanzeige mit Badges (OK/Niedrig/Kritisch)
+   - Warnungen bei niedrigem Bestand mit visuellen Hinweisen
+   - Fortschrittsbalken zur Visualisierung der Verfügbarkeit
+
+4. **Phase 4: Dashboard-Integration**
+   - Resource Pool Widget mit erweiterten Links
+   - Kritische Warnungen bei < 50 Ressourcen mit "Auffüllen" Button
+   - Direkte Navigation zu gefilterten Ansichten (nach Typ/Status)
+   - Verbesserte visuelle Darstellung mit Tooltips
+
+5. **Phase 5: Bidirektionale Navigation**
+   - Von Resource Pool: Links zu Kunden/Lizenzen bei zugewiesenen Ressourcen
+   - "Zurück zu Kunden" Button wenn von Kunden & Lizenzen kommend
+   - Navigation-Links im Dashboard für schnellen Zugriff
+   - SQL-Query erweitert um customer_id für direkte Verlinkung
+
+**Technische Details:**
+- JavaScript-Funktionen für Modal-Dialoge und Ressourcen-Details
+- Erweiterte SQL-Queries mit JOINs für Ressourcen-Informationen
+- Bootstrap 5 Tooltips und Modals für bessere UX
+- Globale Variable `currentLicenses` für Caching der Lizenzdaten
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py` - Neue APIs und erweiterte Queries
+- `v2_adminpanel/templates/customers_licenses.html` - Ressourcen-Details und Modals
+- `v2_adminpanel/templates/index.html` - Erweiterte Verfügbarkeitsanzeige
+- `v2_adminpanel/templates/dashboard.html` - Verbesserte Resource Pool Integration
+- `v2_adminpanel/templates/resources.html` - Bidirektionale Navigation
+
+**Status:** ✅ Alle 5 Phasen erfolgreich implementiert
+
+### 2025-06-10 00:15: IP-Adressen-Erfassung hinter Reverse Proxy korrigiert
+
+**Problem:**
+- Flask-App erfasste nur die Docker-interne IP-Adresse von Nginx (172.19.0.5)
+- Echte Client-IPs wurden nicht in Audit-Logs und Login-Attempts gespeichert
+- Nginx setzte die Header korrekt, aber Flask las sie nicht aus
+
+**Ursache:**
+- Flask verwendet standardmäßig nur `request.remote_addr`
+- Dies gibt bei einem Reverse Proxy nur die Proxy-IP zurück
+- Die Header `X-Real-IP` und `X-Forwarded-For` wurden ignoriert
+
+**Lösung:**
+1. **ProxyFix Middleware** hinzugefügt für korrekte Header-Verarbeitung
+2. **get_client_ip() Funktion** angepasst:
+   - Prüft zuerst `X-Real-IP` Header
+   - Dann `X-Forwarded-For` Header (nimmt erste IP bei mehreren)
+   - Fallback auf `request.remote_addr`
+3. **Debug-Logging** für IP-Erfassung hinzugefügt
+4. **Alle `request.remote_addr` Aufrufe** durch `get_client_ip()` ersetzt
+
+**Technische Details:**
+```python
+# ProxyFix für korrekte IP-Adressen
+app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
+
+# Verbesserte IP-Erfassung
+def get_client_ip():
+    if request.headers.get('X-Real-IP'):
+        return request.headers.get('X-Real-IP')
+    elif request.headers.get('X-Forwarded-For'):
+        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
+    else:
+        return request.remote_addr
+```
+
+**Geänderte Dateien:**
+- `v2_adminpanel/app.py` - ProxyFix und verbesserte IP-Erfassung
+
+**Status:** ✅ Implementiert - Neue Aktionen erfassen jetzt echte Client-IPs
+
+### 2025-06-10 00:30: Docker ENV Legacy-Format Warnungen behoben
+
+**Problem:**
+- Docker Build zeigte Warnungen: "LegacyKeyValueFormat: ENV key=value should be used"
+- Veraltetes Format `ENV KEY VALUE` wurde in Dockerfiles verwendet
+
+**Lösung:**
+- Alle ENV-Anweisungen auf neues Format `ENV KEY=VALUE` umgestellt
+- Betraf hauptsächlich v2_postgres/Dockerfile mit 3 ENV-Zeilen
+
+**Geänderte Dateien:**
+- `v2_postgres/Dockerfile` - ENV-Format modernisiert
+
+**Beispiel der Änderung:**
+```dockerfile
+# Alt (Legacy):
+ENV LANG de_DE.UTF-8
+ENV LANGUAGE de_DE:de
+
+# Neu (Modern):
+ENV LANG=de_DE.UTF-8
+ENV LANGUAGE=de_DE:de
+```
+
+**Status:** ✅ Alle Dockerfiles verwenden jetzt das moderne ENV-Format
+
 **Status:** ✅ Behoben
\ No newline at end of file
diff --git a/v2/.env b/v2/.env
index 3a27e3f..e834125 100644
--- a/v2/.env
+++ b/v2/.env
@@ -1,56 +1,56 @@
-# PostgreSQL-Datenbank
-POSTGRES_DB=meinedatenbank
-POSTGRES_USER=adminuser
-POSTGRES_PASSWORD=supergeheimespasswort
-
-# Admin-Panel Zugangsdaten
-ADMIN1_USERNAME=rac00n
-ADMIN1_PASSWORD=1248163264
-ADMIN2_USERNAME=w@rh@mm3r
-ADMIN2_PASSWORD=Warhammer123!
-
-# Lizenzserver API Key für Authentifizierung
-
-
-# Domains (können von der App ausgewertet werden, z. B. für Links oder CORS)
-API_DOMAIN=api-software-undso.z5m7q9dk3ah2v1plx6ju.com
-ADMIN_PANEL_DOMAIN=admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
-
-# ===================== OPTIONALE VARIABLEN =====================
-
-# JWT für API-Auth
-# JWT_SECRET=geheimer_token_schlüssel
-
-# E-Mail Konfiguration (z. B. bei Ablaufwarnungen)
-# MAIL_SERVER=smtp.meinedomain.de
-# MAIL_PORT=587
-# MAIL_USERNAME=deinemail
-# MAIL_PASSWORD=geheim
-# MAIL_FROM=no-reply@meinedomain.de
-
-# Logging
-# LOG_LEVEL=info
-
-# Erlaubte CORS-Domains (für Web-Frontend)
-# ALLOWED_ORIGINS=https://admin.meinedomain.de
-
-# ===================== VERSION =====================
-
-# Serverseitig gepflegte aktuelle Software-Version
-# Diese wird vom Lizenzserver genutzt, um die Kundenversion zu vergleichen
-LATEST_CLIENT_VERSION=1.0.0
-
-# ===================== BACKUP KONFIGURATION =====================
-
-# E-Mail für Backup-Benachrichtigungen
-EMAIL_ENABLED=false
-
-# Backup-Verschlüsselung (optional, wird automatisch generiert wenn leer)
-# BACKUP_ENCRYPTION_KEY=
-
-# ===================== CAPTCHA KONFIGURATION =====================
-
-# Google reCAPTCHA v2 Keys (https://www.google.com/recaptcha/admin)
-# Für PoC-Phase auskommentiert - CAPTCHA wird übersprungen wenn Keys fehlen
-# RECAPTCHA_SITE_KEY=your-site-key-here
-# RECAPTCHA_SECRET_KEY=your-secret-key-here
+# PostgreSQL-Datenbank
+POSTGRES_DB=meinedatenbank
+POSTGRES_USER=adminuser
+POSTGRES_PASSWORD=supergeheimespasswort
+
+# Admin-Panel Zugangsdaten
+ADMIN1_USERNAME=rac00n
+ADMIN1_PASSWORD=1248163264
+ADMIN2_USERNAME=w@rh@mm3r
+ADMIN2_PASSWORD=Warhammer123!
+
+# Lizenzserver API Key für Authentifizierung
+
+
+# Domains (können von der App ausgewertet werden, z. B. für Links oder CORS)
+API_DOMAIN=api-software-undso.z5m7q9dk3ah2v1plx6ju.com
+ADMIN_PANEL_DOMAIN=admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com
+
+# ===================== OPTIONALE VARIABLEN =====================
+
+# JWT für API-Auth
+# JWT_SECRET=geheimer_token_schlüssel
+
+# E-Mail Konfiguration (z. B. bei Ablaufwarnungen)
+# MAIL_SERVER=smtp.meinedomain.de
+# MAIL_PORT=587
+# MAIL_USERNAME=deinemail
+# MAIL_PASSWORD=geheim
+# MAIL_FROM=no-reply@meinedomain.de
+
+# Logging
+# LOG_LEVEL=info
+
+# Erlaubte CORS-Domains (für Web-Frontend)
+# ALLOWED_ORIGINS=https://admin.meinedomain.de
+
+# ===================== VERSION =====================
+
+# Serverseitig gepflegte aktuelle Software-Version
+# Diese wird vom Lizenzserver genutzt, um die Kundenversion zu vergleichen
+LATEST_CLIENT_VERSION=1.0.0
+
+# ===================== BACKUP KONFIGURATION =====================
+
+# E-Mail für Backup-Benachrichtigungen
+EMAIL_ENABLED=false
+
+# Backup-Verschlüsselung (optional, wird automatisch generiert wenn leer)
+# BACKUP_ENCRYPTION_KEY=
+
+# ===================== CAPTCHA KONFIGURATION =====================
+
+# Google reCAPTCHA v2 Keys (https://www.google.com/recaptcha/admin)
+# Für PoC-Phase auskommentiert - CAPTCHA wird übersprungen wenn Keys fehlen
+# RECAPTCHA_SITE_KEY=your-site-key-here
+# RECAPTCHA_SECRET_KEY=your-secret-key-here
diff --git a/v2/docker-compose.yaml b/v2/docker-compose.yaml
index 5ab8942..fe2acee 100644
--- a/v2/docker-compose.yaml
+++ b/v2/docker-compose.yaml
@@ -1,90 +1,90 @@
-services:
-  postgres:
-    build:
-      context: ../v2_postgres
-    container_name: db
-    restart: always
-    env_file: .env
-    environment:
-      POSTGRES_HOST: postgres
-      POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=de_DE.UTF-8'
-      POSTGRES_COLLATE: 'de_DE.UTF-8'
-      POSTGRES_CTYPE: 'de_DE.UTF-8'
-      TZ: Europe/Berlin
-      PGTZ: Europe/Berlin
-    volumes:
-      # Persistente Speicherung der Datenbank auf dem Windows-Host
-      - postgres_data:/var/lib/postgresql/data
-      # Init-Skript für Tabellen
-      - ../v2_adminpanel/init.sql:/docker-entrypoint-initdb.d/init.sql
-    networks:
-      - internal_net
-    deploy:
-      resources:
-        limits:
-          cpus: '2'
-          memory: 4g
-
-  license-server:
-    build:
-      context: ../v2_lizenzserver
-    container_name: license-server
-    restart: always
-    # Port-Mapping entfernt - nur noch über Nginx erreichbar
-    env_file: .env
-    environment:
-      TZ: Europe/Berlin
-    depends_on:
-      - postgres
-    networks:
-      - internal_net
-    deploy:
-      resources:
-        limits:
-          cpus: '2'
-          memory: 4g
-
-  admin-panel:
-    build:
-      context: ../v2_adminpanel
-    container_name: admin-panel
-    restart: always
-    # Port-Mapping entfernt - nur über nginx erreichbar
-    env_file: .env
-    environment:
-      TZ: Europe/Berlin
-    depends_on:
-      - postgres
-    networks:
-      - internal_net
-    volumes:
-      # Backup-Verzeichnis
-      - ../backups:/app/backups
-    deploy:
-      resources:
-        limits:
-          cpus: '2'
-          memory: 4g
-
-  nginx:
-    build:
-      context: ../v2_nginx
-    container_name: nginx-proxy
-    restart: always
-    ports:
-      - "80:80"
-      - "443:443"
-    environment:
-      TZ: Europe/Berlin
-    depends_on:
-      - admin-panel
-      - license-server
-    networks:
-      - internal_net
-
-networks:
-  internal_net:
-    driver: bridge
-
-volumes:
-  postgres_data:
+services:
+  postgres:
+    build:
+      context: ../v2_postgres
+    container_name: db
+    restart: always
+    env_file: .env
+    environment:
+      POSTGRES_HOST: postgres
+      POSTGRES_INITDB_ARGS: '--encoding=UTF8 --locale=de_DE.UTF-8'
+      POSTGRES_COLLATE: 'de_DE.UTF-8'
+      POSTGRES_CTYPE: 'de_DE.UTF-8'
+      TZ: Europe/Berlin
+      PGTZ: Europe/Berlin
+    volumes:
+      # Persistente Speicherung der Datenbank auf dem Windows-Host
+      - postgres_data:/var/lib/postgresql/data
+      # Init-Skript für Tabellen
+      - ../v2_adminpanel/init.sql:/docker-entrypoint-initdb.d/init.sql
+    networks:
+      - internal_net
+    deploy:
+      resources:
+        limits:
+          cpus: '2'
+          memory: 4g
+
+  license-server:
+    build:
+      context: ../v2_lizenzserver
+    container_name: license-server
+    restart: always
+    # Port-Mapping entfernt - nur noch über Nginx erreichbar
+    env_file: .env
+    environment:
+      TZ: Europe/Berlin
+    depends_on:
+      - postgres
+    networks:
+      - internal_net
+    deploy:
+      resources:
+        limits:
+          cpus: '2'
+          memory: 4g
+
+  admin-panel:
+    build:
+      context: ../v2_adminpanel
+    container_name: admin-panel
+    restart: always
+    # Port-Mapping entfernt - nur über nginx erreichbar
+    env_file: .env
+    environment:
+      TZ: Europe/Berlin
+    depends_on:
+      - postgres
+    networks:
+      - internal_net
+    volumes:
+      # Backup-Verzeichnis
+      - ../backups:/app/backups
+    deploy:
+      resources:
+        limits:
+          cpus: '2'
+          memory: 4g
+
+  nginx:
+    build:
+      context: ../v2_nginx
+    container_name: nginx-proxy
+    restart: always
+    ports:
+      - "80:80"
+      - "443:443"
+    environment:
+      TZ: Europe/Berlin
+    depends_on:
+      - admin-panel
+      - license-server
+    networks:
+      - internal_net
+
+networks:
+  internal_net:
+    driver: bridge
+
+volumes:
+  postgres_data:
diff --git a/v2_adminpanel/Dockerfile b/v2_adminpanel/Dockerfile
index cee53bf..dde0146 100644
--- a/v2_adminpanel/Dockerfile
+++ b/v2_adminpanel/Dockerfile
@@ -1,33 +1,33 @@
-FROM python:3.11-slim
-
-# Locale für deutsche Sprache und UTF-8 setzen
-ENV LANG=de_DE.UTF-8
-ENV LC_ALL=de_DE.UTF-8
-ENV PYTHONIOENCODING=utf-8
-
-# Zeitzone auf Europe/Berlin setzen
-ENV TZ=Europe/Berlin
-
-WORKDIR /app
-
-# System-Dependencies inkl. PostgreSQL-Tools installieren
-RUN apt-get update && apt-get install -y \
-    locales \
-    postgresql-client \
-    tzdata \
-    && sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen \
-    && locale-gen \
-    && update-locale LANG=de_DE.UTF-8 \
-    && ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
-    && echo "Europe/Berlin" > /etc/timezone \
-    && apt-get clean \
-    && rm -rf /var/lib/apt/lists/*
-
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-
-COPY . .
-
-EXPOSE 5000
-
-CMD ["python", "app.py"]
+FROM python:3.11-slim
+
+# Locale für deutsche Sprache und UTF-8 setzen
+ENV LANG=de_DE.UTF-8
+ENV LC_ALL=de_DE.UTF-8
+ENV PYTHONIOENCODING=utf-8
+
+# Zeitzone auf Europe/Berlin setzen
+ENV TZ=Europe/Berlin
+
+WORKDIR /app
+
+# System-Dependencies inkl. PostgreSQL-Tools installieren
+RUN apt-get update && apt-get install -y \
+    locales \
+    postgresql-client \
+    tzdata \
+    && sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen \
+    && locale-gen \
+    && update-locale LANG=de_DE.UTF-8 \
+    && ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
+    && echo "Europe/Berlin" > /etc/timezone \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY . .
+
+EXPOSE 5000
+
+CMD ["python", "app.py"]
diff --git a/v2_adminpanel/__pycache__/app.cpython-312.pyc b/v2_adminpanel/__pycache__/app.cpython-312.pyc
index ae11b74..99427c4 100644
Binary files a/v2_adminpanel/__pycache__/app.cpython-312.pyc and b/v2_adminpanel/__pycache__/app.cpython-312.pyc differ
diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py
index 0622714..4e6204a 100644
--- a/v2_adminpanel/app.py
+++ b/v2_adminpanel/app.py
@@ -77,10 +77,24 @@ logging.basicConfig(level=logging.INFO)
 # Import and register blueprints
 from routes.auth_routes import auth_bp
 from routes.admin_routes import admin_bp
+from routes.license_routes import license_bp
+from routes.customer_routes import customer_bp
+from routes.resource_routes import resource_bp
+from routes.session_routes import session_bp
+from routes.batch_routes import batch_bp
+from routes.api_routes import api_bp
+from routes.export_routes import export_bp
 
-# Temporarily comment out blueprints to avoid conflicts
-# app.register_blueprint(auth_bp)
-# app.register_blueprint(admin_bp)
+# Register blueprints
+app.register_blueprint(auth_bp)
+app.register_blueprint(admin_bp)
+app.register_blueprint(license_bp)
+app.register_blueprint(customer_bp)
+app.register_blueprint(resource_bp)
+app.register_blueprint(session_bp)
+app.register_blueprint(batch_bp)
+app.register_blueprint(api_bp)
+app.register_blueprint(export_bp)
 
 
 # Scheduled Backup Job
@@ -136,213 +150,213 @@ def verify_recaptcha(response):
         return False
 
 
-@app.route("/login", methods=["GET", "POST"])
-def login():
+# @app.route("/login", methods=["GET", "POST"])
+# def login():
     # Timing-Attack Schutz - Start Zeit merken
-    start_time = time.time()
+    # start_time = time.time()
     
     # IP-Adresse ermitteln
-    ip_address = get_client_ip()
+    # ip_address = get_client_ip()
     
     # Prüfen ob IP gesperrt ist
-    is_blocked, blocked_until = check_ip_blocked(ip_address)
-    if is_blocked:
-        time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
-        error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
-        return render_template("login.html", error=error_msg, error_type="blocked")
+    # is_blocked, blocked_until = check_ip_blocked(ip_address)
+    # if is_blocked:
+        # time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
+        # error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
+        # return render_template("login.html", error=error_msg, error_type="blocked")
     
     # Anzahl bisheriger Versuche
-    attempt_count = get_login_attempts(ip_address)
+    # attempt_count = get_login_attempts(ip_address)
     
-    if request.method == "POST":
-        username = request.form.get("username")
-        password = request.form.get("password")
-        captcha_response = request.form.get("g-recaptcha-response")
+    # if request.method == "POST":
+        # username = request.form.get("username")
+        # password = request.form.get("password")
+        # captcha_response = request.form.get("g-recaptcha-response")
         
         # CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
-        recaptcha_site_key = config.RECAPTCHA_SITE_KEY
-        if attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
-            if not captcha_response:
+        # recaptcha_site_key = config.RECAPTCHA_SITE_KEY
+        # if attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
+            # if not captcha_response:
                 # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA ERFORDERLICH!", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
+                # elapsed = time.time() - start_time
+                # if elapsed < 1.0:
+                    # time.sleep(1.0 - elapsed)
+                # return render_template("login.html", 
+                                     # error="CAPTCHA ERFORDERLICH!", 
+                                     # show_captcha=True,
+                                     # error_type="captcha",
+                                     # attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     # recaptcha_site_key=recaptcha_site_key)
             
             # CAPTCHA validieren
-            if not verify_recaptcha(captcha_response):
+            # if not verify_recaptcha(captcha_response):
                 # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
+                # elapsed = time.time() - start_time
+                # if elapsed < 1.0:
+                    # time.sleep(1.0 - elapsed)
+                # return render_template("login.html", 
+                                     # error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
+                                     # show_captcha=True,
+                                     # error_type="captcha",
+                                     # attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     # recaptcha_site_key=recaptcha_site_key)
         
         # Check user in database first, fallback to env vars
-        user = get_user_by_username(username)
-        login_success = False
-        needs_2fa = False
+        # user = get_user_by_username(username)
+        # login_success = False
+        # needs_2fa = False
         
-        if user:
+        # if user:
             # Database user authentication
-            if verify_password(password, user['password_hash']):
-                login_success = True
-                needs_2fa = user['totp_enabled']
-        else:
+            # if verify_password(password, user['password_hash']):
+                # login_success = True
+                # needs_2fa = user['totp_enabled']
+        # else:
             # Fallback to environment variables for backward compatibility
-            if username in config.ADMIN_USERS and password == config.ADMIN_USERS[username]:
-                login_success = True
+            # if username in config.ADMIN_USERS and password == config.ADMIN_USERS[username]:
+                # login_success = True
         
         # Timing-Attack Schutz - Mindestens 1 Sekunde warten
-        elapsed = time.time() - start_time
-        if elapsed < 1.0:
-            time.sleep(1.0 - elapsed)
+        # elapsed = time.time() - start_time
+        # if elapsed < 1.0:
+            # time.sleep(1.0 - elapsed)
         
-        if login_success:
+        # if login_success:
             # Erfolgreicher Login
-            if needs_2fa:
+            # if needs_2fa:
                 # Store temporary session for 2FA verification
-                session['temp_username'] = username
-                session['temp_user_id'] = user['id']
-                session['awaiting_2fa'] = True
-                return redirect(url_for('verify_2fa'))
-            else:
+                # session['temp_username'] = username
+                # session['temp_user_id'] = user['id']
+                # session['awaiting_2fa'] = True
+                # return redirect(url_for('verify_2fa'))
+            # else:
                 # Complete login without 2FA
-                session.permanent = True  # Aktiviert das Timeout
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user['id'] if user else None
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                reset_login_attempts(ip_address)
-                log_audit('LOGIN_SUCCESS', 'user', 
-                         additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
-                return redirect(url_for('dashboard'))
-        else:
+                # session.permanent = True  # Aktiviert das Timeout
+                # session['logged_in'] = True
+                # session['username'] = username
+                # session['user_id'] = user['id'] if user else None
+                # session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                # reset_login_attempts(ip_address)
+                # log_audit('LOGIN_SUCCESS', 'user', 
+                         # additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
+                # return redirect(url_for('dashboard'))
+        # else:
             # Fehlgeschlagener Login
-            error_message = record_failed_attempt(ip_address, username)
-            new_attempt_count = get_login_attempts(ip_address)
+            # error_message = record_failed_attempt(ip_address, username)
+            # new_attempt_count = get_login_attempts(ip_address)
             
             # Prüfen ob jetzt gesperrt
-            is_now_blocked, _ = check_ip_blocked(ip_address)
-            if is_now_blocked:
-                log_audit('LOGIN_BLOCKED', 'security', 
-                         additional_info=f"IP {ip_address} wurde nach {config.MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
+            # is_now_blocked, _ = check_ip_blocked(ip_address)
+            # if is_now_blocked:
+                # log_audit('LOGIN_BLOCKED', 'security', 
+                         # additional_info=f"IP {ip_address} wurde nach {config.MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
             
-            return render_template("login.html", 
-                                 error=error_message,
-                                 show_captcha=(new_attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
-                                 error_type="failed",
-                                 attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - new_attempt_count),
-                                 recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
+            # return render_template("login.html", 
+                                 # error=error_message,
+                                 # show_captcha=(new_attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
+                                 # error_type="failed",
+                                 # attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - new_attempt_count),
+                                 # recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
     
     # GET Request
-    return render_template("login.html", 
-                         show_captcha=(attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
-                         attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
-                         recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
+    # return render_template("login.html", 
+                         # show_captcha=(attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
+                         # attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
+                         # recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
 
-@app.route("/logout")
-def logout():
-    username = session.get('username', 'unknown')
-    log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
-    session.pop('logged_in', None)
-    session.pop('username', None)
-    session.pop('user_id', None)
-    session.pop('temp_username', None)
-    session.pop('temp_user_id', None)
-    session.pop('awaiting_2fa', None)
-    return redirect(url_for('login'))
+# @app.route("/logout")
+# def logout():
+    # username = session.get('username', 'unknown')
+    # log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
+    # session.pop('logged_in', None)
+    # session.pop('username', None)
+    # session.pop('user_id', None)
+    # session.pop('temp_username', None)
+    # session.pop('temp_user_id', None)
+    # session.pop('awaiting_2fa', None)
+    # return redirect(url_for('login'))
 
-@app.route("/verify-2fa", methods=["GET", "POST"])
-def verify_2fa():
-    if not session.get('awaiting_2fa'):
-        return redirect(url_for('login'))
+# @app.route("/verify-2fa", methods=["GET", "POST"])
+# def verify_2fa():
+    # if not session.get('awaiting_2fa'):
+        # return redirect(url_for('login'))
     
-    if request.method == "POST":
-        token = request.form.get('token', '').replace(' ', '')
-        username = session.get('temp_username')
-        user_id = session.get('temp_user_id')
+    # if request.method == "POST":
+        # token = request.form.get('token', '').replace(' ', '')
+        # username = session.get('temp_username')
+        # user_id = session.get('temp_user_id')
         
-        if not username or not user_id:
-            flash('Session expired. Please login again.', 'error')
-            return redirect(url_for('login'))
+        # if not username or not user_id:
+            # flash('Session expired. Please login again.', 'error')
+            # return redirect(url_for('login'))
         
-        user = get_user_by_username(username)
-        if not user:
-            flash('User not found.', 'error')
-            return redirect(url_for('login'))
+        # user = get_user_by_username(username)
+        # if not user:
+            # flash('User not found.', 'error')
+            # return redirect(url_for('login'))
         
         # Check if it's a backup code
-        if len(token) == 8 and token.isupper():
+        # if len(token) == 8 and token.isupper():
             # Try backup code
-            backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
-            if verify_backup_code(token, backup_codes):
+            # backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
+            # if verify_backup_code(token, backup_codes):
                 # Remove used backup code
-                code_hash = hash_backup_code(token)
-                backup_codes.remove(code_hash)
+                # code_hash = hash_backup_code(token)
+                # backup_codes.remove(code_hash)
                 
-                conn = get_connection()
-                cur = conn.cursor()
-                cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
-                          (json.dumps(backup_codes), user_id))
-                conn.commit()
-                cur.close()
-                conn.close()
+                # conn = get_connection()
+                # cur = conn.cursor()
+                # cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
+                          # (json.dumps(backup_codes), user_id))
+                # conn.commit()
+                # cur.close()
+                # conn.close()
                 
                 # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
+                # session.permanent = True
+                # session['logged_in'] = True
+                # session['username'] = username
+                # session['user_id'] = user_id
+                # session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                # session.pop('temp_username', None)
+                # session.pop('temp_user_id', None)
+                # session.pop('awaiting_2fa', None)
                 
-                flash('Login successful using backup code. Please generate new backup codes.', 'warning')
-                log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
-                return redirect(url_for('dashboard'))
-        else:
+                # flash('Login successful using backup code. Please generate new backup codes.', 'warning')
+                # log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
+                # return redirect(url_for('dashboard'))
+        # else:
             # Try TOTP token
-            if verify_totp(user['totp_secret'], token):
+            # if verify_totp(user['totp_secret'], token):
                 # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
+                # session.permanent = True
+                # session['logged_in'] = True
+                # session['username'] = username
+                # session['user_id'] = user_id
+                # session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                # session.pop('temp_username', None)
+                # session.pop('temp_user_id', None)
+                # session.pop('awaiting_2fa', None)
                 
-                log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
-                return redirect(url_for('dashboard'))
+                # log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
+                # return redirect(url_for('dashboard'))
         
         # Failed verification
-        conn = get_connection()
-        cur = conn.cursor()
-        cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
-                  (datetime.now(), user_id))
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        flash('Invalid authentication code. Please try again.', 'error')
-        log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
-    
-    return render_template('verify_2fa.html')
+        # conn = get_connection()
+        # cur = conn.cursor()
+        # cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
+                  # (datetime.now(), user_id))
+        # conn.commit()
+        # cur.close()
+        # conn.close()
+        
+        # flash('Invalid authentication code. Please try again.', 'error')
+        # log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
+    
+    # return render_template('verify_2fa.html')
 
-@app.route("/profile")
-@login_required
+# @app.route("/profile")
+# @login_required
 def profile():
     user = get_user_by_username(session['username'])
     if not user:
@@ -351,8 +365,8 @@ def profile():
         return redirect(url_for('dashboard'))
     return render_template('profile.html', user=user)
 
-@app.route("/profile/change-password", methods=["POST"])
-@login_required
+# @app.route("/profile/change-password", methods=["POST"])
+# @login_required
 def change_password():
     current_password = request.form.get('current_password')
     new_password = request.form.get('new_password')
@@ -389,8 +403,8 @@ def change_password():
     flash('Password changed successfully.', 'success')
     return redirect(url_for('profile'))
 
-@app.route("/profile/setup-2fa")
-@login_required
+# @app.route("/profile/setup-2fa")
+# @login_required
 def setup_2fa():
     user = get_user_by_username(session['username'])
     
@@ -409,8 +423,8 @@ def setup_2fa():
                          totp_secret=totp_secret, 
                          qr_code=qr_code)
 
-@app.route("/profile/enable-2fa", methods=["POST"])
-@login_required
+# @app.route("/profile/enable-2fa", methods=["POST"])
+# @login_required
 def enable_2fa():
     token = request.form.get('token', '').replace(' ', '')
     totp_secret = session.get('temp_totp_secret')
@@ -447,8 +461,8 @@ def enable_2fa():
     # Show backup codes
     return render_template('backup_codes.html', backup_codes=backup_codes)
 
-@app.route("/profile/disable-2fa", methods=["POST"])
-@login_required
+# @app.route("/profile/disable-2fa", methods=["POST"])
+# @login_required
 def disable_2fa():
     password = request.form.get('password')
     user = get_user_by_username(session['username'])
@@ -474,8 +488,8 @@ def disable_2fa():
     flash('2FA has been disabled for your account.', 'success')
     return redirect(url_for('profile'))
 
-@app.route("/heartbeat", methods=['POST'])
-@login_required
+# @app.route("/heartbeat", methods=['POST'])
+# @login_required
 def heartbeat():
     """Endpoint für Session Keep-Alive - aktualisiert last_activity"""
     # Aktualisiere last_activity nur wenn explizit angefordert
@@ -489,8 +503,8 @@ def heartbeat():
         'username': session.get('username')
     })
 
-@app.route("/api/generate-license-key", methods=['POST'])
-@login_required
+# @app.route("/api/generate-license-key", methods=['POST'])
+# @login_required
 def api_generate_key():
     """API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
     try:
@@ -534,8 +548,8 @@ def api_generate_key():
             'error': 'Fehler bei der Key-Generierung'
         }), 500
 
-@app.route("/api/customers", methods=['GET'])
-@login_required
+# @app.route("/api/customers", methods=['GET'])
+# @login_required
 def api_customers():
     """API Endpoint für die Kundensuche mit Select2"""
     try:
@@ -645,8 +659,8 @@ def api_customers():
             'error': 'Fehler bei der Kundensuche'
         }), 500
 
-@app.route("/")
-@login_required
+# @app.route("/")
+# @login_required
 def dashboard():
     conn = get_connection()
     cur = conn.cursor()
@@ -875,8 +889,8 @@ def dashboard():
                          resource_warning=resource_warning,
                          username=session.get('username'))
 
-@app.route("/create", methods=["GET", "POST"])
-@login_required
+# @app.route("/create", methods=["GET", "POST"])
+# @login_required
 def create_license():
     if request.method == "POST":
         customer_id = request.form.get("customer_id")
@@ -1106,8 +1120,8 @@ def create_license():
     preselected_customer_id = request.args.get('customer_id', type=int)
     return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
 
-@app.route("/batch", methods=["GET", "POST"])
-@login_required
+# @app.route("/batch", methods=["GET", "POST"])
+# @login_required
 def batch_licenses():
     """Batch-Generierung mehrerer Lizenzen für einen Kunden"""
     if request.method == "POST":
@@ -1361,8 +1375,8 @@ def batch_licenses():
     # GET Request
     return render_template("batch_form.html")
 
-@app.route("/batch/export")
-@login_required
+# @app.route("/batch/export")
+# @login_required
 def export_batch():
     """Exportiert die zuletzt generierten Batch-Lizenzen"""
     batch_data = session.get('batch_export')
@@ -1400,14 +1414,14 @@ def export_batch():
         download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv"
     )
 
-@app.route("/licenses")
-@login_required
+# @app.route("/licenses")
+# @login_required
 def licenses():
     # Redirect zur kombinierten Ansicht
     return redirect("/customers-licenses")
 
-@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
-@login_required
+# @app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
+# @login_required
 def edit_license(license_id):
     conn = get_connection()
     cur = conn.cursor()
@@ -1498,8 +1512,8 @@ def edit_license(license_id):
     
     return render_template("edit_license.html", license=license, username=session.get('username'))
 
-@app.route("/license/delete/<int:license_id>", methods=["POST"])
-@login_required
+# @app.route("/license/delete/<int:license_id>", methods=["POST"])
+# @login_required
 def delete_license(license_id):
     conn = get_connection()
     cur = conn.cursor()
@@ -1531,14 +1545,14 @@ def delete_license(license_id):
     
     return redirect("/licenses")
 
-@app.route("/customers")
-@login_required
+# @app.route("/customers")
+# @login_required
 def customers():
     # Redirect zur kombinierten Ansicht
     return redirect("/customers-licenses")
 
-@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
-@login_required
+# @app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
+# @login_required
 def edit_customer(customer_id):
     conn = get_connection()
     cur = conn.cursor()
@@ -1621,8 +1635,8 @@ def edit_customer(customer_id):
     
     return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
 
-@app.route("/customer/create", methods=["GET", "POST"])
-@login_required
+# @app.route("/customer/create", methods=["GET", "POST"])
+# @login_required
 def create_customer():
     """Erstellt einen neuen Kunden ohne Lizenz"""
     if request.method == "POST":
@@ -1676,8 +1690,8 @@ def create_customer():
     # GET Request - Formular anzeigen
     return render_template("create_customer.html", username=session.get('username'))
 
-@app.route("/customer/delete/<int:customer_id>", methods=["POST"])
-@login_required
+# @app.route("/customer/delete/<int:customer_id>", methods=["POST"])
+# @login_required
 def delete_customer(customer_id):
     conn = get_connection()
     cur = conn.cursor()
@@ -1714,8 +1728,8 @@ def delete_customer(customer_id):
     
     return redirect("/customers")
 
-@app.route("/customers-licenses")
-@login_required
+# @app.route("/customers-licenses")
+# @login_required
 def customers_licenses():
     """Kombinierte Ansicht für Kunden und deren Lizenzen"""
     conn = get_connection()
@@ -1807,8 +1821,8 @@ def customers_licenses():
                          licenses=licenses,
                          show_test=show_test)
 
-@app.route("/api/customer/<int:customer_id>/licenses")
-@login_required
+# @app.route("/api/customer/<int:customer_id>/licenses")
+# @login_required
 def api_customer_licenses(customer_id):
     """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden"""
     conn = get_connection()
@@ -1910,8 +1924,8 @@ def api_customer_licenses(customer_id):
         'count': len(licenses)
     })
 
-@app.route("/api/customer/<int:customer_id>/quick-stats")
-@login_required
+# @app.route("/api/customer/<int:customer_id>/quick-stats")
+# @login_required
 def api_customer_quick_stats(customer_id):
     """API-Endpoint für Schnellstatistiken eines Kunden"""
     conn = get_connection()
@@ -1943,8 +1957,8 @@ def api_customer_quick_stats(customer_id):
         }
     })
 
-@app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
-@login_required
+# @app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
+# @login_required
 def api_license_quick_edit(license_id):
     """API-Endpoint für schnelle Lizenz-Bearbeitung"""
     conn = get_connection()
@@ -2013,8 +2027,8 @@ def api_license_quick_edit(license_id):
         conn.close()
         return jsonify({'success': False, 'error': str(e)}), 500
 
-@app.route("/api/license/<int:license_id>/resources")
-@login_required
+# @app.route("/api/license/<int:license_id>/resources")
+# @login_required
 def api_license_resources(license_id):
     """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
     conn = get_connection()
@@ -2063,8 +2077,8 @@ def api_license_resources(license_id):
         conn.close()
         return jsonify({'success': False, 'error': str(e)}), 500
 
-@app.route("/sessions")
-@login_required
+# @app.route("/sessions")
+# @login_required
 def sessions():
     conn = get_connection()
     cur = conn.cursor()
@@ -2145,8 +2159,8 @@ def sessions():
                          ended_order=ended_order,
                          username=session.get('username'))
 
-@app.route("/session/end/<int:session_id>", methods=["POST"])
-@login_required
+# @app.route("/session/end/<int:session_id>", methods=["POST"])
+# @login_required
 def end_session(session_id):
     conn = get_connection()
     cur = conn.cursor()
@@ -2164,8 +2178,8 @@ def end_session(session_id):
     
     return redirect("/sessions")
 
-@app.route("/export/licenses")
-@login_required
+# @app.route("/export/licenses")
+# @login_required
 def export_licenses():
     conn = get_connection()
     cur = conn.cursor()
@@ -2274,8 +2288,8 @@ def export_licenses():
             download_name=f'{filename}.xlsx'
         )
 
-@app.route("/export/audit")
-@login_required
+# @app.route("/export/audit")
+# @login_required
 def export_audit():
     conn = get_connection()
     cur = conn.cursor()
@@ -2398,8 +2412,8 @@ def export_audit():
             download_name=f'{filename}.xlsx'
         )
 
-@app.route("/export/customers")
-@login_required
+# @app.route("/export/customers")
+# @login_required
 def export_customers():
     conn = get_connection()
     cur = conn.cursor()
@@ -2502,8 +2516,8 @@ def export_customers():
             download_name=f'{filename}.xlsx'
         )
 
-@app.route("/export/sessions")
-@login_required
+# @app.route("/export/sessions")
+# @login_required
 def export_sessions():
     conn = get_connection()
     cur = conn.cursor()
@@ -2641,8 +2655,8 @@ def export_sessions():
             download_name=f'{filename}.xlsx'
         )
 
-@app.route("/export/resources")
-@login_required
+# @app.route("/export/resources")
+# @login_required
 def export_resources():
     conn = get_connection()
     cur = conn.cursor()
@@ -2770,8 +2784,8 @@ def export_resources():
             download_name=f'{filename}.xlsx'
         )
 
-@app.route("/audit")
-@login_required
+# @app.route("/audit")
+# @login_required
 def audit_log():
     conn = get_connection()
     cur = conn.cursor()
@@ -2864,8 +2878,8 @@ def audit_log():
                          order=order,
                          username=session.get('username'))
 
-@app.route("/backups")
-@login_required
+# @app.route("/backups")
+# @login_required
 def backups():
     """Zeigt die Backup-Historie an"""
     conn = get_connection()
@@ -2899,8 +2913,8 @@ def backups():
                          last_backup=last_backup,
                          username=session.get('username'))
 
-@app.route("/backup/create", methods=["POST"])
-@login_required
+# @app.route("/backup/create", methods=["POST"])
+# @login_required
 def create_backup_route():
     """Erstellt ein manuelles Backup"""
     username = session.get('username')
@@ -2917,8 +2931,8 @@ def create_backup_route():
             'message': f'Backup fehlgeschlagen: {result}'
         }), 500
 
-@app.route("/backup/restore/<int:backup_id>", methods=["POST"])
-@login_required
+# @app.route("/backup/restore/<int:backup_id>", methods=["POST"])
+# @login_required
 def restore_backup_route(backup_id):
     """Stellt ein Backup wieder her"""
     encryption_key = request.form.get('encryption_key')
@@ -2936,8 +2950,8 @@ def restore_backup_route(backup_id):
             'message': message
         }), 500
 
-@app.route("/backup/download/<int:backup_id>")
-@login_required
+# @app.route("/backup/download/<int:backup_id>")
+# @login_required
 def download_backup(backup_id):
     """Lädt eine Backup-Datei herunter"""
     conn = get_connection()
@@ -2968,8 +2982,8 @@ def download_backup(backup_id):
     
     return send_file(filepath, as_attachment=True, download_name=filename)
 
-@app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
-@login_required
+# @app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
+# @login_required
 def delete_backup(backup_id):
     """Löscht ein Backup"""
     conn = get_connection()
@@ -3024,8 +3038,8 @@ def delete_backup(backup_id):
         cur.close()
         conn.close()
 
-@app.route("/security/blocked-ips")
-@login_required
+# @app.route("/security/blocked-ips")
+# @login_required
 def blocked_ips():
     """Zeigt alle gesperrten IPs an"""
     conn = get_connection()
@@ -3065,8 +3079,8 @@ def blocked_ips():
                          blocked_ips=blocked_ips_list,
                          username=session.get('username'))
 
-@app.route("/security/unblock-ip", methods=["POST"])
-@login_required
+# @app.route("/security/unblock-ip", methods=["POST"])
+# @login_required
 def unblock_ip():
     """Entsperrt eine IP-Adresse"""
     ip_address = request.form.get('ip_address')
@@ -3091,8 +3105,8 @@ def unblock_ip():
     
     return redirect(url_for('blocked_ips'))
 
-@app.route("/security/clear-attempts", methods=["POST"])
-@login_required
+# @app.route("/security/clear-attempts", methods=["POST"])
+# @login_required
 def clear_attempts():
     """Löscht alle Login-Versuche für eine IP"""
     ip_address = request.form.get('ip_address')
@@ -3107,8 +3121,8 @@ def clear_attempts():
     return redirect(url_for('blocked_ips'))
 
 # API Endpoints for License Management
-@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
-@login_required
+# @app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
+# @login_required
 def toggle_license_api(license_id):
     """Toggle license active status via API"""
     try:
@@ -3139,8 +3153,8 @@ def toggle_license_api(license_id):
     except Exception as e:
         return jsonify({'success': False, 'message': str(e)}), 500
 
-@app.route("/api/licenses/bulk-activate", methods=["POST"])
-@login_required
+# @app.route("/api/licenses/bulk-activate", methods=["POST"])
+# @login_required
 def bulk_activate_licenses():
     """Activate multiple licenses at once"""
     try:
@@ -3175,8 +3189,8 @@ def bulk_activate_licenses():
     except Exception as e:
         return jsonify({'success': False, 'message': str(e)}), 500
 
-@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
-@login_required
+# @app.route("/api/licenses/bulk-deactivate", methods=["POST"])
+# @login_required
 def bulk_deactivate_licenses():
     """Deactivate multiple licenses at once"""
     try:
@@ -3211,8 +3225,8 @@ def bulk_deactivate_licenses():
     except Exception as e:
         return jsonify({'success': False, 'message': str(e)}), 500
 
-@app.route("/api/license/<int:license_id>/devices")
-@login_required
+# @app.route("/api/license/<int:license_id>/devices")
+# @login_required
 def get_license_devices(license_id):
     """Hole alle registrierten Geräte einer Lizenz"""
     try:
@@ -3266,123 +3280,123 @@ def get_license_devices(license_id):
         logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
         return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
 
-@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
-def register_device(license_id):
-    """Registriere ein neues Gerät für eine Lizenz"""
-    try:
-        data = request.get_json()
-        hardware_id = data.get('hardware_id')
-        device_name = data.get('device_name', '')
-        operating_system = data.get('operating_system', '')
+# @app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
+# def register_device(license_id):
+    # """Registriere ein neues Gerät für eine Lizenz"""
+    # try:
+        # data = request.get_json()
+        # hardware_id = data.get('hardware_id')
+        # device_name = data.get('device_name', '')
+        # operating_system = data.get('operating_system', '')
         
-        if not hardware_id:
-            return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
+        # if not hardware_id:
+            # return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
         
-        conn = get_connection()
-        cur = conn.cursor()
+        # conn = get_connection()
+        # cur = conn.cursor()
         
         # Prüfe ob Lizenz existiert und aktiv ist
-        cur.execute("""
-            SELECT device_limit, is_active, valid_until
-            FROM licenses 
-            WHERE id = %s
-        """, (license_id,))
-        license_data = cur.fetchone()
+        # cur.execute("""
+            # SELECT device_limit, is_active, valid_until
+            # FROM licenses 
+            # WHERE id = %s
+        # """, (license_id,))
+        # license_data = cur.fetchone()
         
-        if not license_data:
-            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
+        # if not license_data:
+            # return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
         
-        device_limit, is_active, valid_until = license_data
+        # device_limit, is_active, valid_until = license_data
         
         # Prüfe ob Lizenz aktiv und gültig ist
-        if not is_active:
-            return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
+        # if not is_active:
+            # return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
         
-        if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
-            return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
+        # if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
+            # return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
         
         # Prüfe ob Gerät bereits registriert ist
-        cur.execute("""
-            SELECT id, is_active FROM device_registrations
-            WHERE license_id = %s AND hardware_id = %s
-        """, (license_id, hardware_id))
-        existing_device = cur.fetchone()
-        
-        if existing_device:
-            device_id, is_device_active = existing_device
-            if is_device_active:
+        # cur.execute("""
+            # SELECT id, is_active FROM device_registrations
+            # WHERE license_id = %s AND hardware_id = %s
+        # """, (license_id, hardware_id))
+        # existing_device = cur.fetchone()
+        
+        # if existing_device:
+            # device_id, is_device_active = existing_device
+            # if is_device_active:
                 # Gerät ist bereits aktiv, update last_seen
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET last_seen = CURRENT_TIMESTAMP,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
-            else:
+                # cur.execute("""
+                    # UPDATE device_registrations
+                    # SET last_seen = CURRENT_TIMESTAMP,
+                        # ip_address = %s,
+                        # user_agent = %s
+                    # WHERE id = %s
+                # """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                # conn.commit()
+                # return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
+            # else:
                 # Gerät war deaktiviert, prüfe ob wir es reaktivieren können
-                cur.execute("""
-                    SELECT COUNT(*) FROM device_registrations
-                    WHERE license_id = %s AND is_active = TRUE
-                """, (license_id,))
-                active_count = cur.fetchone()[0]
+                # cur.execute("""
+                    # SELECT COUNT(*) FROM device_registrations
+                    # WHERE license_id = %s AND is_active = TRUE
+                # """, (license_id,))
+                # active_count = cur.fetchone()[0]
                 
-                if active_count >= device_limit:
-                    return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+                # if active_count >= device_limit:
+                    # return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
                 
                 # Reaktiviere das Gerät
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET is_active = TRUE,
-                        last_seen = CURRENT_TIMESTAMP,
-                        deactivated_at = NULL,
-                        deactivated_by = NULL,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
+                # cur.execute("""
+                    # UPDATE device_registrations
+                    # SET is_active = TRUE,
+                        # last_seen = CURRENT_TIMESTAMP,
+                        # deactivated_at = NULL,
+                        # deactivated_by = NULL,
+                        # ip_address = %s,
+                        # user_agent = %s
+                    # WHERE id = %s
+                # """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                # conn.commit()
+                # return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
         
         # Neues Gerät - prüfe Gerätelimit
-        cur.execute("""
-            SELECT COUNT(*) FROM device_registrations
-            WHERE license_id = %s AND is_active = TRUE
-        """, (license_id,))
-        active_count = cur.fetchone()[0]
+        # cur.execute("""
+            # SELECT COUNT(*) FROM device_registrations
+            # WHERE license_id = %s AND is_active = TRUE
+        # """, (license_id,))
+        # active_count = cur.fetchone()[0]
         
-        if active_count >= device_limit:
-            return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+        # if active_count >= device_limit:
+            # return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
         
         # Registriere neues Gerät
-        cur.execute("""
-            INSERT INTO device_registrations 
-            (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
-            VALUES (%s, %s, %s, %s, %s, %s)
-            RETURNING id
-        """, (license_id, hardware_id, device_name, operating_system, 
-              get_client_ip(), request.headers.get('User-Agent', '')))
-        device_id = cur.fetchone()[0]
+        # cur.execute("""
+            # INSERT INTO device_registrations 
+            # (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
+            # VALUES (%s, %s, %s, %s, %s, %s)
+            # RETURNING id
+        # """, (license_id, hardware_id, device_name, operating_system, 
+              # get_client_ip(), request.headers.get('User-Agent', '')))
+        # device_id = cur.fetchone()[0]
         
-        conn.commit()
+        # conn.commit()
         
         # Audit Log
-        log_audit('DEVICE_REGISTER', 'device', device_id,
-                 new_values={'license_id': license_id, 'hardware_id': hardware_id})
+        # log_audit('DEVICE_REGISTER', 'device', device_id,
+                 # new_values={'license_id': license_id, 'hardware_id': hardware_id})
         
-        cur.close()
-        conn.close()
+        # cur.close()
+        # conn.close()
         
-        return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
+        # return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
         
-    except Exception as e:
-        logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
+    # except Exception as e:
+        # logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
+        # return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
 
-@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
-@login_required
+# @app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
+# @login_required
 def deactivate_device(license_id, device_id):
     """Deaktiviere ein registriertes Gerät"""
     try:
@@ -3423,8 +3437,8 @@ def deactivate_device(license_id, device_id):
         logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
         return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
 
-@app.route("/api/licenses/bulk-delete", methods=["POST"])
-@login_required
+# @app.route("/api/licenses/bulk-delete", methods=["POST"])
+# @login_required
 def bulk_delete_licenses():
     """Delete multiple licenses at once"""
     try:
@@ -3468,8 +3482,8 @@ def bulk_delete_licenses():
 
 # ===================== RESOURCE POOL MANAGEMENT =====================
 
-@app.route('/resources')
-@login_required
+# @app.route('/resources')
+# @login_required
 def resources():
     """Resource Pool Hauptübersicht"""
     conn = get_connection()
@@ -3608,8 +3622,8 @@ def resources():
                          datetime=datetime,
                          timedelta=timedelta)
 
-@app.route('/resources/add', methods=['GET', 'POST'])
-@login_required
+# @app.route('/resources/add', methods=['GET', 'POST'])
+# @login_required
 def add_resources():
     """Ressourcen zum Pool hinzufügen"""
     # Hole show_test Parameter für die Anzeige
@@ -3672,8 +3686,8 @@ def add_resources():
     
     return render_template('add_resources.html', show_test=show_test)
 
-@app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
-@login_required
+# @app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
+# @login_required
 def quarantine_resource(resource_id):
     """Ressource in Quarantäne setzen"""
     reason = request.form.get('reason', 'review')
@@ -3730,8 +3744,8 @@ def quarantine_resource(resource_id):
                           status=request.args.get('status', request.form.get('status', '')),
                           search=request.args.get('search', request.form.get('search', ''))))
 
-@app.route('/resources/release', methods=['POST'])
-@login_required
+# @app.route('/resources/release', methods=['POST'])
+# @login_required
 def release_resources():
     """Ressourcen aus Quarantäne freigeben"""
     resource_ids = request.form.getlist('resource_ids')
@@ -3781,8 +3795,8 @@ def release_resources():
                           status=request.args.get('status', request.form.get('status', '')),
                           search=request.args.get('search', request.form.get('search', ''))))
 
-@app.route('/api/resources/allocate', methods=['POST'])
-@login_required
+# @app.route('/api/resources/allocate', methods=['POST'])
+# @login_required
 def allocate_resources_api():
     """API für Ressourcen-Zuweisung bei Lizenzerstellung"""
     data = request.json
@@ -3929,8 +3943,8 @@ def allocate_resources_api():
             'error': str(e)
         }), 400
 
-@app.route('/api/resources/check-availability', methods=['GET'])
-@login_required
+# @app.route('/api/resources/check-availability', methods=['GET'])
+# @login_required
 def check_resource_availability():
     """Prüft verfügbare Ressourcen"""
     resource_type = request.args.get('type', '')
@@ -3988,8 +4002,8 @@ def check_resource_availability():
         
         return jsonify(availability)
 
-@app.route('/api/global-search', methods=['GET'])
-@login_required
+# @app.route('/api/global-search', methods=['GET'])
+# @login_required
 def global_search():
     """Global search API endpoint for searching customers and licenses"""
     query = request.args.get('q', '').strip()
@@ -4051,8 +4065,8 @@ def global_search():
         'licenses': licenses
     })
 
-@app.route('/resources/history/<int:resource_id>')
-@login_required
+# @app.route('/resources/history/<int:resource_id>')
+# @login_required
 def resource_history(resource_id):
     """Zeigt die komplette Historie einer Ressource"""
     conn = get_connection()
@@ -4138,8 +4152,8 @@ def resource_history(resource_id):
                          license_info=license_info,
                          history=history_objs)
 
-@app.route('/resources/metrics')
-@login_required
+# @app.route('/resources/metrics')
+# @login_required
 def resources_metrics():
     """Dashboard für Resource Metrics und Reports"""
     conn = get_connection()
@@ -4302,8 +4316,8 @@ def resources_metrics():
                          problem_resources=problem_resources,
                          daily_metrics=daily_metrics)
 
-@app.route('/resources/report', methods=['GET'])
-@login_required
+# @app.route('/resources/report', methods=['GET'])
+# @login_required
 def resources_report():
     """Generiert Ressourcen-Reports oder zeigt Report-Formular"""
     # Prüfe ob Download angefordert wurde
diff --git a/v2_adminpanel/app.py.backup b/v2_adminpanel/app.py.backup
index 96c85f1..398d007 100644
--- a/v2_adminpanel/app.py.backup
+++ b/v2_adminpanel/app.py.backup
@@ -1,5032 +1,5032 @@
-import os
-import psycopg2
-from psycopg2.extras import Json
-from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash
-from flask_session import Session
-from functools import wraps
-from dotenv import load_dotenv
-import pandas as pd
-from datetime import datetime, timedelta
-from zoneinfo import ZoneInfo
-import io
-import subprocess
-import gzip
-from cryptography.fernet import Fernet
-from pathlib import Path
-import time
-from apscheduler.schedulers.background import BackgroundScheduler
-import logging
-import random
-import hashlib
-import requests
-import secrets
-import string
-import re
-import bcrypt
-import pyotp
-import qrcode
-from io import BytesIO
-import base64
-import json
-from werkzeug.middleware.proxy_fix import ProxyFix
-from openpyxl.utils import get_column_letter
-
-load_dotenv()
-
-app = Flask(__name__)
-app.config['SECRET_KEY'] = os.urandom(24)
-app.config['SESSION_TYPE'] = 'filesystem'
-app.config['JSON_AS_ASCII'] = False  # JSON-Ausgabe mit UTF-8
-app.config['JSONIFY_MIMETYPE'] = 'application/json; charset=utf-8'
-app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=5)  # 5 Minuten Session-Timeout
-app.config['SESSION_COOKIE_HTTPONLY'] = True
-app.config['SESSION_COOKIE_SECURE'] = False  # Wird auf True gesetzt wenn HTTPS (intern läuft HTTP)
-app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
-app.config['SESSION_COOKIE_NAME'] = 'admin_session'
-# WICHTIG: Session-Cookie soll auch nach 5 Minuten ablaufen
-app.config['SESSION_REFRESH_EACH_REQUEST'] = False
-Session(app)
-
-# ProxyFix für korrekte IP-Adressen hinter Nginx
-app.wsgi_app = ProxyFix(
-    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
-)
-
-# Backup-Konfiguration
-BACKUP_DIR = Path("/app/backups")
-BACKUP_DIR.mkdir(exist_ok=True)
-
-# Rate-Limiting Konfiguration
-FAIL_MESSAGES = [
-    "NOPE!",
-    "ACCESS DENIED, TRY HARDER",
-    "WRONG! 🚫",
-    "COMPUTER SAYS NO",
-    "YOU FAILED"
-]
-
-MAX_LOGIN_ATTEMPTS = 5
-BLOCK_DURATION_HOURS = 24
-CAPTCHA_AFTER_ATTEMPTS = 2
-
-# Scheduler für automatische Backups
-scheduler = BackgroundScheduler()
-scheduler.start()
-
-# Logging konfigurieren
-logging.basicConfig(level=logging.INFO)
-
-
-# Login decorator
-def login_required(f):
-    @wraps(f)
-    def decorated_function(*args, **kwargs):
-        if 'logged_in' not in session:
-            return redirect(url_for('login'))
-        
-        # Prüfe ob Session abgelaufen ist
-        if 'last_activity' in session:
-            last_activity = datetime.fromisoformat(session['last_activity'])
-            time_since_activity = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) - last_activity
-            
-            # Debug-Logging
-            app.logger.info(f"Session check for {session.get('username', 'unknown')}: "
-                          f"Last activity: {last_activity}, "
-                          f"Time since: {time_since_activity.total_seconds()} seconds")
-            
-            if time_since_activity > timedelta(minutes=5):
-                # Session abgelaufen - Logout
-                username = session.get('username', 'unbekannt')
-                app.logger.info(f"Session timeout for user {username} - auto logout")
-                # Audit-Log für automatischen Logout (vor session.clear()!)
-                try:
-                    log_audit('AUTO_LOGOUT', 'session', additional_info={'reason': 'Session timeout (5 minutes)', 'username': username})
-                except:
-                    pass
-                session.clear()
-                flash('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.', 'warning')
-                return redirect(url_for('login'))
-        
-        # Aktivität NICHT automatisch aktualisieren
-        # Nur bei expliziten Benutzeraktionen (wird vom Heartbeat gemacht)
-        return f(*args, **kwargs)
-    return decorated_function
-
-# DB-Verbindung mit UTF-8 Encoding
-def get_connection():
-    conn = psycopg2.connect(
-        host=os.getenv("POSTGRES_HOST", "postgres"),
-        port=os.getenv("POSTGRES_PORT", "5432"),
-        dbname=os.getenv("POSTGRES_DB"),
-        user=os.getenv("POSTGRES_USER"),
-        password=os.getenv("POSTGRES_PASSWORD"),
-        options='-c client_encoding=UTF8'
-    )
-    conn.set_client_encoding('UTF8')
-    return conn
-
-# User Authentication Helper Functions
-def hash_password(password):
-    """Hash a password using bcrypt"""
-    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
-
-def verify_password(password, hashed):
-    """Verify a password against its hash"""
-    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
-
-def get_user_by_username(username):
-    """Get user from database by username"""
-    conn = get_connection()
-    cur = conn.cursor()
-    try:
-        cur.execute("""
-            SELECT id, username, password_hash, email, totp_secret, totp_enabled, 
-                   backup_codes, last_password_change, failed_2fa_attempts
-            FROM users WHERE username = %s
-        """, (username,))
-        user = cur.fetchone()
-        if user:
-            return {
-                'id': user[0],
-                'username': user[1],
-                'password_hash': user[2],
-                'email': user[3],
-                'totp_secret': user[4],
-                'totp_enabled': user[5],
-                'backup_codes': user[6],
-                'last_password_change': user[7],
-                'failed_2fa_attempts': user[8]
-            }
-        return None
-    finally:
-        cur.close()
-        conn.close()
-
-def generate_totp_secret():
-    """Generate a new TOTP secret"""
-    return pyotp.random_base32()
-
-def generate_qr_code(username, totp_secret):
-    """Generate QR code for TOTP setup"""
-    totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
-        name=username,
-        issuer_name='V2 Admin Panel'
-    )
-    
-    qr = qrcode.QRCode(version=1, box_size=10, border=5)
-    qr.add_data(totp_uri)
-    qr.make(fit=True)
-    
-    img = qr.make_image(fill_color="black", back_color="white")
-    buf = BytesIO()
-    img.save(buf, format='PNG')
-    buf.seek(0)
-    
-    return base64.b64encode(buf.getvalue()).decode()
-
-def verify_totp(totp_secret, token):
-    """Verify a TOTP token"""
-    totp = pyotp.TOTP(totp_secret)
-    return totp.verify(token, valid_window=1)
-
-def generate_backup_codes(count=8):
-    """Generate backup codes for 2FA recovery"""
-    codes = []
-    for _ in range(count):
-        code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
-        codes.append(code)
-    return codes
-
-def hash_backup_code(code):
-    """Hash a backup code for storage"""
-    return hashlib.sha256(code.encode()).hexdigest()
-
-def verify_backup_code(code, hashed_codes):
-    """Verify a backup code against stored hashes"""
-    code_hash = hashlib.sha256(code.encode()).hexdigest()
-    return code_hash in hashed_codes
-
-# Audit-Log-Funktion
-def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=None, additional_info=None):
-    """Protokolliert Änderungen im Audit-Log"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        username = session.get('username', 'system')
-        ip_address = get_client_ip() if request else None
-        user_agent = request.headers.get('User-Agent') if request else None
-        
-        # Debug logging
-        app.logger.info(f"Audit log - IP address captured: {ip_address}, Action: {action}, User: {username}")
-        
-        # Konvertiere Dictionaries zu JSONB
-        old_json = Json(old_values) if old_values else None
-        new_json = Json(new_values) if new_values else None
-        
-        cur.execute("""
-            INSERT INTO audit_log 
-            (username, action, entity_type, entity_id, old_values, new_values, 
-             ip_address, user_agent, additional_info)
-            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
-        """, (username, action, entity_type, entity_id, old_json, new_json, 
-              ip_address, user_agent, additional_info))
-        
-        conn.commit()
-    except Exception as e:
-        print(f"Audit log error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-
-# Verschlüsselungs-Funktionen
-def get_or_create_encryption_key():
-    """Holt oder erstellt einen Verschlüsselungsschlüssel"""
-    key_file = BACKUP_DIR / ".backup_key"
-    
-    # Versuche Key aus Umgebungsvariable zu lesen
-    env_key = os.getenv("BACKUP_ENCRYPTION_KEY")
-    if env_key:
-        try:
-            # Validiere den Key
-            Fernet(env_key.encode())
-            return env_key.encode()
-        except:
-            pass
-    
-    # Wenn kein gültiger Key in ENV, prüfe Datei
-    if key_file.exists():
-        return key_file.read_bytes()
-    
-    # Erstelle neuen Key
-    key = Fernet.generate_key()
-    key_file.write_bytes(key)
-    logging.info("Neuer Backup-Verschlüsselungsschlüssel erstellt")
-    return key
-
-# Backup-Funktionen
-def create_backup(backup_type="manual", created_by=None):
-    """Erstellt ein verschlüsseltes Backup der Datenbank"""
-    start_time = time.time()
-    timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")
-    filename = f"backup_v2docker_{timestamp}_encrypted.sql.gz.enc"
-    filepath = BACKUP_DIR / filename
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Backup-Eintrag erstellen
-    cur.execute("""
-        INSERT INTO backup_history 
-        (filename, filepath, backup_type, status, created_by, is_encrypted)
-        VALUES (%s, %s, %s, %s, %s, %s)
-        RETURNING id
-    """, (filename, str(filepath), backup_type, 'in_progress', 
-          created_by or 'system', True))
-    backup_id = cur.fetchone()[0]
-    conn.commit()
-    
-    try:
-        # PostgreSQL Dump erstellen
-        dump_command = [
-            'pg_dump',
-            '-h', os.getenv("POSTGRES_HOST", "postgres"),
-            '-p', os.getenv("POSTGRES_PORT", "5432"),
-            '-U', os.getenv("POSTGRES_USER"),
-            '-d', os.getenv("POSTGRES_DB"),
-            '--no-password',
-            '--verbose'
-        ]
-        
-        # PGPASSWORD setzen
-        env = os.environ.copy()
-        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
-        
-        # Dump ausführen
-        result = subprocess.run(dump_command, capture_output=True, text=True, env=env)
-        
-        if result.returncode != 0:
-            raise Exception(f"pg_dump failed: {result.stderr}")
-        
-        dump_data = result.stdout.encode('utf-8')
-        
-        # Komprimieren
-        compressed_data = gzip.compress(dump_data)
-        
-        # Verschlüsseln
-        key = get_or_create_encryption_key()
-        f = Fernet(key)
-        encrypted_data = f.encrypt(compressed_data)
-        
-        # Speichern
-        filepath.write_bytes(encrypted_data)
-        
-        # Statistiken sammeln
-        cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'")
-        tables_count = cur.fetchone()[0]
-        
-        cur.execute("""
-            SELECT SUM(n_live_tup) 
-            FROM pg_stat_user_tables
-        """)
-        records_count = cur.fetchone()[0] or 0
-        
-        duration = time.time() - start_time
-        filesize = filepath.stat().st_size
-        
-        # Backup-Eintrag aktualisieren
-        cur.execute("""
-            UPDATE backup_history 
-            SET status = %s, filesize = %s, tables_count = %s, 
-                records_count = %s, duration_seconds = %s
-            WHERE id = %s
-        """, ('success', filesize, tables_count, records_count, duration, backup_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('BACKUP', 'database', backup_id,
-                 additional_info=f"Backup erstellt: {filename} ({filesize} bytes)")
-        
-        # E-Mail-Benachrichtigung (wenn konfiguriert)
-        send_backup_notification(True, filename, filesize, duration)
-        
-        logging.info(f"Backup erfolgreich erstellt: {filename}")
-        return True, filename
-        
-    except Exception as e:
-        # Fehler protokollieren
-        cur.execute("""
-            UPDATE backup_history 
-            SET status = %s, error_message = %s, duration_seconds = %s
-            WHERE id = %s
-        """, ('failed', str(e), time.time() - start_time, backup_id))
-        conn.commit()
-        
-        logging.error(f"Backup fehlgeschlagen: {e}")
-        send_backup_notification(False, filename, error=str(e))
-        
-        return False, str(e)
-    
-    finally:
-        cur.close()
-        conn.close()
-
-def restore_backup(backup_id, encryption_key=None):
-    """Stellt ein Backup wieder her"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Backup-Info abrufen
-        cur.execute("""
-            SELECT filename, filepath, is_encrypted 
-            FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        backup_info = cur.fetchone()
-        
-        if not backup_info:
-            raise Exception("Backup nicht gefunden")
-        
-        filename, filepath, is_encrypted = backup_info
-        filepath = Path(filepath)
-        
-        if not filepath.exists():
-            raise Exception("Backup-Datei nicht gefunden")
-        
-        # Datei lesen
-        encrypted_data = filepath.read_bytes()
-        
-        # Entschlüsseln
-        if is_encrypted:
-            key = encryption_key.encode() if encryption_key else get_or_create_encryption_key()
-            try:
-                f = Fernet(key)
-                compressed_data = f.decrypt(encrypted_data)
-            except:
-                raise Exception("Entschlüsselung fehlgeschlagen. Falsches Passwort?")
-        else:
-            compressed_data = encrypted_data
-        
-        # Dekomprimieren
-        dump_data = gzip.decompress(compressed_data)
-        sql_commands = dump_data.decode('utf-8')
-        
-        # Bestehende Verbindungen schließen
-        cur.close()
-        conn.close()
-        
-        # Datenbank wiederherstellen
-        restore_command = [
-            'psql',
-            '-h', os.getenv("POSTGRES_HOST", "postgres"),
-            '-p', os.getenv("POSTGRES_PORT", "5432"),
-            '-U', os.getenv("POSTGRES_USER"),
-            '-d', os.getenv("POSTGRES_DB"),
-            '--no-password'
-        ]
-        
-        env = os.environ.copy()
-        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
-        
-        result = subprocess.run(restore_command, input=sql_commands, 
-                              capture_output=True, text=True, env=env)
-        
-        if result.returncode != 0:
-            raise Exception(f"Wiederherstellung fehlgeschlagen: {result.stderr}")
-        
-        # Audit-Log (neue Verbindung)
-        log_audit('RESTORE', 'database', backup_id,
-                 additional_info=f"Backup wiederhergestellt: {filename}")
-        
-        return True, "Backup erfolgreich wiederhergestellt"
-        
-    except Exception as e:
-        logging.error(f"Wiederherstellung fehlgeschlagen: {e}")
-        return False, str(e)
-
-def send_backup_notification(success, filename, filesize=None, duration=None, error=None):
-    """Sendet E-Mail-Benachrichtigung (wenn konfiguriert)"""
-    if not os.getenv("EMAIL_ENABLED", "false").lower() == "true":
-        return
-    
-    # E-Mail-Funktion vorbereitet aber deaktiviert
-    # TODO: Implementieren wenn E-Mail-Server konfiguriert ist
-    logging.info(f"E-Mail-Benachrichtigung vorbereitet: Backup {'erfolgreich' if success else 'fehlgeschlagen'}")
-
-# Scheduled Backup Job
-def scheduled_backup():
-    """Führt ein geplantes Backup aus"""
-    logging.info("Starte geplantes Backup...")
-    create_backup(backup_type="scheduled", created_by="scheduler")
-
-# Scheduler konfigurieren - täglich um 3:00 Uhr
-scheduler.add_job(
-    scheduled_backup,
-    'cron',
-    hour=3,
-    minute=0,
-    id='daily_backup',
-    replace_existing=True
-)
-
-# Rate-Limiting Funktionen
-def get_client_ip():
-    """Ermittelt die echte IP-Adresse des Clients"""
-    # Debug logging
-    app.logger.info(f"Headers - X-Real-IP: {request.headers.get('X-Real-IP')}, X-Forwarded-For: {request.headers.get('X-Forwarded-For')}, Remote-Addr: {request.remote_addr}")
-    
-    # Try X-Real-IP first (set by nginx)
-    if request.headers.get('X-Real-IP'):
-        return request.headers.get('X-Real-IP')
-    # Then X-Forwarded-For
-    elif request.headers.get('X-Forwarded-For'):
-        # X-Forwarded-For can contain multiple IPs, take the first one
-        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
-    # Fallback to remote_addr
-    else:
-        return request.remote_addr
-
-def check_ip_blocked(ip_address):
-    """Prüft ob eine IP-Adresse gesperrt ist"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT blocked_until FROM login_attempts
-        WHERE ip_address = %s AND blocked_until IS NOT NULL
-    """, (ip_address,))
-    
-    result = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    if result and result[0]:
-        if result[0] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None):
-            return True, result[0]
-    return False, None
-
-def record_failed_attempt(ip_address, username):
-    """Zeichnet einen fehlgeschlagenen Login-Versuch auf"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Random Fehlermeldung
-    error_message = random.choice(FAIL_MESSAGES)
-    
-    try:
-        # Prüfen ob IP bereits existiert
-        cur.execute("""
-            SELECT attempt_count FROM login_attempts
-            WHERE ip_address = %s
-        """, (ip_address,))
-        
-        result = cur.fetchone()
-        
-        if result:
-            # Update bestehenden Eintrag
-            new_count = result[0] + 1
-            blocked_until = None
-            
-            if new_count >= MAX_LOGIN_ATTEMPTS:
-                blocked_until = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) + timedelta(hours=BLOCK_DURATION_HOURS)
-                # E-Mail-Benachrichtigung (wenn aktiviert)
-                if os.getenv("EMAIL_ENABLED", "false").lower() == "true":
-                    send_security_alert_email(ip_address, username, new_count)
-            
-            cur.execute("""
-                UPDATE login_attempts
-                SET attempt_count = %s,
-                    last_attempt = CURRENT_TIMESTAMP,
-                    blocked_until = %s,
-                    last_username_tried = %s,
-                    last_error_message = %s
-                WHERE ip_address = %s
-            """, (new_count, blocked_until, username, error_message, ip_address))
-        else:
-            # Neuen Eintrag erstellen
-            cur.execute("""
-                INSERT INTO login_attempts 
-                (ip_address, attempt_count, last_username_tried, last_error_message)
-                VALUES (%s, 1, %s, %s)
-            """, (ip_address, username, error_message))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('LOGIN_FAILED', 'user', 
-                 additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
-        
-    except Exception as e:
-        print(f"Rate limiting error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-    
-    return error_message
-
-def reset_login_attempts(ip_address):
-    """Setzt die Login-Versuche für eine IP zurück"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        cur.execute("""
-            DELETE FROM login_attempts
-            WHERE ip_address = %s
-        """, (ip_address,))
-        conn.commit()
-    except Exception as e:
-        print(f"Reset attempts error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-
-def get_login_attempts(ip_address):
-    """Gibt die Anzahl der Login-Versuche für eine IP zurück"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT attempt_count FROM login_attempts
-        WHERE ip_address = %s
-    """, (ip_address,))
-    
-    result = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    return result[0] if result else 0
-
-def send_security_alert_email(ip_address, username, attempt_count):
-    """Sendet eine Sicherheitswarnung per E-Mail"""
-    subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
-    body = f"""
-    WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
-    
-    IP-Adresse: {ip_address}
-    Versuchter Benutzername: {username}
-    Anzahl Versuche: {attempt_count}
-    Zeit: {datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d %H:%M:%S')}
-    
-    Die IP-Adresse wurde für 24 Stunden gesperrt.
-    
-    Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
-    """
-    
-    # TODO: E-Mail-Versand implementieren wenn SMTP konfiguriert
-    logging.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
-    print(f"E-Mail würde gesendet: {subject}")
-
-def verify_recaptcha(response):
-    """Verifiziert die reCAPTCHA v2 Response mit Google"""
-    secret_key = os.getenv('RECAPTCHA_SECRET_KEY')
-    
-    # Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC)
-    if not secret_key:
-        logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen")
-        return True
-    
-    # Verifizierung bei Google
-    try:
-        verify_url = 'https://www.google.com/recaptcha/api/siteverify'
-        data = {
-            'secret': secret_key,
-            'response': response
-        }
-        
-        # Timeout für Request setzen
-        r = requests.post(verify_url, data=data, timeout=5)
-        result = r.json()
-        
-        # Log für Debugging
-        if not result.get('success'):
-            logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}")
-        
-        return result.get('success', False)
-    
-    except requests.exceptions.RequestException as e:
-        logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}")
-        # Bei Netzwerkfehlern CAPTCHA als bestanden werten
-        return True
-    except Exception as e:
-        logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
-        return False
-
-def generate_license_key(license_type='full'):
-    """
-    Generiert einen Lizenzschlüssel im Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ
-    
-    AF = Account Factory (Produktkennung)
-    F/T = F für Fullversion, T für Testversion
-    YYYY = Jahr
-    MM = Monat
-    XXXX-YYYY-ZZZZ = Zufällige alphanumerische Zeichen
-    """
-    # Erlaubte Zeichen (ohne verwirrende wie 0/O, 1/I/l)
-    chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
-    
-    # Datum-Teil
-    now = datetime.now(ZoneInfo("Europe/Berlin"))
-    date_part = now.strftime('%Y%m')
-    type_char = 'F' if license_type == 'full' else 'T'
-    
-    # Zufällige Teile generieren (3 Blöcke à 4 Zeichen)
-    parts = []
-    for _ in range(3):
-        part = ''.join(secrets.choice(chars) for _ in range(4))
-        parts.append(part)
-    
-    # Key zusammensetzen
-    key = f"AF-{type_char}-{date_part}-{parts[0]}-{parts[1]}-{parts[2]}"
-    
-    return key
-
-def validate_license_key(key):
-    """
-    Validiert das License Key Format
-    Erwartet: AF-F-YYYYMM-XXXX-YYYY-ZZZZ oder AF-T-YYYYMM-XXXX-YYYY-ZZZZ
-    """
-    if not key:
-        return False
-    
-    # Pattern für das neue Format
-    # AF- (fest) + F oder T + - + 6 Ziffern (YYYYMM) + - + 4 Zeichen + - + 4 Zeichen + - + 4 Zeichen
-    pattern = r'^AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
-    
-    # Großbuchstaben für Vergleich
-    return bool(re.match(pattern, key.upper()))
-
-@app.route("/login", methods=["GET", "POST"])
-def login():
-    # Timing-Attack Schutz - Start Zeit merken
-    start_time = time.time()
-    
-    # IP-Adresse ermitteln
-    ip_address = get_client_ip()
-    
-    # Prüfen ob IP gesperrt ist
-    is_blocked, blocked_until = check_ip_blocked(ip_address)
-    if is_blocked:
-        time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
-        error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
-        return render_template("login.html", error=error_msg, error_type="blocked")
-    
-    # Anzahl bisheriger Versuche
-    attempt_count = get_login_attempts(ip_address)
-    
-    if request.method == "POST":
-        username = request.form.get("username")
-        password = request.form.get("password")
-        captcha_response = request.form.get("g-recaptcha-response")
-        
-        # CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
-        recaptcha_site_key = os.getenv('RECAPTCHA_SITE_KEY')
-        if attempt_count >= CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
-            if not captcha_response:
-                # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA ERFORDERLICH!", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
-            
-            # CAPTCHA validieren
-            if not verify_recaptcha(captcha_response):
-                # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
-        
-        # Check user in database first, fallback to env vars
-        user = get_user_by_username(username)
-        login_success = False
-        needs_2fa = False
-        
-        if user:
-            # Database user authentication
-            if verify_password(password, user['password_hash']):
-                login_success = True
-                needs_2fa = user['totp_enabled']
-        else:
-            # Fallback to environment variables for backward compatibility
-            admin1_user = os.getenv("ADMIN1_USERNAME")
-            admin1_pass = os.getenv("ADMIN1_PASSWORD")
-            admin2_user = os.getenv("ADMIN2_USERNAME")
-            admin2_pass = os.getenv("ADMIN2_PASSWORD")
-            
-            if ((username == admin1_user and password == admin1_pass) or 
-                (username == admin2_user and password == admin2_pass)):
-                login_success = True
-        
-        # Timing-Attack Schutz - Mindestens 1 Sekunde warten
-        elapsed = time.time() - start_time
-        if elapsed < 1.0:
-            time.sleep(1.0 - elapsed)
-        
-        if login_success:
-            # Erfolgreicher Login
-            if needs_2fa:
-                # Store temporary session for 2FA verification
-                session['temp_username'] = username
-                session['temp_user_id'] = user['id']
-                session['awaiting_2fa'] = True
-                return redirect(url_for('verify_2fa'))
-            else:
-                # Complete login without 2FA
-                session.permanent = True  # Aktiviert das Timeout
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user['id'] if user else None
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                reset_login_attempts(ip_address)
-                log_audit('LOGIN_SUCCESS', 'user', 
-                         additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
-                return redirect(url_for('dashboard'))
-        else:
-            # Fehlgeschlagener Login
-            error_message = record_failed_attempt(ip_address, username)
-            new_attempt_count = get_login_attempts(ip_address)
-            
-            # Prüfen ob jetzt gesperrt
-            is_now_blocked, _ = check_ip_blocked(ip_address)
-            if is_now_blocked:
-                log_audit('LOGIN_BLOCKED', 'security', 
-                         additional_info=f"IP {ip_address} wurde nach {MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
-            
-            return render_template("login.html", 
-                                 error=error_message,
-                                 show_captcha=(new_attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
-                                 error_type="failed",
-                                 attempts_left=max(0, MAX_LOGIN_ATTEMPTS - new_attempt_count),
-                                 recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
-    
-    # GET Request
-    return render_template("login.html", 
-                         show_captcha=(attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
-                         attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                         recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
-
-@app.route("/logout")
-def logout():
-    username = session.get('username', 'unknown')
-    log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
-    session.pop('logged_in', None)
-    session.pop('username', None)
-    session.pop('user_id', None)
-    session.pop('temp_username', None)
-    session.pop('temp_user_id', None)
-    session.pop('awaiting_2fa', None)
-    return redirect(url_for('login'))
-
-@app.route("/verify-2fa", methods=["GET", "POST"])
-def verify_2fa():
-    if not session.get('awaiting_2fa'):
-        return redirect(url_for('login'))
-    
-    if request.method == "POST":
-        token = request.form.get('token', '').replace(' ', '')
-        username = session.get('temp_username')
-        user_id = session.get('temp_user_id')
-        
-        if not username or not user_id:
-            flash('Session expired. Please login again.', 'error')
-            return redirect(url_for('login'))
-        
-        user = get_user_by_username(username)
-        if not user:
-            flash('User not found.', 'error')
-            return redirect(url_for('login'))
-        
-        # Check if it's a backup code
-        if len(token) == 8 and token.isupper():
-            # Try backup code
-            backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
-            if verify_backup_code(token, backup_codes):
-                # Remove used backup code
-                code_hash = hash_backup_code(token)
-                backup_codes.remove(code_hash)
-                
-                conn = get_connection()
-                cur = conn.cursor()
-                cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
-                          (json.dumps(backup_codes), user_id))
-                conn.commit()
-                cur.close()
-                conn.close()
-                
-                # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
-                
-                flash('Login successful using backup code. Please generate new backup codes.', 'warning')
-                log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
-                return redirect(url_for('dashboard'))
-        else:
-            # Try TOTP token
-            if verify_totp(user['totp_secret'], token):
-                # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
-                
-                log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
-                return redirect(url_for('dashboard'))
-        
-        # Failed verification
-        conn = get_connection()
-        cur = conn.cursor()
-        cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
-                  (datetime.now(), user_id))
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        flash('Invalid authentication code. Please try again.', 'error')
-        log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
-    
-    return render_template('verify_2fa.html')
-
-@app.route("/profile")
-@login_required
-def profile():
-    user = get_user_by_username(session['username'])
-    if not user:
-        # For environment-based users, redirect with message
-        flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info')
-        return redirect(url_for('dashboard'))
-    return render_template('profile.html', user=user)
-
-@app.route("/profile/change-password", methods=["POST"])
-@login_required
-def change_password():
-    current_password = request.form.get('current_password')
-    new_password = request.form.get('new_password')
-    confirm_password = request.form.get('confirm_password')
-    
-    user = get_user_by_username(session['username'])
-    
-    # Verify current password
-    if not verify_password(current_password, user['password_hash']):
-        flash('Current password is incorrect.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Check new password
-    if new_password != confirm_password:
-        flash('New passwords do not match.', 'error')
-        return redirect(url_for('profile'))
-    
-    if len(new_password) < 8:
-        flash('Password must be at least 8 characters long.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Update password
-    new_hash = hash_password(new_password)
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s",
-              (new_hash, datetime.now(), user['id']))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], 
-             additional_info="Password changed successfully")
-    flash('Password changed successfully.', 'success')
-    return redirect(url_for('profile'))
-
-@app.route("/profile/setup-2fa")
-@login_required
-def setup_2fa():
-    user = get_user_by_username(session['username'])
-    
-    if user['totp_enabled']:
-        flash('2FA is already enabled for your account.', 'info')
-        return redirect(url_for('profile'))
-    
-    # Generate new TOTP secret
-    totp_secret = generate_totp_secret()
-    session['temp_totp_secret'] = totp_secret
-    
-    # Generate QR code
-    qr_code = generate_qr_code(user['username'], totp_secret)
-    
-    return render_template('setup_2fa.html', 
-                         totp_secret=totp_secret, 
-                         qr_code=qr_code)
-
-@app.route("/profile/enable-2fa", methods=["POST"])
-@login_required
-def enable_2fa():
-    token = request.form.get('token', '').replace(' ', '')
-    totp_secret = session.get('temp_totp_secret')
-    
-    if not totp_secret:
-        flash('2FA setup session expired. Please try again.', 'error')
-        return redirect(url_for('setup_2fa'))
-    
-    # Verify the token
-    if not verify_totp(totp_secret, token):
-        flash('Invalid authentication code. Please try again.', 'error')
-        return redirect(url_for('setup_2fa'))
-    
-    # Generate backup codes
-    backup_codes = generate_backup_codes()
-    hashed_codes = [hash_backup_code(code) for code in backup_codes]
-    
-    # Enable 2FA
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("""
-        UPDATE users 
-        SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s 
-        WHERE username = %s
-    """, (totp_secret, json.dumps(hashed_codes), session['username']))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    session.pop('temp_totp_secret', None)
-    
-    log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully")
-    
-    # Show backup codes
-    return render_template('backup_codes.html', backup_codes=backup_codes)
-
-@app.route("/profile/disable-2fa", methods=["POST"])
-@login_required
-def disable_2fa():
-    password = request.form.get('password')
-    user = get_user_by_username(session['username'])
-    
-    # Verify password
-    if not verify_password(password, user['password_hash']):
-        flash('Incorrect password.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Disable 2FA
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("""
-        UPDATE users 
-        SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL 
-        WHERE username = %s
-    """, (session['username'],))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled")
-    flash('2FA has been disabled for your account.', 'success')
-    return redirect(url_for('profile'))
-
-@app.route("/heartbeat", methods=['POST'])
-@login_required
-def heartbeat():
-    """Endpoint für Session Keep-Alive - aktualisiert last_activity"""
-    # Aktualisiere last_activity nur wenn explizit angefordert
-    session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-    # Force session save
-    session.modified = True
-    
-    return jsonify({
-        'status': 'ok',
-        'last_activity': session['last_activity'],
-        'username': session.get('username')
-    })
-
-@app.route("/api/generate-license-key", methods=['POST'])
-@login_required
-def api_generate_key():
-    """API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
-    try:
-        # Lizenztyp aus Request holen (default: full)
-        data = request.get_json() or {}
-        license_type = data.get('type', 'full')
-        
-        # Key generieren
-        key = generate_license_key(license_type)
-        
-        # Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Wiederhole bis eindeutiger Key gefunden
-        attempts = 0
-        while attempts < 10:  # Max 10 Versuche
-            cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
-            if not cur.fetchone():
-                break  # Key ist eindeutig
-            key = generate_license_key(license_type)
-            attempts += 1
-        
-        cur.close()
-        conn.close()
-        
-        # Log für Audit
-        log_audit('GENERATE_KEY', 'license', 
-                 additional_info={'type': license_type, 'key': key})
-        
-        return jsonify({
-            'success': True,
-            'key': key,
-            'type': license_type
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Key-Generierung: {str(e)}")
-        return jsonify({
-            'success': False,
-            'error': 'Fehler bei der Key-Generierung'
-        }), 500
-
-@app.route("/api/customers", methods=['GET'])
-@login_required
-def api_customers():
-    """API Endpoint für die Kundensuche mit Select2"""
-    try:
-        # Suchparameter
-        search = request.args.get('q', '').strip()
-        page = request.args.get('page', 1, type=int)
-        per_page = 20
-        customer_id = request.args.get('id', type=int)
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Einzelnen Kunden per ID abrufen
-        if customer_id:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                WHERE c.id = %s
-                GROUP BY c.id, c.name, c.email
-            """, (customer_id,))
-            
-            customer = cur.fetchone()
-            results = []
-            if customer:
-                results.append({
-                    'id': customer[0],
-                    'text': f"{customer[1]} ({customer[2]})",
-                    'name': customer[1],
-                    'email': customer[2],
-                    'license_count': customer[3]
-                })
-            
-            cur.close()
-            conn.close()
-            
-            return jsonify({
-                'results': results,
-                'pagination': {'more': False}
-            })
-        
-        # SQL Query mit optionaler Suche
-        elif search:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                WHERE LOWER(c.name) LIKE LOWER(%s)
-                   OR LOWER(c.email) LIKE LOWER(%s)
-                GROUP BY c.id, c.name, c.email
-                ORDER BY c.name
-                LIMIT %s OFFSET %s
-            """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
-        else:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                GROUP BY c.id, c.name, c.email
-                ORDER BY c.name
-                LIMIT %s OFFSET %s
-            """, (per_page, (page - 1) * per_page))
-        
-        customers = cur.fetchall()
-        
-        # Format für Select2
-        results = []
-        for customer in customers:
-            results.append({
-                'id': customer[0],
-                'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
-                'name': customer[1],
-                'email': customer[2],
-                'license_count': customer[3]
-            })
-        
-        # Gesamtanzahl für Pagination
-        if search:
-            cur.execute("""
-                SELECT COUNT(*) FROM customers
-                WHERE LOWER(name) LIKE LOWER(%s)
-                   OR LOWER(email) LIKE LOWER(%s)
-            """, (f'%{search}%', f'%{search}%'))
-        else:
-            cur.execute("SELECT COUNT(*) FROM customers")
-        
-        total_count = cur.fetchone()[0]
-        
-        cur.close()
-        conn.close()
-        
-        # Select2 Response Format
-        return jsonify({
-            'results': results,
-            'pagination': {
-                'more': (page * per_page) < total_count
-            }
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Kundensuche: {str(e)}")
-        return jsonify({
-            'results': [],
-            'error': 'Fehler bei der Kundensuche'
-        }), 500
-
-@app.route("/")
-@login_required
-def dashboard():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Statistiken abrufen
-    # Gesamtanzahl Kunden (ohne Testdaten)
-    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE")
-    total_customers = cur.fetchone()[0]
-    
-    # Gesamtanzahl Lizenzen (ohne Testdaten)
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE")
-    total_licenses = cur.fetchone()[0]
-    
-    # Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE
-    """)
-    active_licenses = cur.fetchone()[0]
-    
-    # Aktive Sessions
-    cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
-    active_sessions_count = cur.fetchone()[0]
-    
-    # Abgelaufene Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until < CURRENT_DATE AND is_test = FALSE
-    """)
-    expired_licenses = cur.fetchone()[0]
-    
-    # Deaktivierte Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE is_active = FALSE AND is_test = FALSE
-    """)
-    inactive_licenses = cur.fetchone()[0]
-    
-    # Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until >= CURRENT_DATE 
-        AND valid_until < CURRENT_DATE + INTERVAL '30 days'
-        AND is_active = TRUE
-        AND is_test = FALSE
-    """)
-    expiring_soon = cur.fetchone()[0]
-    
-    # Testlizenzen vs Vollversionen (ohne Testdaten)
-    cur.execute("""
-        SELECT license_type, COUNT(*) 
-        FROM licenses 
-        WHERE is_test = FALSE
-        GROUP BY license_type
-    """)
-    license_types = dict(cur.fetchall())
-    
-    # Anzahl Testdaten
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE")
-    test_data_count = cur.fetchone()[0]
-    
-    # Anzahl Test-Kunden
-    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE")
-    test_customers_count = cur.fetchone()[0]
-    
-    # Anzahl Test-Ressourcen
-    cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE")
-    test_resources_count = cur.fetchone()[0]
-    
-    # Letzte 5 erstellten Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, l.valid_until,
-               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 as status
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.is_test = FALSE
-        ORDER BY l.id DESC
-        LIMIT 5
-    """)
-    recent_licenses = cur.fetchall()
-    
-    # Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten)
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, l.valid_until,
-               l.valid_until - CURRENT_DATE as days_left
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.valid_until >= CURRENT_DATE 
-        AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
-        AND l.is_active = TRUE
-        AND l.is_test = FALSE
-        ORDER BY l.valid_until
-        LIMIT 10
-    """)
-    expiring_licenses = cur.fetchall()
-    
-    # Letztes Backup
-    cur.execute("""
-        SELECT created_at, filesize, duration_seconds, backup_type, status
-        FROM backup_history 
-        ORDER BY created_at DESC 
-        LIMIT 1
-    """)
-    last_backup_info = cur.fetchone()
-    
-    # Sicherheitsstatistiken
-    # Gesperrte IPs
-    cur.execute("""
-        SELECT COUNT(*) FROM login_attempts 
-        WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP
-    """)
-    blocked_ips_count = cur.fetchone()[0]
-    
-    # Fehlversuche heute
-    cur.execute("""
-        SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts
-        WHERE last_attempt::date = CURRENT_DATE
-    """)
-    failed_attempts_today = cur.fetchone()[0]
-    
-    # Letzte 5 Sicherheitsereignisse
-    cur.execute("""
-        SELECT 
-            la.ip_address, 
-            la.attempt_count, 
-            la.last_attempt, 
-            la.blocked_until,
-            la.last_username_tried,
-            la.last_error_message
-        FROM login_attempts la
-        ORDER BY la.last_attempt DESC
-        LIMIT 5
-    """)
-    recent_security_events = []
-    for event in cur.fetchall():
-        recent_security_events.append({
-            'ip_address': event[0],
-            'attempt_count': event[1],
-            'last_attempt': event[2].strftime('%d.%m %H:%M'),
-            'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) else None,
-            'username_tried': event[4],
-            'error_message': event[5]
-        })
-    
-    # Sicherheitslevel berechnen
-    if blocked_ips_count > 5 or failed_attempts_today > 50:
-        security_level = 'danger'
-        security_level_text = 'KRITISCH'
-    elif blocked_ips_count > 2 or failed_attempts_today > 20:
-        security_level = 'warning'
-        security_level_text = 'ERHÖHT'
-    else:
-        security_level = 'success'
-        security_level_text = 'NORMAL'
-    
-    # Resource Pool Statistiken (nur Live-Daten, keine Testdaten)
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'available') as available,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-            COUNT(*) as total
-        FROM resource_pools
-        WHERE is_test = FALSE
-        GROUP BY resource_type
-    """)
-    
-    resource_stats = {}
-    resource_warning = None
-    
-    for row in cur.fetchall():
-        available_percent = round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
-        resource_stats[row[0]] = {
-            'available': row[1],
-            'allocated': row[2],
-            'quarantine': row[3],
-            'total': row[4],
-            'available_percent': available_percent,
-            'warning_level': 'danger' if row[1] < 50 else 'warning' if row[1] < 100 else 'success'
-        }
-        
-        # Warnung bei niedrigem Bestand
-        if row[1] < 50:
-            if not resource_warning:
-                resource_warning = f"Niedriger Bestand bei {row[0].upper()}: nur noch {row[1]} verfügbar!"
-            else:
-                resource_warning += f" | {row[0].upper()}: {row[1]}"
-    
-    cur.close()
-    conn.close()
-    
-    stats = {
-        'total_customers': total_customers,
-        'total_licenses': total_licenses,
-        'active_licenses': active_licenses,
-        'expired_licenses': expired_licenses,
-        'inactive_licenses': inactive_licenses,
-        'expiring_soon': expiring_soon,
-        'full_licenses': license_types.get('full', 0),
-        'test_licenses': license_types.get('test', 0),
-        'test_data_count': test_data_count,
-        'test_customers_count': test_customers_count,
-        'test_resources_count': test_resources_count,
-        'recent_licenses': recent_licenses,
-        'expiring_licenses': expiring_licenses,
-        'active_sessions': active_sessions_count,
-        'last_backup': last_backup_info,
-        # Sicherheitsstatistiken
-        'blocked_ips_count': blocked_ips_count,
-        'failed_attempts_today': failed_attempts_today,
-        'recent_security_events': recent_security_events,
-        'security_level': security_level,
-        'security_level_text': security_level_text,
-        'resource_stats': resource_stats
-    }
-    
-    return render_template("dashboard.html", 
-                         stats=stats, 
-                         resource_stats=resource_stats,
-                         resource_warning=resource_warning,
-                         username=session.get('username'))
-
-@app.route("/create", methods=["GET", "POST"])
-@login_required
-def create_license():
-    if request.method == "POST":
-        customer_id = request.form.get("customer_id")
-        license_key = request.form["license_key"].upper()  # Immer Großbuchstaben
-        license_type = request.form["license_type"]
-        valid_from = request.form["valid_from"]
-        is_test = request.form.get("is_test") == "on"  # Checkbox value
-        
-        # Berechne valid_until basierend auf Laufzeit
-        duration = int(request.form.get("duration", 1))
-        duration_type = request.form.get("duration_type", "years")
-        
-        from datetime import datetime, timedelta
-        from dateutil.relativedelta import relativedelta
-        
-        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
-        
-        if duration_type == "days":
-            end_date = start_date + timedelta(days=duration)
-        elif duration_type == "months":
-            end_date = start_date + relativedelta(months=duration)
-        else:  # years
-            end_date = start_date + relativedelta(years=duration)
-        
-        # Ein Tag abziehen, da der Starttag mitgezählt wird
-        end_date = end_date - timedelta(days=1)
-        valid_until = end_date.strftime("%Y-%m-%d")
-
-        # Validiere License Key Format
-        if not validate_license_key(license_key):
-            flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error')
-            return redirect(url_for('create_license'))
-        
-        # Resource counts
-        domain_count = int(request.form.get("domain_count", 1))
-        ipv4_count = int(request.form.get("ipv4_count", 1))
-        phone_count = int(request.form.get("phone_count", 1))
-        device_limit = int(request.form.get("device_limit", 3))
-
-        conn = get_connection()
-        cur = conn.cursor()
-
-        try:
-            # Prüfe ob neuer Kunde oder bestehender
-            if customer_id == "new":
-                # Neuer Kunde
-                name = request.form.get("customer_name")
-                email = request.form.get("email")
-                
-                if not name:
-                    flash('Kundenname ist erforderlich!', 'error')
-                    return redirect(url_for('create_license'))
-                
-                # Prüfe ob E-Mail bereits existiert
-                if email:
-                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
-                    existing = cur.fetchone()
-                    if existing:
-                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
-                        return redirect(url_for('create_license'))
-                
-                # Kunde einfügen (erbt Test-Status von Lizenz)
-                cur.execute("""
-                    INSERT INTO customers (name, email, is_test, created_at)
-                    VALUES (%s, %s, %s, NOW())
-                    RETURNING id
-                """, (name, email, is_test))
-                customer_id = cur.fetchone()[0]
-                customer_info = {'name': name, 'email': email, 'is_test': is_test}
-                
-                # Audit-Log für neuen Kunden
-                log_audit('CREATE', 'customer', customer_id, 
-                         new_values={'name': name, 'email': email, 'is_test': is_test})
-            else:
-                # Bestehender Kunde - hole Infos für Audit-Log
-                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-                customer_data = cur.fetchone()
-                if not customer_data:
-                    flash('Kunde nicht gefunden!', 'error')
-                    return redirect(url_for('create_license'))
-                customer_info = {'name': customer_data[0], 'email': customer_data[1]}
-                
-                # Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
-                if customer_data[2]:  # is_test des Kunden
-                    is_test = True
-
-            # Lizenz hinzufügen
-            cur.execute("""
-                INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active, 
-                                    domain_count, ipv4_count, phone_count, device_limit, is_test)
-                VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
-                RETURNING id
-            """, (license_key, customer_id, license_type, valid_from, valid_until, 
-                  domain_count, ipv4_count, phone_count, device_limit, is_test))
-            license_id = cur.fetchone()[0]
-            
-            # Ressourcen zuweisen
-            try:
-                # Prüfe Verfügbarkeit
-                cur.execute("""
-                    SELECT 
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
-                """, (is_test, is_test, is_test))
-                available = cur.fetchone()
-                
-                if available[0] < domain_count:
-                    raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})")
-                if available[1] < ipv4_count:
-                    raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})")
-                if available[2] < phone_count:
-                    raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})")
-                
-                # Domains zuweisen
-                if domain_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, domain_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # IPv4s zuweisen
-                if ipv4_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, ipv4_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # Telefonnummern zuweisen
-                if phone_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, phone_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-            except ValueError as e:
-                conn.rollback()
-                flash(str(e), 'error')
-                return redirect(url_for('create_license'))
-
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE', 'license', license_id, 
-                     new_values={
-                         'license_key': license_key,
-                         'customer_name': customer_info['name'],
-                         'customer_email': customer_info['email'],
-                         'license_type': license_type,
-                         'valid_from': valid_from,
-                         'valid_until': valid_until,
-                         'device_limit': device_limit,
-                         'is_test': is_test
-                     })
-            
-            flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
-            
-        except Exception as e:
-            conn.rollback()
-            logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}")
-            flash('Fehler beim Erstellen der Lizenz!', 'error')
-        finally:
-            cur.close()
-            conn.close()
-
-        # Preserve show_test parameter if present
-        redirect_url = "/create"
-        if request.args.get('show_test') == 'true':
-            redirect_url += "?show_test=true"
-        return redirect(redirect_url)
-
-    # Unterstützung für vorausgewählten Kunden
-    preselected_customer_id = request.args.get('customer_id', type=int)
-    return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
-
-@app.route("/batch", methods=["GET", "POST"])
-@login_required
-def batch_licenses():
-    """Batch-Generierung mehrerer Lizenzen für einen Kunden"""
-    if request.method == "POST":
-        # Formulardaten
-        customer_id = request.form.get("customer_id")
-        license_type = request.form["license_type"]
-        quantity = int(request.form["quantity"])
-        valid_from = request.form["valid_from"]
-        is_test = request.form.get("is_test") == "on"  # Checkbox value
-        
-        # Berechne valid_until basierend auf Laufzeit
-        duration = int(request.form.get("duration", 1))
-        duration_type = request.form.get("duration_type", "years")
-        
-        from datetime import datetime, timedelta
-        from dateutil.relativedelta import relativedelta
-        
-        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
-        
-        if duration_type == "days":
-            end_date = start_date + timedelta(days=duration)
-        elif duration_type == "months":
-            end_date = start_date + relativedelta(months=duration)
-        else:  # years
-            end_date = start_date + relativedelta(years=duration)
-        
-        # Ein Tag abziehen, da der Starttag mitgezählt wird
-        end_date = end_date - timedelta(days=1)
-        valid_until = end_date.strftime("%Y-%m-%d")
-        
-        # Resource counts
-        domain_count = int(request.form.get("domain_count", 1))
-        ipv4_count = int(request.form.get("ipv4_count", 1))
-        phone_count = int(request.form.get("phone_count", 1))
-        device_limit = int(request.form.get("device_limit", 3))
-        
-        # Sicherheitslimit
-        if quantity < 1 or quantity > 100:
-            flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
-            return redirect(url_for('batch_licenses'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        try:
-            # Prüfe ob neuer Kunde oder bestehender
-            if customer_id == "new":
-                # Neuer Kunde
-                name = request.form.get("customer_name")
-                email = request.form.get("email")
-                
-                if not name:
-                    flash('Kundenname ist erforderlich!', 'error')
-                    return redirect(url_for('batch_licenses'))
-                
-                # Prüfe ob E-Mail bereits existiert
-                if email:
-                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
-                    existing = cur.fetchone()
-                    if existing:
-                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
-                        return redirect(url_for('batch_licenses'))
-                
-                # Kunde einfügen (erbt Test-Status von Lizenz)
-                cur.execute("""
-                    INSERT INTO customers (name, email, is_test, created_at)
-                    VALUES (%s, %s, %s, NOW())
-                    RETURNING id
-                """, (name, email, is_test))
-                customer_id = cur.fetchone()[0]
-                
-                # Audit-Log für neuen Kunden
-                log_audit('CREATE', 'customer', customer_id, 
-                         new_values={'name': name, 'email': email, 'is_test': is_test})
-            else:
-                # Bestehender Kunde - hole Infos
-                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-                customer_data = cur.fetchone()
-                if not customer_data:
-                    flash('Kunde nicht gefunden!', 'error')
-                    return redirect(url_for('batch_licenses'))
-                name = customer_data[0]
-                email = customer_data[1]
-                
-                # Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren
-                if customer_data[2]:  # is_test des Kunden
-                    is_test = True
-            
-            # Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
-            total_domains_needed = domain_count * quantity
-            total_ipv4s_needed = ipv4_count * quantity
-            total_phones_needed = phone_count * quantity
-            
-            cur.execute("""
-                SELECT 
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
-            """, (is_test, is_test, is_test))
-            available = cur.fetchone()
-            
-            if available[0] < total_domains_needed:
-                flash(f"Nicht genügend Domains verfügbar (benötigt: {total_domains_needed}, verfügbar: {available[0]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            if available[1] < total_ipv4s_needed:
-                flash(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {total_ipv4s_needed}, verfügbar: {available[1]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            if available[2] < total_phones_needed:
-                flash(f"Nicht genügend Telefonnummern verfügbar (benötigt: {total_phones_needed}, verfügbar: {available[2]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            
-            # Lizenzen generieren und speichern
-            generated_licenses = []
-            for i in range(quantity):
-                # Eindeutigen Key generieren
-                attempts = 0
-                while attempts < 10:
-                    license_key = generate_license_key(license_type)
-                    cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
-                    if not cur.fetchone():
-                        break
-                    attempts += 1
-                
-                # Lizenz einfügen
-                cur.execute("""
-                    INSERT INTO licenses (license_key, customer_id, license_type, is_test, 
-                                        valid_from, valid_until, is_active,
-                                        domain_count, ipv4_count, phone_count, device_limit)
-                    VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
-                    RETURNING id
-                """, (license_key, customer_id, license_type, is_test, valid_from, valid_until,
-                      domain_count, ipv4_count, phone_count, device_limit))
-                license_id = cur.fetchone()[0]
-                
-                # Ressourcen für diese Lizenz zuweisen
-                # Domains
-                if domain_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, domain_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # IPv4s
-                if ipv4_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, ipv4_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # Telefonnummern
-                if phone_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, phone_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                generated_licenses.append({
-                    'id': license_id,
-                    'key': license_key,
-                    'type': license_type
-                })
-            
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE_BATCH', 'license', 
-                     new_values={'customer': name, 'quantity': quantity, 'type': license_type},
-                     additional_info=f"Batch-Generierung von {quantity} Lizenzen")
-            
-            # Session für Export speichern
-            session['batch_export'] = {
-                'customer': name,
-                'email': email,
-                'licenses': generated_licenses,
-                'valid_from': valid_from,
-                'valid_until': valid_until,
-                'timestamp': datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-            }
-            
-            flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
-            return render_template("batch_result.html", 
-                                 customer=name,
-                                 email=email,
-                                 licenses=generated_licenses,
-                                 valid_from=valid_from,
-                                 valid_until=valid_until)
-            
-        except Exception as e:
-            conn.rollback()
-            logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
-            flash('Fehler bei der Batch-Generierung!', 'error')
-            return redirect(url_for('batch_licenses'))
-        finally:
-            cur.close()
-            conn.close()
-    
-    # GET Request
-    return render_template("batch_form.html")
-
-@app.route("/batch/export")
-@login_required
-def export_batch():
-    """Exportiert die zuletzt generierten Batch-Lizenzen"""
-    batch_data = session.get('batch_export')
-    if not batch_data:
-        flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
-        return redirect(url_for('batch_licenses'))
-    
-    # CSV generieren
-    output = io.StringIO()
-    output.write('\ufeff')  # UTF-8 BOM für Excel
-    
-    # Header
-    output.write(f"Kunde: {batch_data['customer']}\n")
-    output.write(f"E-Mail: {batch_data['email']}\n")
-    output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
-    output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
-    output.write("\n")
-    output.write("Nr;Lizenzschlüssel;Typ\n")
-    
-    # Lizenzen
-    for i, license in enumerate(batch_data['licenses'], 1):
-        typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
-        output.write(f"{i};{license['key']};{typ_text}\n")
-    
-    output.seek(0)
-    
-    # Audit-Log
-    log_audit('EXPORT', 'batch_licenses', 
-             additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
-    
-    return send_file(
-        io.BytesIO(output.getvalue().encode('utf-8-sig')),
-        mimetype='text/csv',
-        as_attachment=True,
-        download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv"
-    )
-
-@app.route("/licenses")
-@login_required
-def licenses():
-    # Redirect zur kombinierten Ansicht
-    return redirect("/customers-licenses")
-
-@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
-@login_required
-def edit_license(license_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if request.method == "POST":
-        # Alte Werte für Audit-Log abrufen
-        cur.execute("""
-            SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit 
-            FROM licenses WHERE id = %s
-        """, (license_id,))
-        old_license = cur.fetchone()
-        
-        # Update license
-        license_key = request.form["license_key"]
-        license_type = request.form["license_type"]
-        valid_from = request.form["valid_from"]
-        valid_until = request.form["valid_until"]
-        is_active = request.form.get("is_active") == "on"
-        is_test = request.form.get("is_test") == "on"
-        device_limit = int(request.form.get("device_limit", 3))
-        
-        cur.execute("""
-            UPDATE licenses 
-            SET license_key = %s, license_type = %s, valid_from = %s, 
-                valid_until = %s, is_active = %s, is_test = %s, device_limit = %s
-            WHERE id = %s
-        """, (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('UPDATE', 'license', license_id,
-                 old_values={
-                     'license_key': old_license[0],
-                     'license_type': old_license[1],
-                     'valid_from': str(old_license[2]),
-                     'valid_until': str(old_license[3]),
-                     'is_active': old_license[4],
-                     'is_test': old_license[5],
-                     'device_limit': old_license[6]
-                 },
-                 new_values={
-                     'license_key': license_key,
-                     'license_type': license_type,
-                     'valid_from': valid_from,
-                     'valid_until': valid_until,
-                     'is_active': is_active,
-                     'is_test': is_test,
-                     'device_limit': device_limit
-                 })
-        
-        cur.close()
-        conn.close()
-        
-        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
-        redirect_url = "/customers-licenses"
-        
-        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
-        show_test = request.form.get('show_test') or request.args.get('show_test')
-        if show_test == 'true':
-            redirect_url += "?show_test=true"
-            
-        # Behalte customer_id bei wenn vorhanden
-        if request.referrer and 'customer_id=' in request.referrer:
-            import re
-            match = re.search(r'customer_id=(\d+)', request.referrer)
-            if match:
-                connector = "&" if "?" in redirect_url else "?"
-                redirect_url += f"{connector}customer_id={match.group(1)}"
-                
-        return redirect(redirect_url)
-    
-    # Get license data
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, c.email, l.license_type, 
-               l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.id = %s
-    """, (license_id,))
-    
-    license = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    if not license:
-        return redirect("/licenses")
-    
-    return render_template("edit_license.html", license=license, username=session.get('username'))
-
-@app.route("/license/delete/<int:license_id>", methods=["POST"])
-@login_required
-def delete_license(license_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Lizenzdetails für Audit-Log abrufen
-    cur.execute("""
-        SELECT l.license_key, c.name, l.license_type 
-        FROM licenses l 
-        JOIN customers c ON l.customer_id = c.id 
-        WHERE l.id = %s
-    """, (license_id,))
-    license_info = cur.fetchone()
-    
-    cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
-    
-    conn.commit()
-    
-    # Audit-Log
-    if license_info:
-        log_audit('DELETE', 'license', license_id,
-                 old_values={
-                     'license_key': license_info[0],
-                     'customer_name': license_info[1],
-                     'license_type': license_info[2]
-                 })
-    
-    cur.close()
-    conn.close()
-    
-    return redirect("/licenses")
-
-@app.route("/customers")
-@login_required
-def customers():
-    # Redirect zur kombinierten Ansicht
-    return redirect("/customers-licenses")
-
-@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
-@login_required
-def edit_customer(customer_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if request.method == "POST":
-        # Alte Werte für Audit-Log abrufen
-        cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-        old_customer = cur.fetchone()
-        
-        # Update customer
-        name = request.form["name"]
-        email = request.form["email"]
-        is_test = request.form.get("is_test") == "on"
-        
-        cur.execute("""
-            UPDATE customers 
-            SET name = %s, email = %s, is_test = %s
-            WHERE id = %s
-        """, (name, email, is_test, customer_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('UPDATE', 'customer', customer_id,
-                 old_values={
-                     'name': old_customer[0],
-                     'email': old_customer[1],
-                     'is_test': old_customer[2]
-                 },
-                 new_values={
-                     'name': name,
-                     'email': email,
-                     'is_test': is_test
-                 })
-        
-        cur.close()
-        conn.close()
-        
-        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
-        redirect_url = "/customers-licenses"
-        
-        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
-        show_test = request.form.get('show_test') or request.args.get('show_test')
-        if show_test == 'true':
-            redirect_url += "?show_test=true"
-            
-        # Behalte customer_id bei (immer der aktuelle Kunde)
-        connector = "&" if "?" in redirect_url else "?"
-        redirect_url += f"{connector}customer_id={customer_id}"
-                
-        return redirect(redirect_url)
-    
-    # Get customer data with licenses
-    cur.execute("""
-        SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s
-    """, (customer_id,))
-    
-    customer = cur.fetchone()
-    if not customer:
-        cur.close()
-        conn.close()
-        return "Kunde nicht gefunden", 404
-    
-    
-    # Get customer's licenses
-    cur.execute("""
-        SELECT id, license_key, license_type, valid_from, valid_until, is_active
-        FROM licenses
-        WHERE customer_id = %s
-        ORDER BY valid_until DESC
-    """, (customer_id,))
-    
-    licenses = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    if not customer:
-        return redirect("/customers-licenses")
-    
-    return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
-
-@app.route("/customer/create", methods=["GET", "POST"])
-@login_required
-def create_customer():
-    """Erstellt einen neuen Kunden ohne Lizenz"""
-    if request.method == "POST":
-        name = request.form.get('name')
-        email = request.form.get('email')
-        is_test = request.form.get('is_test') == 'on'
-        
-        if not name or not email:
-            flash("Name und E-Mail sind Pflichtfelder!", "error")
-            return render_template("create_customer.html", username=session.get('username'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        try:
-            # Prüfen ob E-Mail bereits existiert
-            cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
-            existing = cur.fetchone()
-            if existing:
-                flash(f"Ein Kunde mit der E-Mail '{email}' existiert bereits: {existing[1]}", "error")
-                return render_template("create_customer.html", username=session.get('username'))
-            
-            # Kunde erstellen
-            cur.execute("""
-                INSERT INTO customers (name, email, created_at, is_test) 
-                VALUES (%s, %s, %s, %s) RETURNING id
-            """, (name, email, datetime.now(), is_test))
-            
-            customer_id = cur.fetchone()[0]
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE', 'customer', customer_id,
-                     new_values={
-                         'name': name,
-                         'email': email,
-                         'is_test': is_test
-                     })
-            
-            flash(f"Kunde '{name}' wurde erfolgreich angelegt!", "success")
-            return redirect(f"/customer/edit/{customer_id}")
-            
-        except Exception as e:
-            conn.rollback()
-            flash(f"Fehler beim Anlegen des Kunden: {str(e)}", "error")
-            return render_template("create_customer.html", username=session.get('username'))
-        finally:
-            cur.close()
-            conn.close()
-    
-    # GET Request - Formular anzeigen
-    return render_template("create_customer.html", username=session.get('username'))
-
-@app.route("/customer/delete/<int:customer_id>", methods=["POST"])
-@login_required
-def delete_customer(customer_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Prüfen ob Kunde Lizenzen hat
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
-    license_count = cur.fetchone()[0]
-    
-    if license_count > 0:
-        # Kunde hat Lizenzen - nicht löschen
-        cur.close()
-        conn.close()
-        return redirect("/customers")
-    
-    # Kundendetails für Audit-Log abrufen
-    cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
-    customer_info = cur.fetchone()
-    
-    # Kunde löschen wenn keine Lizenzen vorhanden
-    cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
-    
-    conn.commit()
-    
-    # Audit-Log
-    if customer_info:
-        log_audit('DELETE', 'customer', customer_id,
-                 old_values={
-                     'name': customer_info[0],
-                     'email': customer_info[1]
-                 })
-    
-    cur.close()
-    conn.close()
-    
-    return redirect("/customers")
-
-@app.route("/customers-licenses")
-@login_required
-def customers_licenses():
-    """Kombinierte Ansicht für Kunden und deren Lizenzen"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    query = """
-        SELECT 
-            c.id,
-            c.name,
-            c.email,
-            c.created_at,
-            COUNT(l.id) as total_licenses,
-            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
-        FROM customers c
-        LEFT JOIN licenses l ON c.id = l.customer_id
-    """
-    
-    if not show_test:
-        query += " WHERE c.is_test = FALSE"
-    
-    query += """
-        GROUP BY c.id, c.name, c.email, c.created_at
-        ORDER BY c.name
-    """
-    
-    cur.execute(query)
-    customers = cur.fetchall()
-    
-    # Hole ausgewählten Kunden nur wenn explizit in URL angegeben
-    selected_customer_id = request.args.get('customer_id', type=int)
-    licenses = []
-    selected_customer = None
-    
-    if customers and selected_customer_id:
-        # Hole Daten des ausgewählten Kunden
-        for customer in customers:
-            if customer[0] == selected_customer_id:
-                selected_customer = customer
-                break
-        
-        # Hole Lizenzen des ausgewählten Kunden
-        if selected_customer:
-            cur.execute("""
-                SELECT 
-                    l.id,
-                    l.license_key,
-                    l.license_type,
-                    l.valid_from,
-                    l.valid_until,
-                    l.is_active,
-                    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 as status,
-                    l.domain_count,
-                    l.ipv4_count,
-                    l.phone_count,
-                    l.device_limit,
-                    (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
-                    -- Actual resource counts
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
-                FROM licenses l
-                WHERE l.customer_id = %s
-                ORDER BY l.created_at DESC, l.id DESC
-            """, (selected_customer_id,))
-            licenses = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("customers_licenses.html",
-                         customers=customers,
-                         selected_customer=selected_customer,
-                         selected_customer_id=selected_customer_id,
-                         licenses=licenses,
-                         show_test=show_test)
-
-@app.route("/api/customer/<int:customer_id>/licenses")
-@login_required
-def api_customer_licenses(customer_id):
-    """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole Lizenzen des Kunden
-    cur.execute("""
-        SELECT 
-            l.id,
-            l.license_key,
-            l.license_type,
-            l.valid_from,
-            l.valid_until,
-            l.is_active,
-            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 as status,
-            l.domain_count,
-            l.ipv4_count,
-            l.phone_count,
-            l.device_limit,
-            (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
-            -- Actual resource counts
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
-        FROM licenses l
-        WHERE l.customer_id = %s
-        ORDER BY l.created_at DESC, l.id DESC
-    """, (customer_id,))
-    
-    licenses = []
-    for row in cur.fetchall():
-        license_id = row[0]
-        
-        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
-        cur.execute("""
-            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
-            FROM resource_pools rp
-            JOIN license_resources lr ON rp.id = lr.resource_id
-            WHERE lr.license_id = %s AND lr.is_active = true
-            ORDER BY rp.resource_type, rp.resource_value
-        """, (license_id,))
-        
-        resources = {
-            'domains': [],
-            'ipv4s': [],
-            'phones': []
-        }
-        
-        for res_row in cur.fetchall():
-            resource_info = {
-                'id': res_row[0],
-                'value': res_row[2],
-                'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else ''
-            }
-            
-            if res_row[1] == 'domain':
-                resources['domains'].append(resource_info)
-            elif res_row[1] == 'ipv4':
-                resources['ipv4s'].append(resource_info)
-            elif res_row[1] == 'phone':
-                resources['phones'].append(resource_info)
-        
-        licenses.append({
-            'id': row[0],
-            'license_key': row[1],
-            'license_type': row[2],
-            'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '',
-            'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '',
-            'is_active': row[5],
-            'status': row[6],
-            'domain_count': row[7],  # limit
-            'ipv4_count': row[8],     # limit
-            'phone_count': row[9],    # limit
-            'device_limit': row[10],
-            'active_devices': row[11],
-            'actual_domain_count': row[12],  # actual count
-            'actual_ipv4_count': row[13],    # actual count
-            'actual_phone_count': row[14],   # actual count
-            'resources': resources
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'success': True,
-        'licenses': licenses,
-        'count': len(licenses)
-    })
-
-@app.route("/api/customer/<int:customer_id>/quick-stats")
-@login_required
-def api_customer_quick_stats(customer_id):
-    """API-Endpoint für Schnellstatistiken eines Kunden"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole Kundenstatistiken
-    cur.execute("""
-        SELECT 
-            COUNT(l.id) as total_licenses,
-            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon
-        FROM licenses l
-        WHERE l.customer_id = %s
-    """, (customer_id,))
-    
-    stats = cur.fetchone()
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'success': True,
-        'stats': {
-            'total': stats[0],
-            'active': stats[1],
-            'expired': stats[2],
-            'expiring_soon': stats[3]
-        }
-    })
-
-@app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
-@login_required
-def api_license_quick_edit(license_id):
-    """API-Endpoint für schnelle Lizenz-Bearbeitung"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        data = request.get_json()
-        
-        # Hole alte Werte für Audit-Log
-        cur.execute("""
-            SELECT is_active, valid_until, license_type 
-            FROM licenses WHERE id = %s
-        """, (license_id,))
-        old_values = cur.fetchone()
-        
-        if not old_values:
-            return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404
-        
-        # Update-Felder vorbereiten
-        updates = []
-        params = []
-        new_values = {}
-        
-        if 'is_active' in data:
-            updates.append("is_active = %s")
-            params.append(data['is_active'])
-            new_values['is_active'] = data['is_active']
-        
-        if 'valid_until' in data:
-            updates.append("valid_until = %s")
-            params.append(data['valid_until'])
-            new_values['valid_until'] = data['valid_until']
-        
-        if 'license_type' in data:
-            updates.append("license_type = %s")
-            params.append(data['license_type'])
-            new_values['license_type'] = data['license_type']
-        
-        if updates:
-            params.append(license_id)
-            cur.execute(f"""
-                UPDATE licenses 
-                SET {', '.join(updates)}
-                WHERE id = %s
-            """, params)
-            
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('UPDATE', 'license', license_id,
-                     old_values={
-                         'is_active': old_values[0],
-                         'valid_until': old_values[1].isoformat() if old_values[1] else None,
-                         'license_type': old_values[2]
-                     },
-                     new_values=new_values)
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True})
-    
-    except Exception as e:
-        conn.rollback()
-        cur.close()
-        conn.close()
-        return jsonify({'success': False, 'error': str(e)}), 500
-
-@app.route("/api/license/<int:license_id>/resources")
-@login_required
-def api_license_resources(license_id):
-    """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
-        cur.execute("""
-            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
-            FROM resource_pools rp
-            JOIN license_resources lr ON rp.id = lr.resource_id
-            WHERE lr.license_id = %s AND lr.is_active = true
-            ORDER BY rp.resource_type, rp.resource_value
-        """, (license_id,))
-        
-        resources = {
-            'domains': [],
-            'ipv4s': [],
-            'phones': []
-        }
-        
-        for row in cur.fetchall():
-            resource_info = {
-                'id': row[0],
-                'value': row[2],
-                'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else ''
-            }
-            
-            if row[1] == 'domain':
-                resources['domains'].append(resource_info)
-            elif row[1] == 'ipv4':
-                resources['ipv4s'].append(resource_info)
-            elif row[1] == 'phone':
-                resources['phones'].append(resource_info)
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'resources': resources
-        })
-        
-    except Exception as e:
-        cur.close()
-        conn.close()
-        return jsonify({'success': False, 'error': str(e)}), 500
-
-@app.route("/sessions")
-@login_required
-def sessions():
-    conn = get_connection()
-    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
-    cur.execute(f"""
-        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
-               s.user_agent, s.started_at, s.last_heartbeat,
-               EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
-        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 {active_sort_fields[active_sort]} {active_order.upper()}
-    """)
-    active_sessions = cur.fetchall()
-    
-    # Inaktive Sessions der letzten 24 Stunden
-    cur.execute(f"""
-        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
-               s.started_at, s.ended_at, 
-               EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
-        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 > NOW() - INTERVAL '24 hours'
-        ORDER BY {ended_sort_fields[ended_sort]} {ended_order.upper()}
-        LIMIT 50
-    """)
-    recent_sessions = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("sessions.html", 
-                         active_sessions=active_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'))
-
-@app.route("/session/end/<int:session_id>", methods=["POST"])
-@login_required
-def end_session(session_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Session beenden
-    cur.execute("""
-        UPDATE sessions 
-        SET is_active = FALSE, ended_at = NOW()
-        WHERE id = %s AND is_active = TRUE
-    """, (session_id,))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    return redirect("/sessions")
-
-@app.route("/export/licenses")
-@login_required
-def export_licenses():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
-    include_test = request.args.get('include_test', 'false').lower() == 'true'
-    customer_id = request.args.get('customer_id', type=int)
-    
-    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.is_test,
-               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 as status
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-    """
-    
-    # Build WHERE clause
-    where_conditions = []
-    params = []
-    
-    if not include_test:
-        where_conditions.append("l.is_test = FALSE")
-    
-    if customer_id:
-        where_conditions.append("l.customer_id = %s")
-        params.append(customer_id)
-    
-    if where_conditions:
-        query += " WHERE " + " AND ".join(where_conditions)
-    
-    query += " ORDER BY l.id"
-    
-    cur.execute(query, params)
-    
-    # Spaltennamen
-    columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 
-               'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status']
-    
-    # Daten in DataFrame
-    data = cur.fetchall()
-    df = pd.DataFrame(data, columns=columns)
-    
-    # Datumsformatierung
-    df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y')
-    df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y')
-    
-    # Typ und Aktiv Status anpassen
-    df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
-    df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
-    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
-    
-    cur.close()
-    conn.close()
-    
-    # Export Format
-    export_format = request.args.get('format', 'excel')
-    
-    # Audit-Log
-    log_audit('EXPORT', 'license', 
-             additional_info=f"Export aller Lizenzen als {export_format.upper()}")
-    filename = f'lizenzen_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
-    
-    if export_format == 'csv':
-        # CSV Export
-        output = io.StringIO()
-        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
-        output.seek(0)
-        
-        return send_file(
-            io.BytesIO(output.getvalue().encode('utf-8-sig')),
-            mimetype='text/csv',
-            as_attachment=True,
-            download_name=f'{filename}.csv'
-        )
-    else:
-        # Excel Export
-        output = io.BytesIO()
-        with pd.ExcelWriter(output, engine='openpyxl') as writer:
-            df.to_excel(writer, sheet_name='Lizenzen', index=False)
-            
-            # Formatierung
-            worksheet = writer.sheets['Lizenzen']
-            for column in worksheet.columns:
-                max_length = 0
-                column_letter = column[0].column_letter
-                for cell in column:
-                    try:
-                        if len(str(cell.value)) > max_length:
-                            max_length = len(str(cell.value))
-                    except:
-                        pass
-                adjusted_width = min(max_length + 2, 50)
-                worksheet.column_dimensions[column_letter].width = adjusted_width
-        
-        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/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():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Check if test data should be included
-    include_test = request.args.get('include_test', 'false').lower() == 'true'
-    
-    # Build query based on test data filter
-    if include_test:
-        # Include all customers
-        query = """
-            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
-                   COUNT(l.id) as total_licenses,
-                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
-            FROM customers c
-            LEFT JOIN licenses l ON c.id = l.customer_id
-            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
-            ORDER BY c.id
-        """
-    else:
-        # Exclude test customers and test licenses
-        query = """
-            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
-                   COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
-                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
-                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
-            FROM customers c
-            LEFT JOIN licenses l ON c.id = l.customer_id
-            WHERE c.is_test = FALSE
-            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
-            ORDER BY c.id
-        """
-    
-    cur.execute(query)
-    
-    # Spaltennamen
-    columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten',
-               'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
-    
-    # Daten in DataFrame
-    data = cur.fetchall()
-    df = pd.DataFrame(data, columns=columns)
-    
-    # Datumsformatierung
-    df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
-    
-    # Testdaten formatting
-    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
-    
-    cur.close()
-    conn.close()
-    
-    # Export Format
-    export_format = request.args.get('format', 'excel')
-    
-    # Audit-Log
-    log_audit('EXPORT', 'customer', 
-             additional_info=f"Export aller Kunden als {export_format.upper()}")
-    filename = f'kunden_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
-    
-    if export_format == 'csv':
-        # CSV Export
-        output = io.StringIO()
-        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
-        output.seek(0)
-        
-        return send_file(
-            io.BytesIO(output.getvalue().encode('utf-8-sig')),
-            mimetype='text/csv',
-            as_attachment=True,
-            download_name=f'{filename}.csv'
-        )
-    else:
-        # Excel Export
-        output = io.BytesIO()
-        with pd.ExcelWriter(output, engine='openpyxl') as writer:
-            df.to_excel(writer, sheet_name='Kunden', index=False)
-            
-            # Formatierung
-            worksheet = writer.sheets['Kunden']
-            for column in worksheet.columns:
-                max_length = 0
-                column_letter = column[0].column_letter
-                for cell in column:
-                    try:
-                        if len(str(cell.value)) > max_length:
-                            max_length = len(str(cell.value))
-                    except:
-                        pass
-                adjusted_width = min(max_length + 2, 50)
-                worksheet.column_dimensions[column_letter].width = adjusted_width
-        
-        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/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.resource_type, r.resource_value, r.status, r.allocated_to_license, 
-               r.created_at, r.status_changed_at,
-               l.license_key, c.name as customer_name, c.email as customer_email,
-               l.license_type
-        FROM resource_pools r
-        LEFT JOIN licenses l ON r.allocated_to_license = 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 (r.is_test = false OR r.is_test IS NULL)"
-    
-    # Filter für Ressourcentyp
-    if filter_type:
-        query += " AND r.resource_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.resource_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():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Parameter
-    filter_user = request.args.get('user', '').strip()
-    filter_action = request.args.get('action', '').strip()
-    filter_entity = request.args.get('entity', '').strip()
-    page = request.args.get('page', 1, type=int)
-    sort = request.args.get('sort', 'timestamp')
-    order = request.args.get('order', 'desc')
-    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
-    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 = []
-    
-    # Filter
-    if filter_user:
-        query += " AND LOWER(username) LIKE LOWER(%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)
-    
-    # Gesamtanzahl für Pagination
-    count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table"
-    cur.execute(count_query, params)
-    total = cur.fetchone()[0]
-    
-    # Pagination
-    offset = (page - 1) * per_page
-    query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
-    params.extend([per_page, offset])
-    
-    cur.execute(query, params)
-    logs = cur.fetchall()
-    
-    # JSON-Werte parsen
-    parsed_logs = []
-    for log in logs:
-        parsed_log = list(log)
-        # old_values und new_values sind bereits Dictionaries (JSONB)
-        # Keine Konvertierung nötig
-        parsed_logs.append(parsed_log)
-    
-    # Pagination Info
-    total_pages = (total + per_page - 1) // per_page
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("audit_log.html", 
-                         logs=parsed_logs,
-                         filter_user=filter_user,
-                         filter_action=filter_action,
-                         filter_entity=filter_entity,
-                         page=page,
-                         total_pages=total_pages,
-                         total=total,
-                         sort=sort,
-                         order=order,
-                         username=session.get('username'))
-
-@app.route("/backups")
-@login_required
-def backups():
-    """Zeigt die Backup-Historie an"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Letztes erfolgreiches Backup für Dashboard
-    cur.execute("""
-        SELECT created_at, filesize, duration_seconds 
-        FROM backup_history 
-        WHERE status = 'success' 
-        ORDER BY created_at DESC 
-        LIMIT 1
-    """)
-    last_backup = cur.fetchone()
-    
-    # Alle Backups abrufen
-    cur.execute("""
-        SELECT id, filename, filesize, backup_type, status, error_message,
-               created_at, created_by, tables_count, records_count, 
-               duration_seconds, is_encrypted
-        FROM backup_history
-        ORDER BY created_at DESC
-    """)
-    backups = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("backups.html", 
-                         backups=backups,
-                         last_backup=last_backup,
-                         username=session.get('username'))
-
-@app.route("/backup/create", methods=["POST"])
-@login_required
-def create_backup_route():
-    """Erstellt ein manuelles Backup"""
-    username = session.get('username')
-    success, result = create_backup(backup_type="manual", created_by=username)
-    
-    if success:
-        return jsonify({
-            'success': True,
-            'message': f'Backup erfolgreich erstellt: {result}'
-        })
-    else:
-        return jsonify({
-            'success': False,
-            'message': f'Backup fehlgeschlagen: {result}'
-        }), 500
-
-@app.route("/backup/restore/<int:backup_id>", methods=["POST"])
-@login_required
-def restore_backup_route(backup_id):
-    """Stellt ein Backup wieder her"""
-    encryption_key = request.form.get('encryption_key')
-    
-    success, message = restore_backup(backup_id, encryption_key)
-    
-    if success:
-        return jsonify({
-            'success': True,
-            'message': message
-        })
-    else:
-        return jsonify({
-            'success': False,
-            'message': message
-        }), 500
-
-@app.route("/backup/download/<int:backup_id>")
-@login_required
-def download_backup(backup_id):
-    """Lädt eine Backup-Datei herunter"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT filename, filepath 
-        FROM backup_history 
-        WHERE id = %s
-    """, (backup_id,))
-    backup_info = cur.fetchone()
-    
-    cur.close()
-    conn.close()
-    
-    if not backup_info:
-        return "Backup nicht gefunden", 404
-    
-    filename, filepath = backup_info
-    filepath = Path(filepath)
-    
-    if not filepath.exists():
-        return "Backup-Datei nicht gefunden", 404
-    
-    # Audit-Log
-    log_audit('DOWNLOAD', 'backup', backup_id,
-             additional_info=f"Backup heruntergeladen: {filename}")
-    
-    return send_file(filepath, as_attachment=True, download_name=filename)
-
-@app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
-@login_required
-def delete_backup(backup_id):
-    """Löscht ein Backup"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Backup-Informationen abrufen
-        cur.execute("""
-            SELECT filename, filepath 
-            FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        backup_info = cur.fetchone()
-        
-        if not backup_info:
-            return jsonify({
-                'success': False,
-                'message': 'Backup nicht gefunden'
-            }), 404
-        
-        filename, filepath = backup_info
-        filepath = Path(filepath)
-        
-        # Datei löschen, wenn sie existiert
-        if filepath.exists():
-            filepath.unlink()
-        
-        # Aus Datenbank löschen
-        cur.execute("""
-            DELETE FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('DELETE', 'backup', backup_id,
-                 additional_info=f"Backup gelöscht: {filename}")
-        
-        return jsonify({
-            'success': True,
-            'message': f'Backup "{filename}" wurde erfolgreich gelöscht'
-        })
-        
-    except Exception as e:
-        conn.rollback()
-        return jsonify({
-            'success': False,
-            'message': f'Fehler beim Löschen des Backups: {str(e)}'
-        }), 500
-    finally:
-        cur.close()
-        conn.close()
-
-@app.route("/security/blocked-ips")
-@login_required
-def blocked_ips():
-    """Zeigt alle gesperrten IPs an"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT 
-            ip_address,
-            attempt_count,
-            first_attempt,
-            last_attempt,
-            blocked_until,
-            last_username_tried,
-            last_error_message
-        FROM login_attempts
-        WHERE blocked_until IS NOT NULL
-        ORDER BY blocked_until DESC
-    """)
-    
-    blocked_ips_list = []
-    for ip in cur.fetchall():
-        blocked_ips_list.append({
-            'ip_address': ip[0],
-            'attempt_count': ip[1],
-            'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'),
-            'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'),
-            'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'),
-            'is_active': ip[4] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None),
-            'last_username': ip[5],
-            'last_error': ip[6]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("blocked_ips.html", 
-                         blocked_ips=blocked_ips_list,
-                         username=session.get('username'))
-
-@app.route("/security/unblock-ip", methods=["POST"])
-@login_required
-def unblock_ip():
-    """Entsperrt eine IP-Adresse"""
-    ip_address = request.form.get('ip_address')
-    
-    if ip_address:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        cur.execute("""
-            UPDATE login_attempts
-            SET blocked_until = NULL
-            WHERE ip_address = %s
-        """, (ip_address,))
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        # Audit-Log
-        log_audit('UNBLOCK_IP', 'security', 
-                 additional_info=f"IP {ip_address} manuell entsperrt")
-    
-    return redirect(url_for('blocked_ips'))
-
-@app.route("/security/clear-attempts", methods=["POST"])
-@login_required
-def clear_attempts():
-    """Löscht alle Login-Versuche für eine IP"""
-    ip_address = request.form.get('ip_address')
-    
-    if ip_address:
-        reset_login_attempts(ip_address)
-        
-        # Audit-Log
-        log_audit('CLEAR_ATTEMPTS', 'security', 
-                 additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt")
-    
-    return redirect(url_for('blocked_ips'))
-
-# API Endpoints for License Management
-@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
-@login_required
-def toggle_license_api(license_id):
-    """Toggle license active status via API"""
-    try:
-        data = request.get_json()
-        is_active = data.get('is_active', False)
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update license status
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = %s
-            WHERE id = %s
-        """, (is_active, license_id))
-        
-        conn.commit()
-        
-        # Log the action
-        log_audit('UPDATE', 'license', license_id,
-                 new_values={'is_active': is_active},
-                 additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Status erfolgreich geändert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/licenses/bulk-activate", methods=["POST"])
-@login_required
-def bulk_activate_licenses():
-    """Activate multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update all selected licenses (nur Live-Daten)
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = TRUE
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_UPDATE', 'licenses', None,
-                 new_values={'is_active': True, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen aktiviert")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
-@login_required
-def bulk_deactivate_licenses():
-    """Deactivate multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update all selected licenses (nur Live-Daten)
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = FALSE
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_UPDATE', 'licenses', None,
-                 new_values={'is_active': False, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen deaktiviert")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/license/<int:license_id>/devices")
-@login_required
-def get_license_devices(license_id):
-    """Hole alle registrierten Geräte einer Lizenz"""
-    try:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob Lizenz existiert und hole device_limit
-        cur.execute("""
-            SELECT device_limit FROM licenses WHERE id = %s
-        """, (license_id,))
-        license_data = cur.fetchone()
-        
-        if not license_data:
-            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
-        
-        device_limit = license_data[0]
-        
-        # Hole alle Geräte für diese Lizenz
-        cur.execute("""
-            SELECT id, hardware_id, device_name, operating_system,
-                   first_seen, last_seen, is_active, ip_address
-            FROM device_registrations
-            WHERE license_id = %s
-            ORDER BY is_active DESC, last_seen DESC
-        """, (license_id,))
-        
-        devices = []
-        for row in cur.fetchall():
-            devices.append({
-                'id': row[0],
-                'hardware_id': row[1],
-                'device_name': row[2] or 'Unbekanntes Gerät',
-                'operating_system': row[3] or 'Unbekannt',
-                'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '',
-                'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '',
-                'is_active': row[6],
-                'ip_address': row[7] or '-'
-            })
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'devices': devices,
-            'device_limit': device_limit,
-            'active_count': sum(1 for d in devices if d['is_active'])
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
-
-@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
-def register_device(license_id):
-    """Registriere ein neues Gerät für eine Lizenz"""
-    try:
-        data = request.get_json()
-        hardware_id = data.get('hardware_id')
-        device_name = data.get('device_name', '')
-        operating_system = data.get('operating_system', '')
-        
-        if not hardware_id:
-            return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob Lizenz existiert und aktiv ist
-        cur.execute("""
-            SELECT device_limit, is_active, valid_until
-            FROM licenses 
-            WHERE id = %s
-        """, (license_id,))
-        license_data = cur.fetchone()
-        
-        if not license_data:
-            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
-        
-        device_limit, is_active, valid_until = license_data
-        
-        # Prüfe ob Lizenz aktiv und gültig ist
-        if not is_active:
-            return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
-        
-        if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
-            return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
-        
-        # Prüfe ob Gerät bereits registriert ist
-        cur.execute("""
-            SELECT id, is_active FROM device_registrations
-            WHERE license_id = %s AND hardware_id = %s
-        """, (license_id, hardware_id))
-        existing_device = cur.fetchone()
-        
-        if existing_device:
-            device_id, is_device_active = existing_device
-            if is_device_active:
-                # Gerät ist bereits aktiv, update last_seen
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET last_seen = CURRENT_TIMESTAMP,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
-            else:
-                # Gerät war deaktiviert, prüfe ob wir es reaktivieren können
-                cur.execute("""
-                    SELECT COUNT(*) FROM device_registrations
-                    WHERE license_id = %s AND is_active = TRUE
-                """, (license_id,))
-                active_count = cur.fetchone()[0]
-                
-                if active_count >= device_limit:
-                    return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
-                
-                # Reaktiviere das Gerät
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET is_active = TRUE,
-                        last_seen = CURRENT_TIMESTAMP,
-                        deactivated_at = NULL,
-                        deactivated_by = NULL,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
-        
-        # Neues Gerät - prüfe Gerätelimit
-        cur.execute("""
-            SELECT COUNT(*) FROM device_registrations
-            WHERE license_id = %s AND is_active = TRUE
-        """, (license_id,))
-        active_count = cur.fetchone()[0]
-        
-        if active_count >= device_limit:
-            return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
-        
-        # Registriere neues Gerät
-        cur.execute("""
-            INSERT INTO device_registrations 
-            (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
-            VALUES (%s, %s, %s, %s, %s, %s)
-            RETURNING id
-        """, (license_id, hardware_id, device_name, operating_system, 
-              get_client_ip(), request.headers.get('User-Agent', '')))
-        device_id = cur.fetchone()[0]
-        
-        conn.commit()
-        
-        # Audit Log
-        log_audit('DEVICE_REGISTER', 'device', device_id,
-                 new_values={'license_id': license_id, 'hardware_id': hardware_id})
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
-
-@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
-@login_required
-def deactivate_device(license_id, device_id):
-    """Deaktiviere ein registriertes Gerät"""
-    try:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob das Gerät zu dieser Lizenz gehört
-        cur.execute("""
-            SELECT id FROM device_registrations
-            WHERE id = %s AND license_id = %s AND is_active = TRUE
-        """, (device_id, license_id))
-        
-        if not cur.fetchone():
-            return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404
-        
-        # Deaktiviere das Gerät
-        cur.execute("""
-            UPDATE device_registrations
-            SET is_active = FALSE,
-                deactivated_at = CURRENT_TIMESTAMP,
-                deactivated_by = %s
-            WHERE id = %s
-        """, (session['username'], device_id))
-        
-        conn.commit()
-        
-        # Audit Log
-        log_audit('DEVICE_DEACTIVATE', 'device', device_id,
-                 old_values={'is_active': True},
-                 new_values={'is_active': False})
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'})
-        
-    except Exception as e:
-        logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
-
-@app.route("/api/licenses/bulk-delete", methods=["POST"])
-@login_required
-def bulk_delete_licenses():
-    """Delete multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Get license info for audit log (nur Live-Daten)
-        cur.execute("""
-            SELECT license_key 
-            FROM licenses 
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        license_keys = [row[0] for row in cur.fetchall()]
-        
-        # Delete all selected licenses (nur Live-Daten)
-        cur.execute("""
-            DELETE FROM licenses 
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_DELETE', 'licenses', None,
-                 old_values={'license_keys': license_keys, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen gelöscht")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-# ===================== RESOURCE POOL MANAGEMENT =====================
-
-@app.route('/resources')
-@login_required
-def resources():
-    """Resource Pool Hauptübersicht"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Prüfe ob Testdaten angezeigt werden sollen (gleiche Logik wie bei Kunden)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    # Statistiken abrufen
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'available') as available,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-            COUNT(*) as total
-        FROM resource_pools
-        WHERE is_test = %s
-        GROUP BY resource_type
-    """, (show_test,))
-    
-    stats = {}
-    for row in cur.fetchall():
-        stats[row[0]] = {
-            'available': row[1],
-            'allocated': row[2],
-            'quarantine': row[3],
-            'total': row[4],
-            'available_percent': round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
-        }
-    
-    # Letzte Aktivitäten (gefiltert nach Test/Live)
-    cur.execute("""
-        SELECT 
-            rh.action,
-            rh.action_by,
-            rh.action_at,
-            rp.resource_type,
-            rp.resource_value,
-            rh.details
-        FROM resource_history rh
-        JOIN resource_pools rp ON rh.resource_id = rp.id
-        WHERE rp.is_test = %s
-        ORDER BY rh.action_at DESC
-        LIMIT 10
-    """, (show_test,))
-    recent_activities = cur.fetchall()
-    
-    # Ressourcen-Liste mit Pagination
-    page = request.args.get('page', 1, type=int)
-    per_page = 50
-    offset = (page - 1) * per_page
-    
-    resource_type = request.args.get('type', '')
-    status_filter = request.args.get('status', '')
-    search = request.args.get('search', '')
-    
-    # Sortierung
-    sort_by = request.args.get('sort', 'id')
-    sort_order = request.args.get('order', 'desc')
-    
-    # Base Query
-    query = """
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            rp.status,
-            rp.allocated_to_license,
-            l.license_key,
-            c.name as customer_name,
-            rp.status_changed_at,
-            rp.quarantine_reason,
-            rp.quarantine_until,
-            c.id as customer_id
-        FROM resource_pools rp
-        LEFT JOIN licenses l ON rp.allocated_to_license = l.id
-        LEFT JOIN customers c ON l.customer_id = c.id
-        WHERE rp.is_test = %s
-    """
-    params = [show_test]
-    
-    if resource_type:
-        query += " AND rp.resource_type = %s"
-        params.append(resource_type)
-    
-    if status_filter:
-        query += " AND rp.status = %s"
-        params.append(status_filter)
-    
-    if search:
-        query += " AND rp.resource_value ILIKE %s"
-        params.append(f'%{search}%')
-    
-    # Count total
-    count_query = f"SELECT COUNT(*) FROM ({query}) as cnt"
-    cur.execute(count_query, params)
-    total = cur.fetchone()[0]
-    total_pages = (total + per_page - 1) // per_page
-    
-    # Get paginated results with dynamic sorting
-    sort_column_map = {
-        'id': 'rp.id',
-        'type': 'rp.resource_type',
-        'resource': 'rp.resource_value',
-        'status': 'rp.status',
-        'assigned': 'c.name',
-        'changed': 'rp.status_changed_at'
-    }
-    
-    sort_column = sort_column_map.get(sort_by, 'rp.id')
-    sort_direction = 'ASC' if sort_order == 'asc' else 'DESC'
-    
-    query += f" ORDER BY {sort_column} {sort_direction} LIMIT %s OFFSET %s"
-    params.extend([per_page, offset])
-    
-    cur.execute(query, params)
-    resources = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template('resources.html',
-                         stats=stats,
-                         resources=resources,
-                         recent_activities=recent_activities,
-                         page=page,
-                         total_pages=total_pages,
-                         total=total,
-                         resource_type=resource_type,
-                         status_filter=status_filter,
-                         search=search,
-                         show_test=show_test,
-                         sort_by=sort_by,
-                         sort_order=sort_order,
-                         datetime=datetime,
-                         timedelta=timedelta)
-
-@app.route('/resources/add', methods=['GET', 'POST'])
-@login_required
-def add_resources():
-    """Ressourcen zum Pool hinzufügen"""
-    # Hole show_test Parameter für die Anzeige
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    if request.method == 'POST':
-        resource_type = request.form.get('resource_type')
-        resources_text = request.form.get('resources_text', '')
-        is_test = request.form.get('is_test') == 'on'  # Checkbox für Testdaten
-        
-        # Parse resources (one per line)
-        resources = [r.strip() for r in resources_text.split('\n') if r.strip()]
-        
-        if not resources:
-            flash('Keine Ressourcen angegeben', 'error')
-            return redirect(url_for('add_resources', show_test=show_test))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        added = 0
-        duplicates = 0
-        
-        for resource_value in resources:
-            try:
-                cur.execute("""
-                    INSERT INTO resource_pools (resource_type, resource_value, status_changed_by, is_test)
-                    VALUES (%s, %s, %s, %s)
-                    ON CONFLICT (resource_type, resource_value) DO NOTHING
-                """, (resource_type, resource_value, session['username'], is_test))
-                
-                if cur.rowcount > 0:
-                    added += 1
-                    # Get the inserted ID
-                    cur.execute("SELECT id FROM resource_pools WHERE resource_type = %s AND resource_value = %s", 
-                              (resource_type, resource_value))
-                    resource_id = cur.fetchone()[0]
-                    
-                    # Log in history
-                    cur.execute("""
-                        INSERT INTO resource_history (resource_id, action, action_by, ip_address)
-                        VALUES (%s, 'created', %s, %s)
-                    """, (resource_id, session['username'], get_client_ip()))
-                else:
-                    duplicates += 1
-                    
-            except Exception as e:
-                app.logger.error(f"Error adding resource {resource_value}: {e}")
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        log_audit('CREATE', 'resource_pool', None,
-                 new_values={'type': resource_type, 'added': added, 'duplicates': duplicates, 'is_test': is_test},
-                 additional_info=f"{added} {'Test-' if is_test else ''}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen")
-        
-        flash(f'{added} {"Test-" if is_test else ""}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen', 'success')
-        return redirect(url_for('resources', show_test=show_test))
-    
-    return render_template('add_resources.html', show_test=show_test)
-
-@app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
-@login_required
-def quarantine_resource(resource_id):
-    """Ressource in Quarantäne setzen"""
-    reason = request.form.get('reason', 'review')
-    until_date = request.form.get('until_date')
-    notes = request.form.get('notes', '')
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Get current resource info
-    cur.execute("SELECT resource_type, resource_value, status FROM resource_pools WHERE id = %s", (resource_id,))
-    resource = cur.fetchone()
-    
-    if not resource:
-        flash('Ressource nicht gefunden', 'error')
-        return redirect(url_for('resources'))
-    
-    old_status = resource[2]
-    
-    # Update resource
-    cur.execute("""
-        UPDATE resource_pools
-        SET status = 'quarantine',
-            quarantine_reason = %s,
-            quarantine_until = %s,
-            notes = %s,
-            status_changed_at = CURRENT_TIMESTAMP,
-            status_changed_by = %s
-        WHERE id = %s
-    """, (reason, until_date if until_date else None, notes, session['username'], resource_id))
-    
-    # Log in history
-    cur.execute("""
-        INSERT INTO resource_history (resource_id, action, action_by, ip_address, details)
-        VALUES (%s, 'quarantined', %s, %s, %s)
-    """, (resource_id, session['username'], get_client_ip(), 
-          Json({'reason': reason, 'until': until_date, 'notes': notes, 'old_status': old_status})))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('UPDATE', 'resource', resource_id,
-             old_values={'status': old_status},
-             new_values={'status': 'quarantine', 'reason': reason},
-             additional_info=f"Ressource {resource[0]}: {resource[1]} in Quarantäne")
-    
-    flash('Ressource in Quarantäne gesetzt', 'success')
-    
-    # Redirect mit allen aktuellen Filtern
-    return redirect(url_for('resources', 
-                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
-                          type=request.args.get('type', request.form.get('type', '')),
-                          status=request.args.get('status', request.form.get('status', '')),
-                          search=request.args.get('search', request.form.get('search', ''))))
-
-@app.route('/resources/release', methods=['POST'])
-@login_required
-def release_resources():
-    """Ressourcen aus Quarantäne freigeben"""
-    resource_ids = request.form.getlist('resource_ids')
-    
-    if not resource_ids:
-        flash('Keine Ressourcen ausgewählt', 'error')
-        return redirect(url_for('resources'))
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    released = 0
-    for resource_id in resource_ids:
-        cur.execute("""
-            UPDATE resource_pools
-            SET status = 'available',
-                quarantine_reason = NULL,
-                quarantine_until = NULL,
-                allocated_to_license = NULL,
-                status_changed_at = CURRENT_TIMESTAMP,
-                status_changed_by = %s
-            WHERE id = %s AND status = 'quarantine'
-        """, (session['username'], resource_id))
-        
-        if cur.rowcount > 0:
-            released += 1
-            # Log in history
-            cur.execute("""
-                INSERT INTO resource_history (resource_id, action, action_by, ip_address)
-                VALUES (%s, 'released', %s, %s)
-            """, (resource_id, session['username'], get_client_ip()))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('UPDATE', 'resource_pool', None,
-             new_values={'released': released},
-             additional_info=f"{released} Ressourcen aus Quarantäne freigegeben")
-    
-    flash(f'{released} Ressourcen freigegeben', 'success')
-    
-    # Redirect mit allen aktuellen Filtern
-    return redirect(url_for('resources', 
-                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
-                          type=request.args.get('type', request.form.get('type', '')),
-                          status=request.args.get('status', request.form.get('status', '')),
-                          search=request.args.get('search', request.form.get('search', ''))))
-
-@app.route('/api/resources/allocate', methods=['POST'])
-@login_required
-def allocate_resources_api():
-    """API für Ressourcen-Zuweisung bei Lizenzerstellung"""
-    data = request.json
-    license_id = data.get('license_id')
-    domain_count = data.get('domain_count', 1)
-    ipv4_count = data.get('ipv4_count', 1)
-    phone_count = data.get('phone_count', 1)
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        allocated = {'domains': [], 'ipv4s': [], 'phones': []}
-        
-        # Allocate domains
-        if domain_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'domain' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (domain_count,))
-            domains = cur.fetchall()
-            
-            if len(domains) < domain_count:
-                raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {len(domains)})")
-            
-            for domain_id, domain_value in domains:
-                # Update resource status
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], domain_id))
-                
-                # Create assignment
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, domain_id, session['username']))
-                
-                # Log history
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (domain_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['domains'].append(domain_value)
-        
-        # Allocate IPv4s (similar logic)
-        if ipv4_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'ipv4' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (ipv4_count,))
-            ipv4s = cur.fetchall()
-            
-            if len(ipv4s) < ipv4_count:
-                raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar")
-            
-            for ipv4_id, ipv4_value in ipv4s:
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], ipv4_id))
-                
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, ipv4_id, session['username']))
-                
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (ipv4_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['ipv4s'].append(ipv4_value)
-        
-        # Allocate phones (similar logic)
-        if phone_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'phone' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (phone_count,))
-            phones = cur.fetchall()
-            
-            if len(phones) < phone_count:
-                raise ValueError(f"Nicht genügend Telefonnummern verfügbar")
-            
-            for phone_id, phone_value in phones:
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], phone_id))
-                
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, phone_id, session['username']))
-                
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (phone_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['phones'].append(phone_value)
-        
-        # Update license resource counts
-        cur.execute("""
-            UPDATE licenses
-            SET domain_count = %s,
-                ipv4_count = %s,
-                phone_count = %s
-            WHERE id = %s
-        """, (domain_count, ipv4_count, phone_count, license_id))
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'allocated': allocated
-        })
-        
-    except Exception as e:
-        conn.rollback()
-        cur.close()
-        conn.close()
-        return jsonify({
-            'success': False,
-            'error': str(e)
-        }), 400
-
-@app.route('/api/resources/check-availability', methods=['GET'])
-@login_required
-def check_resource_availability():
-    """Prüft verfügbare Ressourcen"""
-    resource_type = request.args.get('type', '')
-    count = request.args.get('count', 10, type=int)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if resource_type:
-        # Spezifische Ressourcen für einen Typ
-        cur.execute("""
-            SELECT id, resource_value
-            FROM resource_pools
-            WHERE status = 'available' 
-            AND resource_type = %s
-            AND is_test = %s
-            ORDER BY resource_value
-            LIMIT %s
-        """, (resource_type, show_test, count))
-        
-        resources = []
-        for row in cur.fetchall():
-            resources.append({
-                'id': row[0],
-                'value': row[1]
-            })
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'available': resources,
-            'type': resource_type,
-            'count': len(resources)
-        })
-    else:
-        # Zusammenfassung aller Typen
-        cur.execute("""
-            SELECT 
-                resource_type,
-                COUNT(*) as available
-            FROM resource_pools
-            WHERE status = 'available'
-            AND is_test = %s
-            GROUP BY resource_type
-        """, (show_test,))
-        
-        availability = {}
-        for row in cur.fetchall():
-            availability[row[0]] = row[1]
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify(availability)
-
-@app.route('/api/global-search', methods=['GET'])
-@login_required
-def global_search():
-    """Global search API endpoint for searching customers and licenses"""
-    query = request.args.get('q', '').strip()
-    
-    if not query or len(query) < 2:
-        return jsonify({'customers': [], 'licenses': []})
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Search pattern with wildcards
-    search_pattern = f'%{query}%'
-    
-    # Search customers
-    cur.execute("""
-        SELECT id, name, email, company_name
-        FROM customers
-        WHERE (LOWER(name) LIKE LOWER(%s) 
-               OR LOWER(email) LIKE LOWER(%s)
-               OR LOWER(company_name) LIKE LOWER(%s))
-        AND is_test = FALSE
-        ORDER BY name
-        LIMIT 5
-    """, (search_pattern, search_pattern, search_pattern))
-    
-    customers = []
-    for row in cur.fetchall():
-        customers.append({
-            'id': row[0],
-            'name': row[1],
-            'email': row[2],
-            'company_name': row[3]
-        })
-    
-    # Search licenses
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name as customer_name
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE LOWER(l.license_key) LIKE LOWER(%s)
-        AND l.is_test = FALSE
-        ORDER BY l.created_at DESC
-        LIMIT 5
-    """, (search_pattern,))
-    
-    licenses = []
-    for row in cur.fetchall():
-        licenses.append({
-            'id': row[0],
-            'license_key': row[1],
-            'customer_name': row[2]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'customers': customers,
-        'licenses': licenses
-    })
-
-@app.route('/resources/history/<int:resource_id>')
-@login_required
-def resource_history(resource_id):
-    """Zeigt die komplette Historie einer Ressource"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Get complete resource info using named columns
-    cur.execute("""
-        SELECT id, resource_type, resource_value, status, allocated_to_license,
-               status_changed_at, status_changed_by, quarantine_reason, 
-               quarantine_until, created_at, notes
-        FROM resource_pools
-        WHERE id = %s
-    """, (resource_id,))
-    row = cur.fetchone()
-    
-    if not row:
-        flash('Ressource nicht gefunden', 'error')
-        return redirect(url_for('resources'))
-    
-    # Create resource object with named attributes
-    resource = {
-        'id': row[0],
-        'resource_type': row[1],
-        'resource_value': row[2],
-        'status': row[3],
-        'allocated_to_license': row[4],
-        'status_changed_at': row[5],
-        'status_changed_by': row[6],
-        'quarantine_reason': row[7],
-        'quarantine_until': row[8],
-        'created_at': row[9],
-        'notes': row[10]
-    }
-    
-    # Get license info if allocated
-    license_info = None
-    if resource['allocated_to_license']:
-        cur.execute("SELECT license_key FROM licenses WHERE id = %s", 
-                   (resource['allocated_to_license'],))
-        lic = cur.fetchone()
-        if lic:
-            license_info = {'license_key': lic[0]}
-    
-    # Get history with named columns
-    cur.execute("""
-        SELECT 
-            rh.action,
-            rh.action_by,
-            rh.action_at,
-            rh.details,
-            rh.license_id,
-            rh.ip_address
-        FROM resource_history rh
-        WHERE rh.resource_id = %s
-        ORDER BY rh.action_at DESC
-    """, (resource_id,))
-    
-    history = []
-    for row in cur.fetchall():
-        history.append({
-            'action': row[0],
-            'action_by': row[1],
-            'action_at': row[2],
-            'details': row[3],
-            'license_id': row[4],
-            'ip_address': row[5]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    # Convert to object-like for template
-    class ResourceObj:
-        def __init__(self, data):
-            for key, value in data.items():
-                setattr(self, key, value)
-    
-    resource_obj = ResourceObj(resource)
-    history_objs = [ResourceObj(h) for h in history]
-    
-    return render_template('resource_history.html',
-                         resource=resource_obj,
-                         license_info=license_info,
-                         history=history_objs)
-
-@app.route('/resources/metrics')
-@login_required
-def resources_metrics():
-    """Dashboard für Resource Metrics und Reports"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Overall stats with fallback values
-    cur.execute("""
-        SELECT 
-            COUNT(DISTINCT resource_id) as total_resources,
-            COALESCE(AVG(performance_score), 0) as avg_performance,
-            COALESCE(SUM(cost), 0) as total_cost,
-            COALESCE(SUM(revenue), 0) as total_revenue,
-            COALESCE(SUM(issues_count), 0) as total_issues
-        FROM resource_metrics
-        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
-    """)
-    row = cur.fetchone()
-    
-    # Calculate ROI
-    roi = 0
-    if row[2] > 0:  # if total_cost > 0
-        roi = row[3] / row[2]  # revenue / cost
-    
-    stats = {
-        'total_resources': row[0] or 0,
-        'avg_performance': row[1] or 0,
-        'total_cost': row[2] or 0,
-        'total_revenue': row[3] or 0,
-        'total_issues': row[4] or 0,
-        'roi': roi
-    }
-    
-    # Performance by type
-    cur.execute("""
-        SELECT 
-            rp.resource_type,
-            COALESCE(AVG(rm.performance_score), 0) as avg_score,
-            COUNT(DISTINCT rp.id) as resource_count
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        GROUP BY rp.resource_type
-        ORDER BY rp.resource_type
-    """)
-    performance_by_type = cur.fetchall()
-    
-    # Utilization data
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) as total,
-            ROUND(COUNT(*) FILTER (WHERE status = 'allocated') * 100.0 / COUNT(*), 1) as allocated_percent
-        FROM resource_pools
-        GROUP BY resource_type
-    """)
-    utilization_rows = cur.fetchall()
-    utilization_data = [
-        {
-            'type': row[0].upper(),
-            'allocated': row[1],
-            'total': row[2],
-            'allocated_percent': row[3]
-        }
-        for row in utilization_rows
-    ]
-    
-    # Top performing resources
-    cur.execute("""
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            COALESCE(AVG(rm.performance_score), 0) as avg_score,
-            COALESCE(SUM(rm.revenue), 0) as total_revenue,
-            COALESCE(SUM(rm.cost), 1) as total_cost,
-            CASE 
-                WHEN COALESCE(SUM(rm.cost), 0) = 0 THEN 0
-                ELSE COALESCE(SUM(rm.revenue), 0) / COALESCE(SUM(rm.cost), 1)
-            END as roi
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        WHERE rp.status != 'quarantine'
-        GROUP BY rp.id, rp.resource_type, rp.resource_value
-        HAVING AVG(rm.performance_score) IS NOT NULL
-        ORDER BY avg_score DESC
-        LIMIT 10
-    """)
-    top_rows = cur.fetchall()
-    top_performers = [
-        {
-            'id': row[0],
-            'resource_type': row[1],
-            'resource_value': row[2],
-            'avg_score': row[3],
-            'roi': row[6]
-        }
-        for row in top_rows
-    ]
-    
-    # Resources with issues
-    cur.execute("""
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            rp.status,
-            COALESCE(SUM(rm.issues_count), 0) as total_issues
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        WHERE rm.issues_count > 0 OR rp.status = 'quarantine'
-        GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
-        HAVING SUM(rm.issues_count) > 0
-        ORDER BY total_issues DESC
-        LIMIT 10
-    """)
-    problem_rows = cur.fetchall()
-    problem_resources = [
-        {
-            'id': row[0],
-            'resource_type': row[1],
-            'resource_value': row[2],
-            'status': row[3],
-            'total_issues': row[4]
-        }
-        for row in problem_rows
-    ]
-    
-    # Daily metrics for trend chart (last 30 days)
-    cur.execute("""
-        SELECT 
-            metric_date,
-            COALESCE(AVG(performance_score), 0) as avg_performance,
-            COALESCE(SUM(issues_count), 0) as total_issues
-        FROM resource_metrics
-        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        GROUP BY metric_date
-        ORDER BY metric_date
-    """)
-    daily_rows = cur.fetchall()
-    daily_metrics = [
-        {
-            'date': row[0].strftime('%d.%m'),
-            'performance': float(row[1]),
-            'issues': int(row[2])
-        }
-        for row in daily_rows
-    ]
-    
-    cur.close()
-    conn.close()
-    
-    return render_template('resource_metrics.html',
-                         stats=stats,
-                         performance_by_type=performance_by_type,
-                         utilization_data=utilization_data,
-                         top_performers=top_performers,
-                         problem_resources=problem_resources,
-                         daily_metrics=daily_metrics)
-
-@app.route('/resources/report', methods=['GET'])
-@login_required
-def resources_report():
-    """Generiert Ressourcen-Reports oder zeigt Report-Formular"""
-    # Prüfe ob Download angefordert wurde
-    if request.args.get('download') == 'true':
-        report_type = request.args.get('type', 'usage')
-        format_type = request.args.get('format', 'excel')
-        date_from = request.args.get('from', (datetime.now(ZoneInfo("Europe/Berlin")) - timedelta(days=30)).strftime('%Y-%m-%d'))
-        date_to = request.args.get('to', datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        if report_type == 'usage':
-            # Auslastungsreport
-            query = """
-                SELECT 
-                    rp.resource_type,
-                    rp.resource_value,
-                    rp.status,
-                    COUNT(DISTINCT rh.license_id) as unique_licenses,
-                    COUNT(rh.id) as total_allocations,
-                    MIN(rh.action_at) as first_used,
-                    MAX(rh.action_at) as last_used
-                FROM resource_pools rp
-                LEFT JOIN resource_history rh ON rp.id = rh.resource_id 
-                    AND rh.action = 'allocated'
-                    AND rh.action_at BETWEEN %s AND %s
-                GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
-                ORDER BY rp.resource_type, total_allocations DESC
-            """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Typ', 'Ressource', 'Status', 'Unique Lizenzen', 'Gesamt Zuweisungen', 'Erste Nutzung', 'Letzte Nutzung']
-            
-        elif report_type == 'performance':
-            # Performance-Report
-            query = """
-                SELECT 
-                    rp.resource_type,
-                rp.resource_value,
-                AVG(rm.performance_score) as avg_performance,
-                SUM(rm.usage_count) as total_usage,
-                SUM(rm.revenue) as total_revenue,
-                SUM(rm.cost) as total_cost,
-                SUM(rm.revenue - rm.cost) as profit,
-                SUM(rm.issues_count) as total_issues
-            FROM resource_pools rp
-            JOIN resource_metrics rm ON rp.id = rm.resource_id
-            WHERE rm.metric_date BETWEEN %s AND %s
-            GROUP BY rp.id, rp.resource_type, rp.resource_value
-            ORDER BY profit DESC
-        """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Typ', 'Ressource', 'Durchschn. Performance', 'Gesamt Nutzung', 'Umsatz', 'Kosten', 'Gewinn', 'Issues']
-            
-        elif report_type == 'compliance':
-            # Compliance-Report
-            query = """
-            SELECT 
-                rh.action_at,
-                rh.action,
-                rh.action_by,
-                rp.resource_type,
-                rp.resource_value,
-                l.license_key,
-                c.name as customer_name,
-                rh.ip_address
-            FROM resource_history rh
-            JOIN resource_pools rp ON rh.resource_id = rp.id
-            LEFT JOIN licenses l ON rh.license_id = l.id
-            LEFT JOIN customers c ON l.customer_id = c.id
-            WHERE rh.action_at BETWEEN %s AND %s
-                ORDER BY rh.action_at DESC
-            """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Zeit', 'Aktion', 'Von', 'Typ', 'Ressource', 'Lizenz', 'Kunde', 'IP-Adresse']
-        
-        else:  # inventory report
-            # Inventar-Report
-            query = """
-                SELECT 
-                    resource_type,
-                    COUNT(*) FILTER (WHERE status = 'available') as available,
-                    COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-                    COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-                    COUNT(*) as total
-                FROM resource_pools
-                GROUP BY resource_type
-                ORDER BY resource_type
-            """
-            cur.execute(query)
-            columns = ['Typ', 'Verfügbar', 'Zugeteilt', 'Quarantäne', 'Gesamt']
-        
-        # Convert to DataFrame
-        data = cur.fetchall()
-        df = pd.DataFrame(data, columns=columns)
-        
-        cur.close()
-        conn.close()
-        
-        # Generate file
-        timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
-        filename = f"resource_report_{report_type}_{timestamp}"
-        
-        if format_type == 'excel':
-            output = io.BytesIO()
-            with pd.ExcelWriter(output, engine='openpyxl') as writer:
-                df.to_excel(writer, sheet_name='Report', index=False)
-                
-                # Auto-adjust columns width
-                worksheet = writer.sheets['Report']
-                for column in worksheet.columns:
-                    max_length = 0
-                    column = [cell for cell in column]
-                    for cell in column:
-                        try:
-                            if len(str(cell.value)) > max_length:
-                                max_length = len(str(cell.value))
-                        except:
-                            pass
-                    adjusted_width = (max_length + 2)
-                    worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
-            
-            output.seek(0)
-            
-            log_audit('EXPORT', 'resource_report', None,
-                     new_values={'type': report_type, 'format': 'excel', 'rows': len(df)},
-                     additional_info=f"Resource Report {report_type} exportiert")
-            
-            return send_file(output, 
-                            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-                            as_attachment=True,
-                            download_name=f'{filename}.xlsx')
-        
-        else:  # CSV
-            output = io.StringIO()
-            df.to_csv(output, index=False, sep=';', encoding='utf-8-sig')
-            output.seek(0)
-            
-            log_audit('EXPORT', 'resource_report', None,
-                     new_values={'type': report_type, 'format': 'csv', 'rows': len(df)},
-                     additional_info=f"Resource Report {report_type} exportiert")
-            
-            return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')),
-                            mimetype='text/csv',
-                            as_attachment=True,
-                            download_name=f'{filename}.csv')
-    
-    # Wenn kein Download, zeige Report-Formular
-    return render_template('resource_report.html',
-                         datetime=datetime,
-                         timedelta=timedelta,
-                         username=session.get('username'))
-
-if __name__ == "__main__":
-    app.run(host="0.0.0.0", port=5000)
+import os
+import psycopg2
+from psycopg2.extras import Json
+from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash
+from flask_session import Session
+from functools import wraps
+from dotenv import load_dotenv
+import pandas as pd
+from datetime import datetime, timedelta
+from zoneinfo import ZoneInfo
+import io
+import subprocess
+import gzip
+from cryptography.fernet import Fernet
+from pathlib import Path
+import time
+from apscheduler.schedulers.background import BackgroundScheduler
+import logging
+import random
+import hashlib
+import requests
+import secrets
+import string
+import re
+import bcrypt
+import pyotp
+import qrcode
+from io import BytesIO
+import base64
+import json
+from werkzeug.middleware.proxy_fix import ProxyFix
+from openpyxl.utils import get_column_letter
+
+load_dotenv()
+
+app = Flask(__name__)
+app.config['SECRET_KEY'] = os.urandom(24)
+app.config['SESSION_TYPE'] = 'filesystem'
+app.config['JSON_AS_ASCII'] = False  # JSON-Ausgabe mit UTF-8
+app.config['JSONIFY_MIMETYPE'] = 'application/json; charset=utf-8'
+app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=5)  # 5 Minuten Session-Timeout
+app.config['SESSION_COOKIE_HTTPONLY'] = True
+app.config['SESSION_COOKIE_SECURE'] = False  # Wird auf True gesetzt wenn HTTPS (intern läuft HTTP)
+app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
+app.config['SESSION_COOKIE_NAME'] = 'admin_session'
+# WICHTIG: Session-Cookie soll auch nach 5 Minuten ablaufen
+app.config['SESSION_REFRESH_EACH_REQUEST'] = False
+Session(app)
+
+# ProxyFix für korrekte IP-Adressen hinter Nginx
+app.wsgi_app = ProxyFix(
+    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
+)
+
+# Backup-Konfiguration
+BACKUP_DIR = Path("/app/backups")
+BACKUP_DIR.mkdir(exist_ok=True)
+
+# Rate-Limiting Konfiguration
+FAIL_MESSAGES = [
+    "NOPE!",
+    "ACCESS DENIED, TRY HARDER",
+    "WRONG! 🚫",
+    "COMPUTER SAYS NO",
+    "YOU FAILED"
+]
+
+MAX_LOGIN_ATTEMPTS = 5
+BLOCK_DURATION_HOURS = 24
+CAPTCHA_AFTER_ATTEMPTS = 2
+
+# Scheduler für automatische Backups
+scheduler = BackgroundScheduler()
+scheduler.start()
+
+# Logging konfigurieren
+logging.basicConfig(level=logging.INFO)
+
+
+# Login decorator
+def login_required(f):
+    @wraps(f)
+    def decorated_function(*args, **kwargs):
+        if 'logged_in' not in session:
+            return redirect(url_for('login'))
+        
+        # Prüfe ob Session abgelaufen ist
+        if 'last_activity' in session:
+            last_activity = datetime.fromisoformat(session['last_activity'])
+            time_since_activity = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) - last_activity
+            
+            # Debug-Logging
+            app.logger.info(f"Session check for {session.get('username', 'unknown')}: "
+                          f"Last activity: {last_activity}, "
+                          f"Time since: {time_since_activity.total_seconds()} seconds")
+            
+            if time_since_activity > timedelta(minutes=5):
+                # Session abgelaufen - Logout
+                username = session.get('username', 'unbekannt')
+                app.logger.info(f"Session timeout for user {username} - auto logout")
+                # Audit-Log für automatischen Logout (vor session.clear()!)
+                try:
+                    log_audit('AUTO_LOGOUT', 'session', additional_info={'reason': 'Session timeout (5 minutes)', 'username': username})
+                except:
+                    pass
+                session.clear()
+                flash('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.', 'warning')
+                return redirect(url_for('login'))
+        
+        # Aktivität NICHT automatisch aktualisieren
+        # Nur bei expliziten Benutzeraktionen (wird vom Heartbeat gemacht)
+        return f(*args, **kwargs)
+    return decorated_function
+
+# DB-Verbindung mit UTF-8 Encoding
+def get_connection():
+    conn = psycopg2.connect(
+        host=os.getenv("POSTGRES_HOST", "postgres"),
+        port=os.getenv("POSTGRES_PORT", "5432"),
+        dbname=os.getenv("POSTGRES_DB"),
+        user=os.getenv("POSTGRES_USER"),
+        password=os.getenv("POSTGRES_PASSWORD"),
+        options='-c client_encoding=UTF8'
+    )
+    conn.set_client_encoding('UTF8')
+    return conn
+
+# User Authentication Helper Functions
+def hash_password(password):
+    """Hash a password using bcrypt"""
+    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+
+def verify_password(password, hashed):
+    """Verify a password against its hash"""
+    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
+
+def get_user_by_username(username):
+    """Get user from database by username"""
+    conn = get_connection()
+    cur = conn.cursor()
+    try:
+        cur.execute("""
+            SELECT id, username, password_hash, email, totp_secret, totp_enabled, 
+                   backup_codes, last_password_change, failed_2fa_attempts
+            FROM users WHERE username = %s
+        """, (username,))
+        user = cur.fetchone()
+        if user:
+            return {
+                'id': user[0],
+                'username': user[1],
+                'password_hash': user[2],
+                'email': user[3],
+                'totp_secret': user[4],
+                'totp_enabled': user[5],
+                'backup_codes': user[6],
+                'last_password_change': user[7],
+                'failed_2fa_attempts': user[8]
+            }
+        return None
+    finally:
+        cur.close()
+        conn.close()
+
+def generate_totp_secret():
+    """Generate a new TOTP secret"""
+    return pyotp.random_base32()
+
+def generate_qr_code(username, totp_secret):
+    """Generate QR code for TOTP setup"""
+    totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
+        name=username,
+        issuer_name='V2 Admin Panel'
+    )
+    
+    qr = qrcode.QRCode(version=1, box_size=10, border=5)
+    qr.add_data(totp_uri)
+    qr.make(fit=True)
+    
+    img = qr.make_image(fill_color="black", back_color="white")
+    buf = BytesIO()
+    img.save(buf, format='PNG')
+    buf.seek(0)
+    
+    return base64.b64encode(buf.getvalue()).decode()
+
+def verify_totp(totp_secret, token):
+    """Verify a TOTP token"""
+    totp = pyotp.TOTP(totp_secret)
+    return totp.verify(token, valid_window=1)
+
+def generate_backup_codes(count=8):
+    """Generate backup codes for 2FA recovery"""
+    codes = []
+    for _ in range(count):
+        code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+        codes.append(code)
+    return codes
+
+def hash_backup_code(code):
+    """Hash a backup code for storage"""
+    return hashlib.sha256(code.encode()).hexdigest()
+
+def verify_backup_code(code, hashed_codes):
+    """Verify a backup code against stored hashes"""
+    code_hash = hashlib.sha256(code.encode()).hexdigest()
+    return code_hash in hashed_codes
+
+# Audit-Log-Funktion
+def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=None, additional_info=None):
+    """Protokolliert Änderungen im Audit-Log"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        username = session.get('username', 'system')
+        ip_address = get_client_ip() if request else None
+        user_agent = request.headers.get('User-Agent') if request else None
+        
+        # Debug logging
+        app.logger.info(f"Audit log - IP address captured: {ip_address}, Action: {action}, User: {username}")
+        
+        # Konvertiere Dictionaries zu JSONB
+        old_json = Json(old_values) if old_values else None
+        new_json = Json(new_values) if new_values else None
+        
+        cur.execute("""
+            INSERT INTO audit_log 
+            (username, action, entity_type, entity_id, old_values, new_values, 
+             ip_address, user_agent, additional_info)
+            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+        """, (username, action, entity_type, entity_id, old_json, new_json, 
+              ip_address, user_agent, additional_info))
+        
+        conn.commit()
+    except Exception as e:
+        print(f"Audit log error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+
+# Verschlüsselungs-Funktionen
+def get_or_create_encryption_key():
+    """Holt oder erstellt einen Verschlüsselungsschlüssel"""
+    key_file = BACKUP_DIR / ".backup_key"
+    
+    # Versuche Key aus Umgebungsvariable zu lesen
+    env_key = os.getenv("BACKUP_ENCRYPTION_KEY")
+    if env_key:
+        try:
+            # Validiere den Key
+            Fernet(env_key.encode())
+            return env_key.encode()
+        except:
+            pass
+    
+    # Wenn kein gültiger Key in ENV, prüfe Datei
+    if key_file.exists():
+        return key_file.read_bytes()
+    
+    # Erstelle neuen Key
+    key = Fernet.generate_key()
+    key_file.write_bytes(key)
+    logging.info("Neuer Backup-Verschlüsselungsschlüssel erstellt")
+    return key
+
+# Backup-Funktionen
+def create_backup(backup_type="manual", created_by=None):
+    """Erstellt ein verschlüsseltes Backup der Datenbank"""
+    start_time = time.time()
+    timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")
+    filename = f"backup_v2docker_{timestamp}_encrypted.sql.gz.enc"
+    filepath = BACKUP_DIR / filename
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Backup-Eintrag erstellen
+    cur.execute("""
+        INSERT INTO backup_history 
+        (filename, filepath, backup_type, status, created_by, is_encrypted)
+        VALUES (%s, %s, %s, %s, %s, %s)
+        RETURNING id
+    """, (filename, str(filepath), backup_type, 'in_progress', 
+          created_by or 'system', True))
+    backup_id = cur.fetchone()[0]
+    conn.commit()
+    
+    try:
+        # PostgreSQL Dump erstellen
+        dump_command = [
+            'pg_dump',
+            '-h', os.getenv("POSTGRES_HOST", "postgres"),
+            '-p', os.getenv("POSTGRES_PORT", "5432"),
+            '-U', os.getenv("POSTGRES_USER"),
+            '-d', os.getenv("POSTGRES_DB"),
+            '--no-password',
+            '--verbose'
+        ]
+        
+        # PGPASSWORD setzen
+        env = os.environ.copy()
+        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
+        
+        # Dump ausführen
+        result = subprocess.run(dump_command, capture_output=True, text=True, env=env)
+        
+        if result.returncode != 0:
+            raise Exception(f"pg_dump failed: {result.stderr}")
+        
+        dump_data = result.stdout.encode('utf-8')
+        
+        # Komprimieren
+        compressed_data = gzip.compress(dump_data)
+        
+        # Verschlüsseln
+        key = get_or_create_encryption_key()
+        f = Fernet(key)
+        encrypted_data = f.encrypt(compressed_data)
+        
+        # Speichern
+        filepath.write_bytes(encrypted_data)
+        
+        # Statistiken sammeln
+        cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'")
+        tables_count = cur.fetchone()[0]
+        
+        cur.execute("""
+            SELECT SUM(n_live_tup) 
+            FROM pg_stat_user_tables
+        """)
+        records_count = cur.fetchone()[0] or 0
+        
+        duration = time.time() - start_time
+        filesize = filepath.stat().st_size
+        
+        # Backup-Eintrag aktualisieren
+        cur.execute("""
+            UPDATE backup_history 
+            SET status = %s, filesize = %s, tables_count = %s, 
+                records_count = %s, duration_seconds = %s
+            WHERE id = %s
+        """, ('success', filesize, tables_count, records_count, duration, backup_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('BACKUP', 'database', backup_id,
+                 additional_info=f"Backup erstellt: {filename} ({filesize} bytes)")
+        
+        # E-Mail-Benachrichtigung (wenn konfiguriert)
+        send_backup_notification(True, filename, filesize, duration)
+        
+        logging.info(f"Backup erfolgreich erstellt: {filename}")
+        return True, filename
+        
+    except Exception as e:
+        # Fehler protokollieren
+        cur.execute("""
+            UPDATE backup_history 
+            SET status = %s, error_message = %s, duration_seconds = %s
+            WHERE id = %s
+        """, ('failed', str(e), time.time() - start_time, backup_id))
+        conn.commit()
+        
+        logging.error(f"Backup fehlgeschlagen: {e}")
+        send_backup_notification(False, filename, error=str(e))
+        
+        return False, str(e)
+    
+    finally:
+        cur.close()
+        conn.close()
+
+def restore_backup(backup_id, encryption_key=None):
+    """Stellt ein Backup wieder her"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Backup-Info abrufen
+        cur.execute("""
+            SELECT filename, filepath, is_encrypted 
+            FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        backup_info = cur.fetchone()
+        
+        if not backup_info:
+            raise Exception("Backup nicht gefunden")
+        
+        filename, filepath, is_encrypted = backup_info
+        filepath = Path(filepath)
+        
+        if not filepath.exists():
+            raise Exception("Backup-Datei nicht gefunden")
+        
+        # Datei lesen
+        encrypted_data = filepath.read_bytes()
+        
+        # Entschlüsseln
+        if is_encrypted:
+            key = encryption_key.encode() if encryption_key else get_or_create_encryption_key()
+            try:
+                f = Fernet(key)
+                compressed_data = f.decrypt(encrypted_data)
+            except:
+                raise Exception("Entschlüsselung fehlgeschlagen. Falsches Passwort?")
+        else:
+            compressed_data = encrypted_data
+        
+        # Dekomprimieren
+        dump_data = gzip.decompress(compressed_data)
+        sql_commands = dump_data.decode('utf-8')
+        
+        # Bestehende Verbindungen schließen
+        cur.close()
+        conn.close()
+        
+        # Datenbank wiederherstellen
+        restore_command = [
+            'psql',
+            '-h', os.getenv("POSTGRES_HOST", "postgres"),
+            '-p', os.getenv("POSTGRES_PORT", "5432"),
+            '-U', os.getenv("POSTGRES_USER"),
+            '-d', os.getenv("POSTGRES_DB"),
+            '--no-password'
+        ]
+        
+        env = os.environ.copy()
+        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
+        
+        result = subprocess.run(restore_command, input=sql_commands, 
+                              capture_output=True, text=True, env=env)
+        
+        if result.returncode != 0:
+            raise Exception(f"Wiederherstellung fehlgeschlagen: {result.stderr}")
+        
+        # Audit-Log (neue Verbindung)
+        log_audit('RESTORE', 'database', backup_id,
+                 additional_info=f"Backup wiederhergestellt: {filename}")
+        
+        return True, "Backup erfolgreich wiederhergestellt"
+        
+    except Exception as e:
+        logging.error(f"Wiederherstellung fehlgeschlagen: {e}")
+        return False, str(e)
+
+def send_backup_notification(success, filename, filesize=None, duration=None, error=None):
+    """Sendet E-Mail-Benachrichtigung (wenn konfiguriert)"""
+    if not os.getenv("EMAIL_ENABLED", "false").lower() == "true":
+        return
+    
+    # E-Mail-Funktion vorbereitet aber deaktiviert
+    # TODO: Implementieren wenn E-Mail-Server konfiguriert ist
+    logging.info(f"E-Mail-Benachrichtigung vorbereitet: Backup {'erfolgreich' if success else 'fehlgeschlagen'}")
+
+# Scheduled Backup Job
+def scheduled_backup():
+    """Führt ein geplantes Backup aus"""
+    logging.info("Starte geplantes Backup...")
+    create_backup(backup_type="scheduled", created_by="scheduler")
+
+# Scheduler konfigurieren - täglich um 3:00 Uhr
+scheduler.add_job(
+    scheduled_backup,
+    'cron',
+    hour=3,
+    minute=0,
+    id='daily_backup',
+    replace_existing=True
+)
+
+# Rate-Limiting Funktionen
+def get_client_ip():
+    """Ermittelt die echte IP-Adresse des Clients"""
+    # Debug logging
+    app.logger.info(f"Headers - X-Real-IP: {request.headers.get('X-Real-IP')}, X-Forwarded-For: {request.headers.get('X-Forwarded-For')}, Remote-Addr: {request.remote_addr}")
+    
+    # Try X-Real-IP first (set by nginx)
+    if request.headers.get('X-Real-IP'):
+        return request.headers.get('X-Real-IP')
+    # Then X-Forwarded-For
+    elif request.headers.get('X-Forwarded-For'):
+        # X-Forwarded-For can contain multiple IPs, take the first one
+        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
+    # Fallback to remote_addr
+    else:
+        return request.remote_addr
+
+def check_ip_blocked(ip_address):
+    """Prüft ob eine IP-Adresse gesperrt ist"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT blocked_until FROM login_attempts
+        WHERE ip_address = %s AND blocked_until IS NOT NULL
+    """, (ip_address,))
+    
+    result = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    if result and result[0]:
+        if result[0] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None):
+            return True, result[0]
+    return False, None
+
+def record_failed_attempt(ip_address, username):
+    """Zeichnet einen fehlgeschlagenen Login-Versuch auf"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Random Fehlermeldung
+    error_message = random.choice(FAIL_MESSAGES)
+    
+    try:
+        # Prüfen ob IP bereits existiert
+        cur.execute("""
+            SELECT attempt_count FROM login_attempts
+            WHERE ip_address = %s
+        """, (ip_address,))
+        
+        result = cur.fetchone()
+        
+        if result:
+            # Update bestehenden Eintrag
+            new_count = result[0] + 1
+            blocked_until = None
+            
+            if new_count >= MAX_LOGIN_ATTEMPTS:
+                blocked_until = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) + timedelta(hours=BLOCK_DURATION_HOURS)
+                # E-Mail-Benachrichtigung (wenn aktiviert)
+                if os.getenv("EMAIL_ENABLED", "false").lower() == "true":
+                    send_security_alert_email(ip_address, username, new_count)
+            
+            cur.execute("""
+                UPDATE login_attempts
+                SET attempt_count = %s,
+                    last_attempt = CURRENT_TIMESTAMP,
+                    blocked_until = %s,
+                    last_username_tried = %s,
+                    last_error_message = %s
+                WHERE ip_address = %s
+            """, (new_count, blocked_until, username, error_message, ip_address))
+        else:
+            # Neuen Eintrag erstellen
+            cur.execute("""
+                INSERT INTO login_attempts 
+                (ip_address, attempt_count, last_username_tried, last_error_message)
+                VALUES (%s, 1, %s, %s)
+            """, (ip_address, username, error_message))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('LOGIN_FAILED', 'user', 
+                 additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
+        
+    except Exception as e:
+        print(f"Rate limiting error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+    
+    return error_message
+
+def reset_login_attempts(ip_address):
+    """Setzt die Login-Versuche für eine IP zurück"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        cur.execute("""
+            DELETE FROM login_attempts
+            WHERE ip_address = %s
+        """, (ip_address,))
+        conn.commit()
+    except Exception as e:
+        print(f"Reset attempts error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+
+def get_login_attempts(ip_address):
+    """Gibt die Anzahl der Login-Versuche für eine IP zurück"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT attempt_count FROM login_attempts
+        WHERE ip_address = %s
+    """, (ip_address,))
+    
+    result = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    return result[0] if result else 0
+
+def send_security_alert_email(ip_address, username, attempt_count):
+    """Sendet eine Sicherheitswarnung per E-Mail"""
+    subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
+    body = f"""
+    WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
+    
+    IP-Adresse: {ip_address}
+    Versuchter Benutzername: {username}
+    Anzahl Versuche: {attempt_count}
+    Zeit: {datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d %H:%M:%S')}
+    
+    Die IP-Adresse wurde für 24 Stunden gesperrt.
+    
+    Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
+    """
+    
+    # TODO: E-Mail-Versand implementieren wenn SMTP konfiguriert
+    logging.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
+    print(f"E-Mail würde gesendet: {subject}")
+
+def verify_recaptcha(response):
+    """Verifiziert die reCAPTCHA v2 Response mit Google"""
+    secret_key = os.getenv('RECAPTCHA_SECRET_KEY')
+    
+    # Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC)
+    if not secret_key:
+        logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen")
+        return True
+    
+    # Verifizierung bei Google
+    try:
+        verify_url = 'https://www.google.com/recaptcha/api/siteverify'
+        data = {
+            'secret': secret_key,
+            'response': response
+        }
+        
+        # Timeout für Request setzen
+        r = requests.post(verify_url, data=data, timeout=5)
+        result = r.json()
+        
+        # Log für Debugging
+        if not result.get('success'):
+            logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}")
+        
+        return result.get('success', False)
+    
+    except requests.exceptions.RequestException as e:
+        logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}")
+        # Bei Netzwerkfehlern CAPTCHA als bestanden werten
+        return True
+    except Exception as e:
+        logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
+        return False
+
+def generate_license_key(license_type='full'):
+    """
+    Generiert einen Lizenzschlüssel im Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ
+    
+    AF = Account Factory (Produktkennung)
+    F/T = F für Fullversion, T für Testversion
+    YYYY = Jahr
+    MM = Monat
+    XXXX-YYYY-ZZZZ = Zufällige alphanumerische Zeichen
+    """
+    # Erlaubte Zeichen (ohne verwirrende wie 0/O, 1/I/l)
+    chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
+    
+    # Datum-Teil
+    now = datetime.now(ZoneInfo("Europe/Berlin"))
+    date_part = now.strftime('%Y%m')
+    type_char = 'F' if license_type == 'full' else 'T'
+    
+    # Zufällige Teile generieren (3 Blöcke à 4 Zeichen)
+    parts = []
+    for _ in range(3):
+        part = ''.join(secrets.choice(chars) for _ in range(4))
+        parts.append(part)
+    
+    # Key zusammensetzen
+    key = f"AF-{type_char}-{date_part}-{parts[0]}-{parts[1]}-{parts[2]}"
+    
+    return key
+
+def validate_license_key(key):
+    """
+    Validiert das License Key Format
+    Erwartet: AF-F-YYYYMM-XXXX-YYYY-ZZZZ oder AF-T-YYYYMM-XXXX-YYYY-ZZZZ
+    """
+    if not key:
+        return False
+    
+    # Pattern für das neue Format
+    # AF- (fest) + F oder T + - + 6 Ziffern (YYYYMM) + - + 4 Zeichen + - + 4 Zeichen + - + 4 Zeichen
+    pattern = r'^AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
+    
+    # Großbuchstaben für Vergleich
+    return bool(re.match(pattern, key.upper()))
+
+@app.route("/login", methods=["GET", "POST"])
+def login():
+    # Timing-Attack Schutz - Start Zeit merken
+    start_time = time.time()
+    
+    # IP-Adresse ermitteln
+    ip_address = get_client_ip()
+    
+    # Prüfen ob IP gesperrt ist
+    is_blocked, blocked_until = check_ip_blocked(ip_address)
+    if is_blocked:
+        time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
+        error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
+        return render_template("login.html", error=error_msg, error_type="blocked")
+    
+    # Anzahl bisheriger Versuche
+    attempt_count = get_login_attempts(ip_address)
+    
+    if request.method == "POST":
+        username = request.form.get("username")
+        password = request.form.get("password")
+        captcha_response = request.form.get("g-recaptcha-response")
+        
+        # CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
+        recaptcha_site_key = os.getenv('RECAPTCHA_SITE_KEY')
+        if attempt_count >= CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
+            if not captcha_response:
+                # Timing-Attack Schutz
+                elapsed = time.time() - start_time
+                if elapsed < 1.0:
+                    time.sleep(1.0 - elapsed)
+                return render_template("login.html", 
+                                     error="CAPTCHA ERFORDERLICH!", 
+                                     show_captcha=True,
+                                     error_type="captcha",
+                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     recaptcha_site_key=recaptcha_site_key)
+            
+            # CAPTCHA validieren
+            if not verify_recaptcha(captcha_response):
+                # Timing-Attack Schutz
+                elapsed = time.time() - start_time
+                if elapsed < 1.0:
+                    time.sleep(1.0 - elapsed)
+                return render_template("login.html", 
+                                     error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
+                                     show_captcha=True,
+                                     error_type="captcha",
+                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     recaptcha_site_key=recaptcha_site_key)
+        
+        # Check user in database first, fallback to env vars
+        user = get_user_by_username(username)
+        login_success = False
+        needs_2fa = False
+        
+        if user:
+            # Database user authentication
+            if verify_password(password, user['password_hash']):
+                login_success = True
+                needs_2fa = user['totp_enabled']
+        else:
+            # Fallback to environment variables for backward compatibility
+            admin1_user = os.getenv("ADMIN1_USERNAME")
+            admin1_pass = os.getenv("ADMIN1_PASSWORD")
+            admin2_user = os.getenv("ADMIN2_USERNAME")
+            admin2_pass = os.getenv("ADMIN2_PASSWORD")
+            
+            if ((username == admin1_user and password == admin1_pass) or 
+                (username == admin2_user and password == admin2_pass)):
+                login_success = True
+        
+        # Timing-Attack Schutz - Mindestens 1 Sekunde warten
+        elapsed = time.time() - start_time
+        if elapsed < 1.0:
+            time.sleep(1.0 - elapsed)
+        
+        if login_success:
+            # Erfolgreicher Login
+            if needs_2fa:
+                # Store temporary session for 2FA verification
+                session['temp_username'] = username
+                session['temp_user_id'] = user['id']
+                session['awaiting_2fa'] = True
+                return redirect(url_for('verify_2fa'))
+            else:
+                # Complete login without 2FA
+                session.permanent = True  # Aktiviert das Timeout
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user['id'] if user else None
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                reset_login_attempts(ip_address)
+                log_audit('LOGIN_SUCCESS', 'user', 
+                         additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
+                return redirect(url_for('dashboard'))
+        else:
+            # Fehlgeschlagener Login
+            error_message = record_failed_attempt(ip_address, username)
+            new_attempt_count = get_login_attempts(ip_address)
+            
+            # Prüfen ob jetzt gesperrt
+            is_now_blocked, _ = check_ip_blocked(ip_address)
+            if is_now_blocked:
+                log_audit('LOGIN_BLOCKED', 'security', 
+                         additional_info=f"IP {ip_address} wurde nach {MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
+            
+            return render_template("login.html", 
+                                 error=error_message,
+                                 show_captcha=(new_attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
+                                 error_type="failed",
+                                 attempts_left=max(0, MAX_LOGIN_ATTEMPTS - new_attempt_count),
+                                 recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
+    
+    # GET Request
+    return render_template("login.html", 
+                         show_captcha=(attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
+                         attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                         recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
+
+@app.route("/logout")
+def logout():
+    username = session.get('username', 'unknown')
+    log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
+    session.pop('logged_in', None)
+    session.pop('username', None)
+    session.pop('user_id', None)
+    session.pop('temp_username', None)
+    session.pop('temp_user_id', None)
+    session.pop('awaiting_2fa', None)
+    return redirect(url_for('login'))
+
+@app.route("/verify-2fa", methods=["GET", "POST"])
+def verify_2fa():
+    if not session.get('awaiting_2fa'):
+        return redirect(url_for('login'))
+    
+    if request.method == "POST":
+        token = request.form.get('token', '').replace(' ', '')
+        username = session.get('temp_username')
+        user_id = session.get('temp_user_id')
+        
+        if not username or not user_id:
+            flash('Session expired. Please login again.', 'error')
+            return redirect(url_for('login'))
+        
+        user = get_user_by_username(username)
+        if not user:
+            flash('User not found.', 'error')
+            return redirect(url_for('login'))
+        
+        # Check if it's a backup code
+        if len(token) == 8 and token.isupper():
+            # Try backup code
+            backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
+            if verify_backup_code(token, backup_codes):
+                # Remove used backup code
+                code_hash = hash_backup_code(token)
+                backup_codes.remove(code_hash)
+                
+                conn = get_connection()
+                cur = conn.cursor()
+                cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
+                          (json.dumps(backup_codes), user_id))
+                conn.commit()
+                cur.close()
+                conn.close()
+                
+                # Complete login
+                session.permanent = True
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user_id
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                session.pop('temp_username', None)
+                session.pop('temp_user_id', None)
+                session.pop('awaiting_2fa', None)
+                
+                flash('Login successful using backup code. Please generate new backup codes.', 'warning')
+                log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
+                return redirect(url_for('dashboard'))
+        else:
+            # Try TOTP token
+            if verify_totp(user['totp_secret'], token):
+                # Complete login
+                session.permanent = True
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user_id
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                session.pop('temp_username', None)
+                session.pop('temp_user_id', None)
+                session.pop('awaiting_2fa', None)
+                
+                log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
+                return redirect(url_for('dashboard'))
+        
+        # Failed verification
+        conn = get_connection()
+        cur = conn.cursor()
+        cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
+                  (datetime.now(), user_id))
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        flash('Invalid authentication code. Please try again.', 'error')
+        log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
+    
+    return render_template('verify_2fa.html')
+
+@app.route("/profile")
+@login_required
+def profile():
+    user = get_user_by_username(session['username'])
+    if not user:
+        # For environment-based users, redirect with message
+        flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info')
+        return redirect(url_for('dashboard'))
+    return render_template('profile.html', user=user)
+
+@app.route("/profile/change-password", methods=["POST"])
+@login_required
+def change_password():
+    current_password = request.form.get('current_password')
+    new_password = request.form.get('new_password')
+    confirm_password = request.form.get('confirm_password')
+    
+    user = get_user_by_username(session['username'])
+    
+    # Verify current password
+    if not verify_password(current_password, user['password_hash']):
+        flash('Current password is incorrect.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Check new password
+    if new_password != confirm_password:
+        flash('New passwords do not match.', 'error')
+        return redirect(url_for('profile'))
+    
+    if len(new_password) < 8:
+        flash('Password must be at least 8 characters long.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Update password
+    new_hash = hash_password(new_password)
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s",
+              (new_hash, datetime.now(), user['id']))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], 
+             additional_info="Password changed successfully")
+    flash('Password changed successfully.', 'success')
+    return redirect(url_for('profile'))
+
+@app.route("/profile/setup-2fa")
+@login_required
+def setup_2fa():
+    user = get_user_by_username(session['username'])
+    
+    if user['totp_enabled']:
+        flash('2FA is already enabled for your account.', 'info')
+        return redirect(url_for('profile'))
+    
+    # Generate new TOTP secret
+    totp_secret = generate_totp_secret()
+    session['temp_totp_secret'] = totp_secret
+    
+    # Generate QR code
+    qr_code = generate_qr_code(user['username'], totp_secret)
+    
+    return render_template('setup_2fa.html', 
+                         totp_secret=totp_secret, 
+                         qr_code=qr_code)
+
+@app.route("/profile/enable-2fa", methods=["POST"])
+@login_required
+def enable_2fa():
+    token = request.form.get('token', '').replace(' ', '')
+    totp_secret = session.get('temp_totp_secret')
+    
+    if not totp_secret:
+        flash('2FA setup session expired. Please try again.', 'error')
+        return redirect(url_for('setup_2fa'))
+    
+    # Verify the token
+    if not verify_totp(totp_secret, token):
+        flash('Invalid authentication code. Please try again.', 'error')
+        return redirect(url_for('setup_2fa'))
+    
+    # Generate backup codes
+    backup_codes = generate_backup_codes()
+    hashed_codes = [hash_backup_code(code) for code in backup_codes]
+    
+    # Enable 2FA
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("""
+        UPDATE users 
+        SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s 
+        WHERE username = %s
+    """, (totp_secret, json.dumps(hashed_codes), session['username']))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    session.pop('temp_totp_secret', None)
+    
+    log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully")
+    
+    # Show backup codes
+    return render_template('backup_codes.html', backup_codes=backup_codes)
+
+@app.route("/profile/disable-2fa", methods=["POST"])
+@login_required
+def disable_2fa():
+    password = request.form.get('password')
+    user = get_user_by_username(session['username'])
+    
+    # Verify password
+    if not verify_password(password, user['password_hash']):
+        flash('Incorrect password.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Disable 2FA
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("""
+        UPDATE users 
+        SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL 
+        WHERE username = %s
+    """, (session['username'],))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled")
+    flash('2FA has been disabled for your account.', 'success')
+    return redirect(url_for('profile'))
+
+@app.route("/heartbeat", methods=['POST'])
+@login_required
+def heartbeat():
+    """Endpoint für Session Keep-Alive - aktualisiert last_activity"""
+    # Aktualisiere last_activity nur wenn explizit angefordert
+    session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+    # Force session save
+    session.modified = True
+    
+    return jsonify({
+        'status': 'ok',
+        'last_activity': session['last_activity'],
+        'username': session.get('username')
+    })
+
+@app.route("/api/generate-license-key", methods=['POST'])
+@login_required
+def api_generate_key():
+    """API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
+    try:
+        # Lizenztyp aus Request holen (default: full)
+        data = request.get_json() or {}
+        license_type = data.get('type', 'full')
+        
+        # Key generieren
+        key = generate_license_key(license_type)
+        
+        # Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Wiederhole bis eindeutiger Key gefunden
+        attempts = 0
+        while attempts < 10:  # Max 10 Versuche
+            cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
+            if not cur.fetchone():
+                break  # Key ist eindeutig
+            key = generate_license_key(license_type)
+            attempts += 1
+        
+        cur.close()
+        conn.close()
+        
+        # Log für Audit
+        log_audit('GENERATE_KEY', 'license', 
+                 additional_info={'type': license_type, 'key': key})
+        
+        return jsonify({
+            'success': True,
+            'key': key,
+            'type': license_type
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Key-Generierung: {str(e)}")
+        return jsonify({
+            'success': False,
+            'error': 'Fehler bei der Key-Generierung'
+        }), 500
+
+@app.route("/api/customers", methods=['GET'])
+@login_required
+def api_customers():
+    """API Endpoint für die Kundensuche mit Select2"""
+    try:
+        # Suchparameter
+        search = request.args.get('q', '').strip()
+        page = request.args.get('page', 1, type=int)
+        per_page = 20
+        customer_id = request.args.get('id', type=int)
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Einzelnen Kunden per ID abrufen
+        if customer_id:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                WHERE c.id = %s
+                GROUP BY c.id, c.name, c.email
+            """, (customer_id,))
+            
+            customer = cur.fetchone()
+            results = []
+            if customer:
+                results.append({
+                    'id': customer[0],
+                    'text': f"{customer[1]} ({customer[2]})",
+                    'name': customer[1],
+                    'email': customer[2],
+                    'license_count': customer[3]
+                })
+            
+            cur.close()
+            conn.close()
+            
+            return jsonify({
+                'results': results,
+                'pagination': {'more': False}
+            })
+        
+        # SQL Query mit optionaler Suche
+        elif search:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                WHERE LOWER(c.name) LIKE LOWER(%s)
+                   OR LOWER(c.email) LIKE LOWER(%s)
+                GROUP BY c.id, c.name, c.email
+                ORDER BY c.name
+                LIMIT %s OFFSET %s
+            """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
+        else:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                GROUP BY c.id, c.name, c.email
+                ORDER BY c.name
+                LIMIT %s OFFSET %s
+            """, (per_page, (page - 1) * per_page))
+        
+        customers = cur.fetchall()
+        
+        # Format für Select2
+        results = []
+        for customer in customers:
+            results.append({
+                'id': customer[0],
+                'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
+                'name': customer[1],
+                'email': customer[2],
+                'license_count': customer[3]
+            })
+        
+        # Gesamtanzahl für Pagination
+        if search:
+            cur.execute("""
+                SELECT COUNT(*) FROM customers
+                WHERE LOWER(name) LIKE LOWER(%s)
+                   OR LOWER(email) LIKE LOWER(%s)
+            """, (f'%{search}%', f'%{search}%'))
+        else:
+            cur.execute("SELECT COUNT(*) FROM customers")
+        
+        total_count = cur.fetchone()[0]
+        
+        cur.close()
+        conn.close()
+        
+        # Select2 Response Format
+        return jsonify({
+            'results': results,
+            'pagination': {
+                'more': (page * per_page) < total_count
+            }
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Kundensuche: {str(e)}")
+        return jsonify({
+            'results': [],
+            'error': 'Fehler bei der Kundensuche'
+        }), 500
+
+@app.route("/")
+@login_required
+def dashboard():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Statistiken abrufen
+    # Gesamtanzahl Kunden (ohne Testdaten)
+    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE")
+    total_customers = cur.fetchone()[0]
+    
+    # Gesamtanzahl Lizenzen (ohne Testdaten)
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE")
+    total_licenses = cur.fetchone()[0]
+    
+    # Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE
+    """)
+    active_licenses = cur.fetchone()[0]
+    
+    # Aktive Sessions
+    cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
+    active_sessions_count = cur.fetchone()[0]
+    
+    # Abgelaufene Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until < CURRENT_DATE AND is_test = FALSE
+    """)
+    expired_licenses = cur.fetchone()[0]
+    
+    # Deaktivierte Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE is_active = FALSE AND is_test = FALSE
+    """)
+    inactive_licenses = cur.fetchone()[0]
+    
+    # Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until >= CURRENT_DATE 
+        AND valid_until < CURRENT_DATE + INTERVAL '30 days'
+        AND is_active = TRUE
+        AND is_test = FALSE
+    """)
+    expiring_soon = cur.fetchone()[0]
+    
+    # Testlizenzen vs Vollversionen (ohne Testdaten)
+    cur.execute("""
+        SELECT license_type, COUNT(*) 
+        FROM licenses 
+        WHERE is_test = FALSE
+        GROUP BY license_type
+    """)
+    license_types = dict(cur.fetchall())
+    
+    # Anzahl Testdaten
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE")
+    test_data_count = cur.fetchone()[0]
+    
+    # Anzahl Test-Kunden
+    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE")
+    test_customers_count = cur.fetchone()[0]
+    
+    # Anzahl Test-Ressourcen
+    cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE")
+    test_resources_count = cur.fetchone()[0]
+    
+    # Letzte 5 erstellten Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, l.valid_until,
+               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 as status
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.is_test = FALSE
+        ORDER BY l.id DESC
+        LIMIT 5
+    """)
+    recent_licenses = cur.fetchall()
+    
+    # Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten)
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, l.valid_until,
+               l.valid_until - CURRENT_DATE as days_left
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.valid_until >= CURRENT_DATE 
+        AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
+        AND l.is_active = TRUE
+        AND l.is_test = FALSE
+        ORDER BY l.valid_until
+        LIMIT 10
+    """)
+    expiring_licenses = cur.fetchall()
+    
+    # Letztes Backup
+    cur.execute("""
+        SELECT created_at, filesize, duration_seconds, backup_type, status
+        FROM backup_history 
+        ORDER BY created_at DESC 
+        LIMIT 1
+    """)
+    last_backup_info = cur.fetchone()
+    
+    # Sicherheitsstatistiken
+    # Gesperrte IPs
+    cur.execute("""
+        SELECT COUNT(*) FROM login_attempts 
+        WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP
+    """)
+    blocked_ips_count = cur.fetchone()[0]
+    
+    # Fehlversuche heute
+    cur.execute("""
+        SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts
+        WHERE last_attempt::date = CURRENT_DATE
+    """)
+    failed_attempts_today = cur.fetchone()[0]
+    
+    # Letzte 5 Sicherheitsereignisse
+    cur.execute("""
+        SELECT 
+            la.ip_address, 
+            la.attempt_count, 
+            la.last_attempt, 
+            la.blocked_until,
+            la.last_username_tried,
+            la.last_error_message
+        FROM login_attempts la
+        ORDER BY la.last_attempt DESC
+        LIMIT 5
+    """)
+    recent_security_events = []
+    for event in cur.fetchall():
+        recent_security_events.append({
+            'ip_address': event[0],
+            'attempt_count': event[1],
+            'last_attempt': event[2].strftime('%d.%m %H:%M'),
+            'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) else None,
+            'username_tried': event[4],
+            'error_message': event[5]
+        })
+    
+    # Sicherheitslevel berechnen
+    if blocked_ips_count > 5 or failed_attempts_today > 50:
+        security_level = 'danger'
+        security_level_text = 'KRITISCH'
+    elif blocked_ips_count > 2 or failed_attempts_today > 20:
+        security_level = 'warning'
+        security_level_text = 'ERHÖHT'
+    else:
+        security_level = 'success'
+        security_level_text = 'NORMAL'
+    
+    # Resource Pool Statistiken (nur Live-Daten, keine Testdaten)
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'available') as available,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+            COUNT(*) as total
+        FROM resource_pools
+        WHERE is_test = FALSE
+        GROUP BY resource_type
+    """)
+    
+    resource_stats = {}
+    resource_warning = None
+    
+    for row in cur.fetchall():
+        available_percent = round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
+        resource_stats[row[0]] = {
+            'available': row[1],
+            'allocated': row[2],
+            'quarantine': row[3],
+            'total': row[4],
+            'available_percent': available_percent,
+            'warning_level': 'danger' if row[1] < 50 else 'warning' if row[1] < 100 else 'success'
+        }
+        
+        # Warnung bei niedrigem Bestand
+        if row[1] < 50:
+            if not resource_warning:
+                resource_warning = f"Niedriger Bestand bei {row[0].upper()}: nur noch {row[1]} verfügbar!"
+            else:
+                resource_warning += f" | {row[0].upper()}: {row[1]}"
+    
+    cur.close()
+    conn.close()
+    
+    stats = {
+        'total_customers': total_customers,
+        'total_licenses': total_licenses,
+        'active_licenses': active_licenses,
+        'expired_licenses': expired_licenses,
+        'inactive_licenses': inactive_licenses,
+        'expiring_soon': expiring_soon,
+        'full_licenses': license_types.get('full', 0),
+        'test_licenses': license_types.get('test', 0),
+        'test_data_count': test_data_count,
+        'test_customers_count': test_customers_count,
+        'test_resources_count': test_resources_count,
+        'recent_licenses': recent_licenses,
+        'expiring_licenses': expiring_licenses,
+        'active_sessions': active_sessions_count,
+        'last_backup': last_backup_info,
+        # Sicherheitsstatistiken
+        'blocked_ips_count': blocked_ips_count,
+        'failed_attempts_today': failed_attempts_today,
+        'recent_security_events': recent_security_events,
+        'security_level': security_level,
+        'security_level_text': security_level_text,
+        'resource_stats': resource_stats
+    }
+    
+    return render_template("dashboard.html", 
+                         stats=stats, 
+                         resource_stats=resource_stats,
+                         resource_warning=resource_warning,
+                         username=session.get('username'))
+
+@app.route("/create", methods=["GET", "POST"])
+@login_required
+def create_license():
+    if request.method == "POST":
+        customer_id = request.form.get("customer_id")
+        license_key = request.form["license_key"].upper()  # Immer Großbuchstaben
+        license_type = request.form["license_type"]
+        valid_from = request.form["valid_from"]
+        is_test = request.form.get("is_test") == "on"  # Checkbox value
+        
+        # Berechne valid_until basierend auf Laufzeit
+        duration = int(request.form.get("duration", 1))
+        duration_type = request.form.get("duration_type", "years")
+        
+        from datetime import datetime, timedelta
+        from dateutil.relativedelta import relativedelta
+        
+        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
+        
+        if duration_type == "days":
+            end_date = start_date + timedelta(days=duration)
+        elif duration_type == "months":
+            end_date = start_date + relativedelta(months=duration)
+        else:  # years
+            end_date = start_date + relativedelta(years=duration)
+        
+        # Ein Tag abziehen, da der Starttag mitgezählt wird
+        end_date = end_date - timedelta(days=1)
+        valid_until = end_date.strftime("%Y-%m-%d")
+
+        # Validiere License Key Format
+        if not validate_license_key(license_key):
+            flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error')
+            return redirect(url_for('create_license'))
+        
+        # Resource counts
+        domain_count = int(request.form.get("domain_count", 1))
+        ipv4_count = int(request.form.get("ipv4_count", 1))
+        phone_count = int(request.form.get("phone_count", 1))
+        device_limit = int(request.form.get("device_limit", 3))
+
+        conn = get_connection()
+        cur = conn.cursor()
+
+        try:
+            # Prüfe ob neuer Kunde oder bestehender
+            if customer_id == "new":
+                # Neuer Kunde
+                name = request.form.get("customer_name")
+                email = request.form.get("email")
+                
+                if not name:
+                    flash('Kundenname ist erforderlich!', 'error')
+                    return redirect(url_for('create_license'))
+                
+                # Prüfe ob E-Mail bereits existiert
+                if email:
+                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
+                    existing = cur.fetchone()
+                    if existing:
+                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
+                        return redirect(url_for('create_license'))
+                
+                # Kunde einfügen (erbt Test-Status von Lizenz)
+                cur.execute("""
+                    INSERT INTO customers (name, email, is_test, created_at)
+                    VALUES (%s, %s, %s, NOW())
+                    RETURNING id
+                """, (name, email, is_test))
+                customer_id = cur.fetchone()[0]
+                customer_info = {'name': name, 'email': email, 'is_test': is_test}
+                
+                # Audit-Log für neuen Kunden
+                log_audit('CREATE', 'customer', customer_id, 
+                         new_values={'name': name, 'email': email, 'is_test': is_test})
+            else:
+                # Bestehender Kunde - hole Infos für Audit-Log
+                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+                customer_data = cur.fetchone()
+                if not customer_data:
+                    flash('Kunde nicht gefunden!', 'error')
+                    return redirect(url_for('create_license'))
+                customer_info = {'name': customer_data[0], 'email': customer_data[1]}
+                
+                # Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
+                if customer_data[2]:  # is_test des Kunden
+                    is_test = True
+
+            # Lizenz hinzufügen
+            cur.execute("""
+                INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active, 
+                                    domain_count, ipv4_count, phone_count, device_limit, is_test)
+                VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
+                RETURNING id
+            """, (license_key, customer_id, license_type, valid_from, valid_until, 
+                  domain_count, ipv4_count, phone_count, device_limit, is_test))
+            license_id = cur.fetchone()[0]
+            
+            # Ressourcen zuweisen
+            try:
+                # Prüfe Verfügbarkeit
+                cur.execute("""
+                    SELECT 
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
+                """, (is_test, is_test, is_test))
+                available = cur.fetchone()
+                
+                if available[0] < domain_count:
+                    raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})")
+                if available[1] < ipv4_count:
+                    raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})")
+                if available[2] < phone_count:
+                    raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})")
+                
+                # Domains zuweisen
+                if domain_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, domain_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # IPv4s zuweisen
+                if ipv4_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, ipv4_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # Telefonnummern zuweisen
+                if phone_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, phone_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+            except ValueError as e:
+                conn.rollback()
+                flash(str(e), 'error')
+                return redirect(url_for('create_license'))
+
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE', 'license', license_id, 
+                     new_values={
+                         'license_key': license_key,
+                         'customer_name': customer_info['name'],
+                         'customer_email': customer_info['email'],
+                         'license_type': license_type,
+                         'valid_from': valid_from,
+                         'valid_until': valid_until,
+                         'device_limit': device_limit,
+                         'is_test': is_test
+                     })
+            
+            flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
+            
+        except Exception as e:
+            conn.rollback()
+            logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}")
+            flash('Fehler beim Erstellen der Lizenz!', 'error')
+        finally:
+            cur.close()
+            conn.close()
+
+        # Preserve show_test parameter if present
+        redirect_url = "/create"
+        if request.args.get('show_test') == 'true':
+            redirect_url += "?show_test=true"
+        return redirect(redirect_url)
+
+    # Unterstützung für vorausgewählten Kunden
+    preselected_customer_id = request.args.get('customer_id', type=int)
+    return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
+
+@app.route("/batch", methods=["GET", "POST"])
+@login_required
+def batch_licenses():
+    """Batch-Generierung mehrerer Lizenzen für einen Kunden"""
+    if request.method == "POST":
+        # Formulardaten
+        customer_id = request.form.get("customer_id")
+        license_type = request.form["license_type"]
+        quantity = int(request.form["quantity"])
+        valid_from = request.form["valid_from"]
+        is_test = request.form.get("is_test") == "on"  # Checkbox value
+        
+        # Berechne valid_until basierend auf Laufzeit
+        duration = int(request.form.get("duration", 1))
+        duration_type = request.form.get("duration_type", "years")
+        
+        from datetime import datetime, timedelta
+        from dateutil.relativedelta import relativedelta
+        
+        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
+        
+        if duration_type == "days":
+            end_date = start_date + timedelta(days=duration)
+        elif duration_type == "months":
+            end_date = start_date + relativedelta(months=duration)
+        else:  # years
+            end_date = start_date + relativedelta(years=duration)
+        
+        # Ein Tag abziehen, da der Starttag mitgezählt wird
+        end_date = end_date - timedelta(days=1)
+        valid_until = end_date.strftime("%Y-%m-%d")
+        
+        # Resource counts
+        domain_count = int(request.form.get("domain_count", 1))
+        ipv4_count = int(request.form.get("ipv4_count", 1))
+        phone_count = int(request.form.get("phone_count", 1))
+        device_limit = int(request.form.get("device_limit", 3))
+        
+        # Sicherheitslimit
+        if quantity < 1 or quantity > 100:
+            flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
+            return redirect(url_for('batch_licenses'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        try:
+            # Prüfe ob neuer Kunde oder bestehender
+            if customer_id == "new":
+                # Neuer Kunde
+                name = request.form.get("customer_name")
+                email = request.form.get("email")
+                
+                if not name:
+                    flash('Kundenname ist erforderlich!', 'error')
+                    return redirect(url_for('batch_licenses'))
+                
+                # Prüfe ob E-Mail bereits existiert
+                if email:
+                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
+                    existing = cur.fetchone()
+                    if existing:
+                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
+                        return redirect(url_for('batch_licenses'))
+                
+                # Kunde einfügen (erbt Test-Status von Lizenz)
+                cur.execute("""
+                    INSERT INTO customers (name, email, is_test, created_at)
+                    VALUES (%s, %s, %s, NOW())
+                    RETURNING id
+                """, (name, email, is_test))
+                customer_id = cur.fetchone()[0]
+                
+                # Audit-Log für neuen Kunden
+                log_audit('CREATE', 'customer', customer_id, 
+                         new_values={'name': name, 'email': email, 'is_test': is_test})
+            else:
+                # Bestehender Kunde - hole Infos
+                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+                customer_data = cur.fetchone()
+                if not customer_data:
+                    flash('Kunde nicht gefunden!', 'error')
+                    return redirect(url_for('batch_licenses'))
+                name = customer_data[0]
+                email = customer_data[1]
+                
+                # Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren
+                if customer_data[2]:  # is_test des Kunden
+                    is_test = True
+            
+            # Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
+            total_domains_needed = domain_count * quantity
+            total_ipv4s_needed = ipv4_count * quantity
+            total_phones_needed = phone_count * quantity
+            
+            cur.execute("""
+                SELECT 
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
+            """, (is_test, is_test, is_test))
+            available = cur.fetchone()
+            
+            if available[0] < total_domains_needed:
+                flash(f"Nicht genügend Domains verfügbar (benötigt: {total_domains_needed}, verfügbar: {available[0]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            if available[1] < total_ipv4s_needed:
+                flash(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {total_ipv4s_needed}, verfügbar: {available[1]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            if available[2] < total_phones_needed:
+                flash(f"Nicht genügend Telefonnummern verfügbar (benötigt: {total_phones_needed}, verfügbar: {available[2]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            
+            # Lizenzen generieren und speichern
+            generated_licenses = []
+            for i in range(quantity):
+                # Eindeutigen Key generieren
+                attempts = 0
+                while attempts < 10:
+                    license_key = generate_license_key(license_type)
+                    cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
+                    if not cur.fetchone():
+                        break
+                    attempts += 1
+                
+                # Lizenz einfügen
+                cur.execute("""
+                    INSERT INTO licenses (license_key, customer_id, license_type, is_test, 
+                                        valid_from, valid_until, is_active,
+                                        domain_count, ipv4_count, phone_count, device_limit)
+                    VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
+                    RETURNING id
+                """, (license_key, customer_id, license_type, is_test, valid_from, valid_until,
+                      domain_count, ipv4_count, phone_count, device_limit))
+                license_id = cur.fetchone()[0]
+                
+                # Ressourcen für diese Lizenz zuweisen
+                # Domains
+                if domain_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, domain_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # IPv4s
+                if ipv4_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, ipv4_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # Telefonnummern
+                if phone_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, phone_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                generated_licenses.append({
+                    'id': license_id,
+                    'key': license_key,
+                    'type': license_type
+                })
+            
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE_BATCH', 'license', 
+                     new_values={'customer': name, 'quantity': quantity, 'type': license_type},
+                     additional_info=f"Batch-Generierung von {quantity} Lizenzen")
+            
+            # Session für Export speichern
+            session['batch_export'] = {
+                'customer': name,
+                'email': email,
+                'licenses': generated_licenses,
+                'valid_from': valid_from,
+                'valid_until': valid_until,
+                'timestamp': datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+            }
+            
+            flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
+            return render_template("batch_result.html", 
+                                 customer=name,
+                                 email=email,
+                                 licenses=generated_licenses,
+                                 valid_from=valid_from,
+                                 valid_until=valid_until)
+            
+        except Exception as e:
+            conn.rollback()
+            logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
+            flash('Fehler bei der Batch-Generierung!', 'error')
+            return redirect(url_for('batch_licenses'))
+        finally:
+            cur.close()
+            conn.close()
+    
+    # GET Request
+    return render_template("batch_form.html")
+
+@app.route("/batch/export")
+@login_required
+def export_batch():
+    """Exportiert die zuletzt generierten Batch-Lizenzen"""
+    batch_data = session.get('batch_export')
+    if not batch_data:
+        flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
+        return redirect(url_for('batch_licenses'))
+    
+    # CSV generieren
+    output = io.StringIO()
+    output.write('\ufeff')  # UTF-8 BOM für Excel
+    
+    # Header
+    output.write(f"Kunde: {batch_data['customer']}\n")
+    output.write(f"E-Mail: {batch_data['email']}\n")
+    output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
+    output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
+    output.write("\n")
+    output.write("Nr;Lizenzschlüssel;Typ\n")
+    
+    # Lizenzen
+    for i, license in enumerate(batch_data['licenses'], 1):
+        typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
+        output.write(f"{i};{license['key']};{typ_text}\n")
+    
+    output.seek(0)
+    
+    # Audit-Log
+    log_audit('EXPORT', 'batch_licenses', 
+             additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
+    
+    return send_file(
+        io.BytesIO(output.getvalue().encode('utf-8-sig')),
+        mimetype='text/csv',
+        as_attachment=True,
+        download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv"
+    )
+
+@app.route("/licenses")
+@login_required
+def licenses():
+    # Redirect zur kombinierten Ansicht
+    return redirect("/customers-licenses")
+
+@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
+@login_required
+def edit_license(license_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if request.method == "POST":
+        # Alte Werte für Audit-Log abrufen
+        cur.execute("""
+            SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit 
+            FROM licenses WHERE id = %s
+        """, (license_id,))
+        old_license = cur.fetchone()
+        
+        # Update license
+        license_key = request.form["license_key"]
+        license_type = request.form["license_type"]
+        valid_from = request.form["valid_from"]
+        valid_until = request.form["valid_until"]
+        is_active = request.form.get("is_active") == "on"
+        is_test = request.form.get("is_test") == "on"
+        device_limit = int(request.form.get("device_limit", 3))
+        
+        cur.execute("""
+            UPDATE licenses 
+            SET license_key = %s, license_type = %s, valid_from = %s, 
+                valid_until = %s, is_active = %s, is_test = %s, device_limit = %s
+            WHERE id = %s
+        """, (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('UPDATE', 'license', license_id,
+                 old_values={
+                     'license_key': old_license[0],
+                     'license_type': old_license[1],
+                     'valid_from': str(old_license[2]),
+                     'valid_until': str(old_license[3]),
+                     'is_active': old_license[4],
+                     'is_test': old_license[5],
+                     'device_limit': old_license[6]
+                 },
+                 new_values={
+                     'license_key': license_key,
+                     'license_type': license_type,
+                     'valid_from': valid_from,
+                     'valid_until': valid_until,
+                     'is_active': is_active,
+                     'is_test': is_test,
+                     'device_limit': device_limit
+                 })
+        
+        cur.close()
+        conn.close()
+        
+        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
+        redirect_url = "/customers-licenses"
+        
+        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
+        show_test = request.form.get('show_test') or request.args.get('show_test')
+        if show_test == 'true':
+            redirect_url += "?show_test=true"
+            
+        # Behalte customer_id bei wenn vorhanden
+        if request.referrer and 'customer_id=' in request.referrer:
+            import re
+            match = re.search(r'customer_id=(\d+)', request.referrer)
+            if match:
+                connector = "&" if "?" in redirect_url else "?"
+                redirect_url += f"{connector}customer_id={match.group(1)}"
+                
+        return redirect(redirect_url)
+    
+    # Get license data
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, c.email, l.license_type, 
+               l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.id = %s
+    """, (license_id,))
+    
+    license = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    if not license:
+        return redirect("/licenses")
+    
+    return render_template("edit_license.html", license=license, username=session.get('username'))
+
+@app.route("/license/delete/<int:license_id>", methods=["POST"])
+@login_required
+def delete_license(license_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Lizenzdetails für Audit-Log abrufen
+    cur.execute("""
+        SELECT l.license_key, c.name, l.license_type 
+        FROM licenses l 
+        JOIN customers c ON l.customer_id = c.id 
+        WHERE l.id = %s
+    """, (license_id,))
+    license_info = cur.fetchone()
+    
+    cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
+    
+    conn.commit()
+    
+    # Audit-Log
+    if license_info:
+        log_audit('DELETE', 'license', license_id,
+                 old_values={
+                     'license_key': license_info[0],
+                     'customer_name': license_info[1],
+                     'license_type': license_info[2]
+                 })
+    
+    cur.close()
+    conn.close()
+    
+    return redirect("/licenses")
+
+@app.route("/customers")
+@login_required
+def customers():
+    # Redirect zur kombinierten Ansicht
+    return redirect("/customers-licenses")
+
+@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
+@login_required
+def edit_customer(customer_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if request.method == "POST":
+        # Alte Werte für Audit-Log abrufen
+        cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+        old_customer = cur.fetchone()
+        
+        # Update customer
+        name = request.form["name"]
+        email = request.form["email"]
+        is_test = request.form.get("is_test") == "on"
+        
+        cur.execute("""
+            UPDATE customers 
+            SET name = %s, email = %s, is_test = %s
+            WHERE id = %s
+        """, (name, email, is_test, customer_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('UPDATE', 'customer', customer_id,
+                 old_values={
+                     'name': old_customer[0],
+                     'email': old_customer[1],
+                     'is_test': old_customer[2]
+                 },
+                 new_values={
+                     'name': name,
+                     'email': email,
+                     'is_test': is_test
+                 })
+        
+        cur.close()
+        conn.close()
+        
+        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
+        redirect_url = "/customers-licenses"
+        
+        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
+        show_test = request.form.get('show_test') or request.args.get('show_test')
+        if show_test == 'true':
+            redirect_url += "?show_test=true"
+            
+        # Behalte customer_id bei (immer der aktuelle Kunde)
+        connector = "&" if "?" in redirect_url else "?"
+        redirect_url += f"{connector}customer_id={customer_id}"
+                
+        return redirect(redirect_url)
+    
+    # Get customer data with licenses
+    cur.execute("""
+        SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s
+    """, (customer_id,))
+    
+    customer = cur.fetchone()
+    if not customer:
+        cur.close()
+        conn.close()
+        return "Kunde nicht gefunden", 404
+    
+    
+    # Get customer's licenses
+    cur.execute("""
+        SELECT id, license_key, license_type, valid_from, valid_until, is_active
+        FROM licenses
+        WHERE customer_id = %s
+        ORDER BY valid_until DESC
+    """, (customer_id,))
+    
+    licenses = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    if not customer:
+        return redirect("/customers-licenses")
+    
+    return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
+
+@app.route("/customer/create", methods=["GET", "POST"])
+@login_required
+def create_customer():
+    """Erstellt einen neuen Kunden ohne Lizenz"""
+    if request.method == "POST":
+        name = request.form.get('name')
+        email = request.form.get('email')
+        is_test = request.form.get('is_test') == 'on'
+        
+        if not name or not email:
+            flash("Name und E-Mail sind Pflichtfelder!", "error")
+            return render_template("create_customer.html", username=session.get('username'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        try:
+            # Prüfen ob E-Mail bereits existiert
+            cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
+            existing = cur.fetchone()
+            if existing:
+                flash(f"Ein Kunde mit der E-Mail '{email}' existiert bereits: {existing[1]}", "error")
+                return render_template("create_customer.html", username=session.get('username'))
+            
+            # Kunde erstellen
+            cur.execute("""
+                INSERT INTO customers (name, email, created_at, is_test) 
+                VALUES (%s, %s, %s, %s) RETURNING id
+            """, (name, email, datetime.now(), is_test))
+            
+            customer_id = cur.fetchone()[0]
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE', 'customer', customer_id,
+                     new_values={
+                         'name': name,
+                         'email': email,
+                         'is_test': is_test
+                     })
+            
+            flash(f"Kunde '{name}' wurde erfolgreich angelegt!", "success")
+            return redirect(f"/customer/edit/{customer_id}")
+            
+        except Exception as e:
+            conn.rollback()
+            flash(f"Fehler beim Anlegen des Kunden: {str(e)}", "error")
+            return render_template("create_customer.html", username=session.get('username'))
+        finally:
+            cur.close()
+            conn.close()
+    
+    # GET Request - Formular anzeigen
+    return render_template("create_customer.html", username=session.get('username'))
+
+@app.route("/customer/delete/<int:customer_id>", methods=["POST"])
+@login_required
+def delete_customer(customer_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Prüfen ob Kunde Lizenzen hat
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
+    license_count = cur.fetchone()[0]
+    
+    if license_count > 0:
+        # Kunde hat Lizenzen - nicht löschen
+        cur.close()
+        conn.close()
+        return redirect("/customers")
+    
+    # Kundendetails für Audit-Log abrufen
+    cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
+    customer_info = cur.fetchone()
+    
+    # Kunde löschen wenn keine Lizenzen vorhanden
+    cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
+    
+    conn.commit()
+    
+    # Audit-Log
+    if customer_info:
+        log_audit('DELETE', 'customer', customer_id,
+                 old_values={
+                     'name': customer_info[0],
+                     'email': customer_info[1]
+                 })
+    
+    cur.close()
+    conn.close()
+    
+    return redirect("/customers")
+
+@app.route("/customers-licenses")
+@login_required
+def customers_licenses():
+    """Kombinierte Ansicht für Kunden und deren Lizenzen"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    query = """
+        SELECT 
+            c.id,
+            c.name,
+            c.email,
+            c.created_at,
+            COUNT(l.id) as total_licenses,
+            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
+        FROM customers c
+        LEFT JOIN licenses l ON c.id = l.customer_id
+    """
+    
+    if not show_test:
+        query += " WHERE c.is_test = FALSE"
+    
+    query += """
+        GROUP BY c.id, c.name, c.email, c.created_at
+        ORDER BY c.name
+    """
+    
+    cur.execute(query)
+    customers = cur.fetchall()
+    
+    # Hole ausgewählten Kunden nur wenn explizit in URL angegeben
+    selected_customer_id = request.args.get('customer_id', type=int)
+    licenses = []
+    selected_customer = None
+    
+    if customers and selected_customer_id:
+        # Hole Daten des ausgewählten Kunden
+        for customer in customers:
+            if customer[0] == selected_customer_id:
+                selected_customer = customer
+                break
+        
+        # Hole Lizenzen des ausgewählten Kunden
+        if selected_customer:
+            cur.execute("""
+                SELECT 
+                    l.id,
+                    l.license_key,
+                    l.license_type,
+                    l.valid_from,
+                    l.valid_until,
+                    l.is_active,
+                    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 as status,
+                    l.domain_count,
+                    l.ipv4_count,
+                    l.phone_count,
+                    l.device_limit,
+                    (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
+                    -- Actual resource counts
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
+                FROM licenses l
+                WHERE l.customer_id = %s
+                ORDER BY l.created_at DESC, l.id DESC
+            """, (selected_customer_id,))
+            licenses = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("customers_licenses.html",
+                         customers=customers,
+                         selected_customer=selected_customer,
+                         selected_customer_id=selected_customer_id,
+                         licenses=licenses,
+                         show_test=show_test)
+
+@app.route("/api/customer/<int:customer_id>/licenses")
+@login_required
+def api_customer_licenses(customer_id):
+    """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole Lizenzen des Kunden
+    cur.execute("""
+        SELECT 
+            l.id,
+            l.license_key,
+            l.license_type,
+            l.valid_from,
+            l.valid_until,
+            l.is_active,
+            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 as status,
+            l.domain_count,
+            l.ipv4_count,
+            l.phone_count,
+            l.device_limit,
+            (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
+            -- Actual resource counts
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
+        FROM licenses l
+        WHERE l.customer_id = %s
+        ORDER BY l.created_at DESC, l.id DESC
+    """, (customer_id,))
+    
+    licenses = []
+    for row in cur.fetchall():
+        license_id = row[0]
+        
+        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
+        cur.execute("""
+            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
+            FROM resource_pools rp
+            JOIN license_resources lr ON rp.id = lr.resource_id
+            WHERE lr.license_id = %s AND lr.is_active = true
+            ORDER BY rp.resource_type, rp.resource_value
+        """, (license_id,))
+        
+        resources = {
+            'domains': [],
+            'ipv4s': [],
+            'phones': []
+        }
+        
+        for res_row in cur.fetchall():
+            resource_info = {
+                'id': res_row[0],
+                'value': res_row[2],
+                'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else ''
+            }
+            
+            if res_row[1] == 'domain':
+                resources['domains'].append(resource_info)
+            elif res_row[1] == 'ipv4':
+                resources['ipv4s'].append(resource_info)
+            elif res_row[1] == 'phone':
+                resources['phones'].append(resource_info)
+        
+        licenses.append({
+            'id': row[0],
+            'license_key': row[1],
+            'license_type': row[2],
+            'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '',
+            'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '',
+            'is_active': row[5],
+            'status': row[6],
+            'domain_count': row[7],  # limit
+            'ipv4_count': row[8],     # limit
+            'phone_count': row[9],    # limit
+            'device_limit': row[10],
+            'active_devices': row[11],
+            'actual_domain_count': row[12],  # actual count
+            'actual_ipv4_count': row[13],    # actual count
+            'actual_phone_count': row[14],   # actual count
+            'resources': resources
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'success': True,
+        'licenses': licenses,
+        'count': len(licenses)
+    })
+
+@app.route("/api/customer/<int:customer_id>/quick-stats")
+@login_required
+def api_customer_quick_stats(customer_id):
+    """API-Endpoint für Schnellstatistiken eines Kunden"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole Kundenstatistiken
+    cur.execute("""
+        SELECT 
+            COUNT(l.id) as total_licenses,
+            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon
+        FROM licenses l
+        WHERE l.customer_id = %s
+    """, (customer_id,))
+    
+    stats = cur.fetchone()
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'success': True,
+        'stats': {
+            'total': stats[0],
+            'active': stats[1],
+            'expired': stats[2],
+            'expiring_soon': stats[3]
+        }
+    })
+
+@app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
+@login_required
+def api_license_quick_edit(license_id):
+    """API-Endpoint für schnelle Lizenz-Bearbeitung"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        data = request.get_json()
+        
+        # Hole alte Werte für Audit-Log
+        cur.execute("""
+            SELECT is_active, valid_until, license_type 
+            FROM licenses WHERE id = %s
+        """, (license_id,))
+        old_values = cur.fetchone()
+        
+        if not old_values:
+            return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404
+        
+        # Update-Felder vorbereiten
+        updates = []
+        params = []
+        new_values = {}
+        
+        if 'is_active' in data:
+            updates.append("is_active = %s")
+            params.append(data['is_active'])
+            new_values['is_active'] = data['is_active']
+        
+        if 'valid_until' in data:
+            updates.append("valid_until = %s")
+            params.append(data['valid_until'])
+            new_values['valid_until'] = data['valid_until']
+        
+        if 'license_type' in data:
+            updates.append("license_type = %s")
+            params.append(data['license_type'])
+            new_values['license_type'] = data['license_type']
+        
+        if updates:
+            params.append(license_id)
+            cur.execute(f"""
+                UPDATE licenses 
+                SET {', '.join(updates)}
+                WHERE id = %s
+            """, params)
+            
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('UPDATE', 'license', license_id,
+                     old_values={
+                         'is_active': old_values[0],
+                         'valid_until': old_values[1].isoformat() if old_values[1] else None,
+                         'license_type': old_values[2]
+                     },
+                     new_values=new_values)
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True})
+    
+    except Exception as e:
+        conn.rollback()
+        cur.close()
+        conn.close()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route("/api/license/<int:license_id>/resources")
+@login_required
+def api_license_resources(license_id):
+    """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
+        cur.execute("""
+            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
+            FROM resource_pools rp
+            JOIN license_resources lr ON rp.id = lr.resource_id
+            WHERE lr.license_id = %s AND lr.is_active = true
+            ORDER BY rp.resource_type, rp.resource_value
+        """, (license_id,))
+        
+        resources = {
+            'domains': [],
+            'ipv4s': [],
+            'phones': []
+        }
+        
+        for row in cur.fetchall():
+            resource_info = {
+                'id': row[0],
+                'value': row[2],
+                'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else ''
+            }
+            
+            if row[1] == 'domain':
+                resources['domains'].append(resource_info)
+            elif row[1] == 'ipv4':
+                resources['ipv4s'].append(resource_info)
+            elif row[1] == 'phone':
+                resources['phones'].append(resource_info)
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'resources': resources
+        })
+        
+    except Exception as e:
+        cur.close()
+        conn.close()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route("/sessions")
+@login_required
+def sessions():
+    conn = get_connection()
+    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
+    cur.execute(f"""
+        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
+               s.user_agent, s.started_at, s.last_heartbeat,
+               EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
+        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 {active_sort_fields[active_sort]} {active_order.upper()}
+    """)
+    active_sessions = cur.fetchall()
+    
+    # Inaktive Sessions der letzten 24 Stunden
+    cur.execute(f"""
+        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
+               s.started_at, s.ended_at, 
+               EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
+        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 > NOW() - INTERVAL '24 hours'
+        ORDER BY {ended_sort_fields[ended_sort]} {ended_order.upper()}
+        LIMIT 50
+    """)
+    recent_sessions = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("sessions.html", 
+                         active_sessions=active_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'))
+
+@app.route("/session/end/<int:session_id>", methods=["POST"])
+@login_required
+def end_session(session_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Session beenden
+    cur.execute("""
+        UPDATE sessions 
+        SET is_active = FALSE, ended_at = NOW()
+        WHERE id = %s AND is_active = TRUE
+    """, (session_id,))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    return redirect("/sessions")
+
+@app.route("/export/licenses")
+@login_required
+def export_licenses():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
+    include_test = request.args.get('include_test', 'false').lower() == 'true'
+    customer_id = request.args.get('customer_id', type=int)
+    
+    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.is_test,
+               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 as status
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+    """
+    
+    # Build WHERE clause
+    where_conditions = []
+    params = []
+    
+    if not include_test:
+        where_conditions.append("l.is_test = FALSE")
+    
+    if customer_id:
+        where_conditions.append("l.customer_id = %s")
+        params.append(customer_id)
+    
+    if where_conditions:
+        query += " WHERE " + " AND ".join(where_conditions)
+    
+    query += " ORDER BY l.id"
+    
+    cur.execute(query, params)
+    
+    # Spaltennamen
+    columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 
+               'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status']
+    
+    # Daten in DataFrame
+    data = cur.fetchall()
+    df = pd.DataFrame(data, columns=columns)
+    
+    # Datumsformatierung
+    df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y')
+    df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y')
+    
+    # Typ und Aktiv Status anpassen
+    df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
+    df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
+    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
+    
+    cur.close()
+    conn.close()
+    
+    # Export Format
+    export_format = request.args.get('format', 'excel')
+    
+    # Audit-Log
+    log_audit('EXPORT', 'license', 
+             additional_info=f"Export aller Lizenzen als {export_format.upper()}")
+    filename = f'lizenzen_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
+    
+    if export_format == 'csv':
+        # CSV Export
+        output = io.StringIO()
+        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
+        output.seek(0)
+        
+        return send_file(
+            io.BytesIO(output.getvalue().encode('utf-8-sig')),
+            mimetype='text/csv',
+            as_attachment=True,
+            download_name=f'{filename}.csv'
+        )
+    else:
+        # Excel Export
+        output = io.BytesIO()
+        with pd.ExcelWriter(output, engine='openpyxl') as writer:
+            df.to_excel(writer, sheet_name='Lizenzen', index=False)
+            
+            # Formatierung
+            worksheet = writer.sheets['Lizenzen']
+            for column in worksheet.columns:
+                max_length = 0
+                column_letter = column[0].column_letter
+                for cell in column:
+                    try:
+                        if len(str(cell.value)) > max_length:
+                            max_length = len(str(cell.value))
+                    except:
+                        pass
+                adjusted_width = min(max_length + 2, 50)
+                worksheet.column_dimensions[column_letter].width = adjusted_width
+        
+        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/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():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Check if test data should be included
+    include_test = request.args.get('include_test', 'false').lower() == 'true'
+    
+    # Build query based on test data filter
+    if include_test:
+        # Include all customers
+        query = """
+            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
+                   COUNT(l.id) as total_licenses,
+                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
+            FROM customers c
+            LEFT JOIN licenses l ON c.id = l.customer_id
+            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
+            ORDER BY c.id
+        """
+    else:
+        # Exclude test customers and test licenses
+        query = """
+            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
+                   COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
+                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
+                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
+            FROM customers c
+            LEFT JOIN licenses l ON c.id = l.customer_id
+            WHERE c.is_test = FALSE
+            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
+            ORDER BY c.id
+        """
+    
+    cur.execute(query)
+    
+    # Spaltennamen
+    columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten',
+               'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
+    
+    # Daten in DataFrame
+    data = cur.fetchall()
+    df = pd.DataFrame(data, columns=columns)
+    
+    # Datumsformatierung
+    df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
+    
+    # Testdaten formatting
+    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
+    
+    cur.close()
+    conn.close()
+    
+    # Export Format
+    export_format = request.args.get('format', 'excel')
+    
+    # Audit-Log
+    log_audit('EXPORT', 'customer', 
+             additional_info=f"Export aller Kunden als {export_format.upper()}")
+    filename = f'kunden_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
+    
+    if export_format == 'csv':
+        # CSV Export
+        output = io.StringIO()
+        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
+        output.seek(0)
+        
+        return send_file(
+            io.BytesIO(output.getvalue().encode('utf-8-sig')),
+            mimetype='text/csv',
+            as_attachment=True,
+            download_name=f'{filename}.csv'
+        )
+    else:
+        # Excel Export
+        output = io.BytesIO()
+        with pd.ExcelWriter(output, engine='openpyxl') as writer:
+            df.to_excel(writer, sheet_name='Kunden', index=False)
+            
+            # Formatierung
+            worksheet = writer.sheets['Kunden']
+            for column in worksheet.columns:
+                max_length = 0
+                column_letter = column[0].column_letter
+                for cell in column:
+                    try:
+                        if len(str(cell.value)) > max_length:
+                            max_length = len(str(cell.value))
+                    except:
+                        pass
+                adjusted_width = min(max_length + 2, 50)
+                worksheet.column_dimensions[column_letter].width = adjusted_width
+        
+        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/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.resource_type, r.resource_value, r.status, r.allocated_to_license, 
+               r.created_at, r.status_changed_at,
+               l.license_key, c.name as customer_name, c.email as customer_email,
+               l.license_type
+        FROM resource_pools r
+        LEFT JOIN licenses l ON r.allocated_to_license = 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 (r.is_test = false OR r.is_test IS NULL)"
+    
+    # Filter für Ressourcentyp
+    if filter_type:
+        query += " AND r.resource_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.resource_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():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Parameter
+    filter_user = request.args.get('user', '').strip()
+    filter_action = request.args.get('action', '').strip()
+    filter_entity = request.args.get('entity', '').strip()
+    page = request.args.get('page', 1, type=int)
+    sort = request.args.get('sort', 'timestamp')
+    order = request.args.get('order', 'desc')
+    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
+    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 = []
+    
+    # Filter
+    if filter_user:
+        query += " AND LOWER(username) LIKE LOWER(%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)
+    
+    # Gesamtanzahl für Pagination
+    count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table"
+    cur.execute(count_query, params)
+    total = cur.fetchone()[0]
+    
+    # Pagination
+    offset = (page - 1) * per_page
+    query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
+    params.extend([per_page, offset])
+    
+    cur.execute(query, params)
+    logs = cur.fetchall()
+    
+    # JSON-Werte parsen
+    parsed_logs = []
+    for log in logs:
+        parsed_log = list(log)
+        # old_values und new_values sind bereits Dictionaries (JSONB)
+        # Keine Konvertierung nötig
+        parsed_logs.append(parsed_log)
+    
+    # Pagination Info
+    total_pages = (total + per_page - 1) // per_page
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("audit_log.html", 
+                         logs=parsed_logs,
+                         filter_user=filter_user,
+                         filter_action=filter_action,
+                         filter_entity=filter_entity,
+                         page=page,
+                         total_pages=total_pages,
+                         total=total,
+                         sort=sort,
+                         order=order,
+                         username=session.get('username'))
+
+@app.route("/backups")
+@login_required
+def backups():
+    """Zeigt die Backup-Historie an"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Letztes erfolgreiches Backup für Dashboard
+    cur.execute("""
+        SELECT created_at, filesize, duration_seconds 
+        FROM backup_history 
+        WHERE status = 'success' 
+        ORDER BY created_at DESC 
+        LIMIT 1
+    """)
+    last_backup = cur.fetchone()
+    
+    # Alle Backups abrufen
+    cur.execute("""
+        SELECT id, filename, filesize, backup_type, status, error_message,
+               created_at, created_by, tables_count, records_count, 
+               duration_seconds, is_encrypted
+        FROM backup_history
+        ORDER BY created_at DESC
+    """)
+    backups = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("backups.html", 
+                         backups=backups,
+                         last_backup=last_backup,
+                         username=session.get('username'))
+
+@app.route("/backup/create", methods=["POST"])
+@login_required
+def create_backup_route():
+    """Erstellt ein manuelles Backup"""
+    username = session.get('username')
+    success, result = create_backup(backup_type="manual", created_by=username)
+    
+    if success:
+        return jsonify({
+            'success': True,
+            'message': f'Backup erfolgreich erstellt: {result}'
+        })
+    else:
+        return jsonify({
+            'success': False,
+            'message': f'Backup fehlgeschlagen: {result}'
+        }), 500
+
+@app.route("/backup/restore/<int:backup_id>", methods=["POST"])
+@login_required
+def restore_backup_route(backup_id):
+    """Stellt ein Backup wieder her"""
+    encryption_key = request.form.get('encryption_key')
+    
+    success, message = restore_backup(backup_id, encryption_key)
+    
+    if success:
+        return jsonify({
+            'success': True,
+            'message': message
+        })
+    else:
+        return jsonify({
+            'success': False,
+            'message': message
+        }), 500
+
+@app.route("/backup/download/<int:backup_id>")
+@login_required
+def download_backup(backup_id):
+    """Lädt eine Backup-Datei herunter"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT filename, filepath 
+        FROM backup_history 
+        WHERE id = %s
+    """, (backup_id,))
+    backup_info = cur.fetchone()
+    
+    cur.close()
+    conn.close()
+    
+    if not backup_info:
+        return "Backup nicht gefunden", 404
+    
+    filename, filepath = backup_info
+    filepath = Path(filepath)
+    
+    if not filepath.exists():
+        return "Backup-Datei nicht gefunden", 404
+    
+    # Audit-Log
+    log_audit('DOWNLOAD', 'backup', backup_id,
+             additional_info=f"Backup heruntergeladen: {filename}")
+    
+    return send_file(filepath, as_attachment=True, download_name=filename)
+
+@app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
+@login_required
+def delete_backup(backup_id):
+    """Löscht ein Backup"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Backup-Informationen abrufen
+        cur.execute("""
+            SELECT filename, filepath 
+            FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        backup_info = cur.fetchone()
+        
+        if not backup_info:
+            return jsonify({
+                'success': False,
+                'message': 'Backup nicht gefunden'
+            }), 404
+        
+        filename, filepath = backup_info
+        filepath = Path(filepath)
+        
+        # Datei löschen, wenn sie existiert
+        if filepath.exists():
+            filepath.unlink()
+        
+        # Aus Datenbank löschen
+        cur.execute("""
+            DELETE FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('DELETE', 'backup', backup_id,
+                 additional_info=f"Backup gelöscht: {filename}")
+        
+        return jsonify({
+            'success': True,
+            'message': f'Backup "{filename}" wurde erfolgreich gelöscht'
+        })
+        
+    except Exception as e:
+        conn.rollback()
+        return jsonify({
+            'success': False,
+            'message': f'Fehler beim Löschen des Backups: {str(e)}'
+        }), 500
+    finally:
+        cur.close()
+        conn.close()
+
+@app.route("/security/blocked-ips")
+@login_required
+def blocked_ips():
+    """Zeigt alle gesperrten IPs an"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT 
+            ip_address,
+            attempt_count,
+            first_attempt,
+            last_attempt,
+            blocked_until,
+            last_username_tried,
+            last_error_message
+        FROM login_attempts
+        WHERE blocked_until IS NOT NULL
+        ORDER BY blocked_until DESC
+    """)
+    
+    blocked_ips_list = []
+    for ip in cur.fetchall():
+        blocked_ips_list.append({
+            'ip_address': ip[0],
+            'attempt_count': ip[1],
+            'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'),
+            'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'),
+            'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'),
+            'is_active': ip[4] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None),
+            'last_username': ip[5],
+            'last_error': ip[6]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("blocked_ips.html", 
+                         blocked_ips=blocked_ips_list,
+                         username=session.get('username'))
+
+@app.route("/security/unblock-ip", methods=["POST"])
+@login_required
+def unblock_ip():
+    """Entsperrt eine IP-Adresse"""
+    ip_address = request.form.get('ip_address')
+    
+    if ip_address:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        cur.execute("""
+            UPDATE login_attempts
+            SET blocked_until = NULL
+            WHERE ip_address = %s
+        """, (ip_address,))
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        # Audit-Log
+        log_audit('UNBLOCK_IP', 'security', 
+                 additional_info=f"IP {ip_address} manuell entsperrt")
+    
+    return redirect(url_for('blocked_ips'))
+
+@app.route("/security/clear-attempts", methods=["POST"])
+@login_required
+def clear_attempts():
+    """Löscht alle Login-Versuche für eine IP"""
+    ip_address = request.form.get('ip_address')
+    
+    if ip_address:
+        reset_login_attempts(ip_address)
+        
+        # Audit-Log
+        log_audit('CLEAR_ATTEMPTS', 'security', 
+                 additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt")
+    
+    return redirect(url_for('blocked_ips'))
+
+# API Endpoints for License Management
+@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
+@login_required
+def toggle_license_api(license_id):
+    """Toggle license active status via API"""
+    try:
+        data = request.get_json()
+        is_active = data.get('is_active', False)
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update license status
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = %s
+            WHERE id = %s
+        """, (is_active, license_id))
+        
+        conn.commit()
+        
+        # Log the action
+        log_audit('UPDATE', 'license', license_id,
+                 new_values={'is_active': is_active},
+                 additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Status erfolgreich geändert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/licenses/bulk-activate", methods=["POST"])
+@login_required
+def bulk_activate_licenses():
+    """Activate multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update all selected licenses (nur Live-Daten)
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = TRUE
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_UPDATE', 'licenses', None,
+                 new_values={'is_active': True, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen aktiviert")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
+@login_required
+def bulk_deactivate_licenses():
+    """Deactivate multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update all selected licenses (nur Live-Daten)
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = FALSE
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_UPDATE', 'licenses', None,
+                 new_values={'is_active': False, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen deaktiviert")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/license/<int:license_id>/devices")
+@login_required
+def get_license_devices(license_id):
+    """Hole alle registrierten Geräte einer Lizenz"""
+    try:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob Lizenz existiert und hole device_limit
+        cur.execute("""
+            SELECT device_limit FROM licenses WHERE id = %s
+        """, (license_id,))
+        license_data = cur.fetchone()
+        
+        if not license_data:
+            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
+        
+        device_limit = license_data[0]
+        
+        # Hole alle Geräte für diese Lizenz
+        cur.execute("""
+            SELECT id, hardware_id, device_name, operating_system,
+                   first_seen, last_seen, is_active, ip_address
+            FROM device_registrations
+            WHERE license_id = %s
+            ORDER BY is_active DESC, last_seen DESC
+        """, (license_id,))
+        
+        devices = []
+        for row in cur.fetchall():
+            devices.append({
+                'id': row[0],
+                'hardware_id': row[1],
+                'device_name': row[2] or 'Unbekanntes Gerät',
+                'operating_system': row[3] or 'Unbekannt',
+                'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '',
+                'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '',
+                'is_active': row[6],
+                'ip_address': row[7] or '-'
+            })
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'devices': devices,
+            'device_limit': device_limit,
+            'active_count': sum(1 for d in devices if d['is_active'])
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
+
+@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
+def register_device(license_id):
+    """Registriere ein neues Gerät für eine Lizenz"""
+    try:
+        data = request.get_json()
+        hardware_id = data.get('hardware_id')
+        device_name = data.get('device_name', '')
+        operating_system = data.get('operating_system', '')
+        
+        if not hardware_id:
+            return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob Lizenz existiert und aktiv ist
+        cur.execute("""
+            SELECT device_limit, is_active, valid_until
+            FROM licenses 
+            WHERE id = %s
+        """, (license_id,))
+        license_data = cur.fetchone()
+        
+        if not license_data:
+            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
+        
+        device_limit, is_active, valid_until = license_data
+        
+        # Prüfe ob Lizenz aktiv und gültig ist
+        if not is_active:
+            return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
+        
+        if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
+            return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
+        
+        # Prüfe ob Gerät bereits registriert ist
+        cur.execute("""
+            SELECT id, is_active FROM device_registrations
+            WHERE license_id = %s AND hardware_id = %s
+        """, (license_id, hardware_id))
+        existing_device = cur.fetchone()
+        
+        if existing_device:
+            device_id, is_device_active = existing_device
+            if is_device_active:
+                # Gerät ist bereits aktiv, update last_seen
+                cur.execute("""
+                    UPDATE device_registrations
+                    SET last_seen = CURRENT_TIMESTAMP,
+                        ip_address = %s,
+                        user_agent = %s
+                    WHERE id = %s
+                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                conn.commit()
+                return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
+            else:
+                # Gerät war deaktiviert, prüfe ob wir es reaktivieren können
+                cur.execute("""
+                    SELECT COUNT(*) FROM device_registrations
+                    WHERE license_id = %s AND is_active = TRUE
+                """, (license_id,))
+                active_count = cur.fetchone()[0]
+                
+                if active_count >= device_limit:
+                    return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+                
+                # Reaktiviere das Gerät
+                cur.execute("""
+                    UPDATE device_registrations
+                    SET is_active = TRUE,
+                        last_seen = CURRENT_TIMESTAMP,
+                        deactivated_at = NULL,
+                        deactivated_by = NULL,
+                        ip_address = %s,
+                        user_agent = %s
+                    WHERE id = %s
+                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                conn.commit()
+                return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
+        
+        # Neues Gerät - prüfe Gerätelimit
+        cur.execute("""
+            SELECT COUNT(*) FROM device_registrations
+            WHERE license_id = %s AND is_active = TRUE
+        """, (license_id,))
+        active_count = cur.fetchone()[0]
+        
+        if active_count >= device_limit:
+            return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+        
+        # Registriere neues Gerät
+        cur.execute("""
+            INSERT INTO device_registrations 
+            (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
+            VALUES (%s, %s, %s, %s, %s, %s)
+            RETURNING id
+        """, (license_id, hardware_id, device_name, operating_system, 
+              get_client_ip(), request.headers.get('User-Agent', '')))
+        device_id = cur.fetchone()[0]
+        
+        conn.commit()
+        
+        # Audit Log
+        log_audit('DEVICE_REGISTER', 'device', device_id,
+                 new_values={'license_id': license_id, 'hardware_id': hardware_id})
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
+
+@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
+@login_required
+def deactivate_device(license_id, device_id):
+    """Deaktiviere ein registriertes Gerät"""
+    try:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob das Gerät zu dieser Lizenz gehört
+        cur.execute("""
+            SELECT id FROM device_registrations
+            WHERE id = %s AND license_id = %s AND is_active = TRUE
+        """, (device_id, license_id))
+        
+        if not cur.fetchone():
+            return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404
+        
+        # Deaktiviere das Gerät
+        cur.execute("""
+            UPDATE device_registrations
+            SET is_active = FALSE,
+                deactivated_at = CURRENT_TIMESTAMP,
+                deactivated_by = %s
+            WHERE id = %s
+        """, (session['username'], device_id))
+        
+        conn.commit()
+        
+        # Audit Log
+        log_audit('DEVICE_DEACTIVATE', 'device', device_id,
+                 old_values={'is_active': True},
+                 new_values={'is_active': False})
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'})
+        
+    except Exception as e:
+        logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
+
+@app.route("/api/licenses/bulk-delete", methods=["POST"])
+@login_required
+def bulk_delete_licenses():
+    """Delete multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Get license info for audit log (nur Live-Daten)
+        cur.execute("""
+            SELECT license_key 
+            FROM licenses 
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        license_keys = [row[0] for row in cur.fetchall()]
+        
+        # Delete all selected licenses (nur Live-Daten)
+        cur.execute("""
+            DELETE FROM licenses 
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_DELETE', 'licenses', None,
+                 old_values={'license_keys': license_keys, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen gelöscht")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+# ===================== RESOURCE POOL MANAGEMENT =====================
+
+@app.route('/resources')
+@login_required
+def resources():
+    """Resource Pool Hauptübersicht"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Prüfe ob Testdaten angezeigt werden sollen (gleiche Logik wie bei Kunden)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    # Statistiken abrufen
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'available') as available,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+            COUNT(*) as total
+        FROM resource_pools
+        WHERE is_test = %s
+        GROUP BY resource_type
+    """, (show_test,))
+    
+    stats = {}
+    for row in cur.fetchall():
+        stats[row[0]] = {
+            'available': row[1],
+            'allocated': row[2],
+            'quarantine': row[3],
+            'total': row[4],
+            'available_percent': round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
+        }
+    
+    # Letzte Aktivitäten (gefiltert nach Test/Live)
+    cur.execute("""
+        SELECT 
+            rh.action,
+            rh.action_by,
+            rh.action_at,
+            rp.resource_type,
+            rp.resource_value,
+            rh.details
+        FROM resource_history rh
+        JOIN resource_pools rp ON rh.resource_id = rp.id
+        WHERE rp.is_test = %s
+        ORDER BY rh.action_at DESC
+        LIMIT 10
+    """, (show_test,))
+    recent_activities = cur.fetchall()
+    
+    # Ressourcen-Liste mit Pagination
+    page = request.args.get('page', 1, type=int)
+    per_page = 50
+    offset = (page - 1) * per_page
+    
+    resource_type = request.args.get('type', '')
+    status_filter = request.args.get('status', '')
+    search = request.args.get('search', '')
+    
+    # Sortierung
+    sort_by = request.args.get('sort', 'id')
+    sort_order = request.args.get('order', 'desc')
+    
+    # Base Query
+    query = """
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            rp.status,
+            rp.allocated_to_license,
+            l.license_key,
+            c.name as customer_name,
+            rp.status_changed_at,
+            rp.quarantine_reason,
+            rp.quarantine_until,
+            c.id as customer_id
+        FROM resource_pools rp
+        LEFT JOIN licenses l ON rp.allocated_to_license = l.id
+        LEFT JOIN customers c ON l.customer_id = c.id
+        WHERE rp.is_test = %s
+    """
+    params = [show_test]
+    
+    if resource_type:
+        query += " AND rp.resource_type = %s"
+        params.append(resource_type)
+    
+    if status_filter:
+        query += " AND rp.status = %s"
+        params.append(status_filter)
+    
+    if search:
+        query += " AND rp.resource_value ILIKE %s"
+        params.append(f'%{search}%')
+    
+    # Count total
+    count_query = f"SELECT COUNT(*) FROM ({query}) as cnt"
+    cur.execute(count_query, params)
+    total = cur.fetchone()[0]
+    total_pages = (total + per_page - 1) // per_page
+    
+    # Get paginated results with dynamic sorting
+    sort_column_map = {
+        'id': 'rp.id',
+        'type': 'rp.resource_type',
+        'resource': 'rp.resource_value',
+        'status': 'rp.status',
+        'assigned': 'c.name',
+        'changed': 'rp.status_changed_at'
+    }
+    
+    sort_column = sort_column_map.get(sort_by, 'rp.id')
+    sort_direction = 'ASC' if sort_order == 'asc' else 'DESC'
+    
+    query += f" ORDER BY {sort_column} {sort_direction} LIMIT %s OFFSET %s"
+    params.extend([per_page, offset])
+    
+    cur.execute(query, params)
+    resources = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template('resources.html',
+                         stats=stats,
+                         resources=resources,
+                         recent_activities=recent_activities,
+                         page=page,
+                         total_pages=total_pages,
+                         total=total,
+                         resource_type=resource_type,
+                         status_filter=status_filter,
+                         search=search,
+                         show_test=show_test,
+                         sort_by=sort_by,
+                         sort_order=sort_order,
+                         datetime=datetime,
+                         timedelta=timedelta)
+
+@app.route('/resources/add', methods=['GET', 'POST'])
+@login_required
+def add_resources():
+    """Ressourcen zum Pool hinzufügen"""
+    # Hole show_test Parameter für die Anzeige
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    if request.method == 'POST':
+        resource_type = request.form.get('resource_type')
+        resources_text = request.form.get('resources_text', '')
+        is_test = request.form.get('is_test') == 'on'  # Checkbox für Testdaten
+        
+        # Parse resources (one per line)
+        resources = [r.strip() for r in resources_text.split('\n') if r.strip()]
+        
+        if not resources:
+            flash('Keine Ressourcen angegeben', 'error')
+            return redirect(url_for('add_resources', show_test=show_test))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        added = 0
+        duplicates = 0
+        
+        for resource_value in resources:
+            try:
+                cur.execute("""
+                    INSERT INTO resource_pools (resource_type, resource_value, status_changed_by, is_test)
+                    VALUES (%s, %s, %s, %s)
+                    ON CONFLICT (resource_type, resource_value) DO NOTHING
+                """, (resource_type, resource_value, session['username'], is_test))
+                
+                if cur.rowcount > 0:
+                    added += 1
+                    # Get the inserted ID
+                    cur.execute("SELECT id FROM resource_pools WHERE resource_type = %s AND resource_value = %s", 
+                              (resource_type, resource_value))
+                    resource_id = cur.fetchone()[0]
+                    
+                    # Log in history
+                    cur.execute("""
+                        INSERT INTO resource_history (resource_id, action, action_by, ip_address)
+                        VALUES (%s, 'created', %s, %s)
+                    """, (resource_id, session['username'], get_client_ip()))
+                else:
+                    duplicates += 1
+                    
+            except Exception as e:
+                app.logger.error(f"Error adding resource {resource_value}: {e}")
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        log_audit('CREATE', 'resource_pool', None,
+                 new_values={'type': resource_type, 'added': added, 'duplicates': duplicates, 'is_test': is_test},
+                 additional_info=f"{added} {'Test-' if is_test else ''}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen")
+        
+        flash(f'{added} {"Test-" if is_test else ""}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen', 'success')
+        return redirect(url_for('resources', show_test=show_test))
+    
+    return render_template('add_resources.html', show_test=show_test)
+
+@app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
+@login_required
+def quarantine_resource(resource_id):
+    """Ressource in Quarantäne setzen"""
+    reason = request.form.get('reason', 'review')
+    until_date = request.form.get('until_date')
+    notes = request.form.get('notes', '')
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Get current resource info
+    cur.execute("SELECT resource_type, resource_value, status FROM resource_pools WHERE id = %s", (resource_id,))
+    resource = cur.fetchone()
+    
+    if not resource:
+        flash('Ressource nicht gefunden', 'error')
+        return redirect(url_for('resources'))
+    
+    old_status = resource[2]
+    
+    # Update resource
+    cur.execute("""
+        UPDATE resource_pools
+        SET status = 'quarantine',
+            quarantine_reason = %s,
+            quarantine_until = %s,
+            notes = %s,
+            status_changed_at = CURRENT_TIMESTAMP,
+            status_changed_by = %s
+        WHERE id = %s
+    """, (reason, until_date if until_date else None, notes, session['username'], resource_id))
+    
+    # Log in history
+    cur.execute("""
+        INSERT INTO resource_history (resource_id, action, action_by, ip_address, details)
+        VALUES (%s, 'quarantined', %s, %s, %s)
+    """, (resource_id, session['username'], get_client_ip(), 
+          Json({'reason': reason, 'until': until_date, 'notes': notes, 'old_status': old_status})))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('UPDATE', 'resource', resource_id,
+             old_values={'status': old_status},
+             new_values={'status': 'quarantine', 'reason': reason},
+             additional_info=f"Ressource {resource[0]}: {resource[1]} in Quarantäne")
+    
+    flash('Ressource in Quarantäne gesetzt', 'success')
+    
+    # Redirect mit allen aktuellen Filtern
+    return redirect(url_for('resources', 
+                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
+                          type=request.args.get('type', request.form.get('type', '')),
+                          status=request.args.get('status', request.form.get('status', '')),
+                          search=request.args.get('search', request.form.get('search', ''))))
+
+@app.route('/resources/release', methods=['POST'])
+@login_required
+def release_resources():
+    """Ressourcen aus Quarantäne freigeben"""
+    resource_ids = request.form.getlist('resource_ids')
+    
+    if not resource_ids:
+        flash('Keine Ressourcen ausgewählt', 'error')
+        return redirect(url_for('resources'))
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    released = 0
+    for resource_id in resource_ids:
+        cur.execute("""
+            UPDATE resource_pools
+            SET status = 'available',
+                quarantine_reason = NULL,
+                quarantine_until = NULL,
+                allocated_to_license = NULL,
+                status_changed_at = CURRENT_TIMESTAMP,
+                status_changed_by = %s
+            WHERE id = %s AND status = 'quarantine'
+        """, (session['username'], resource_id))
+        
+        if cur.rowcount > 0:
+            released += 1
+            # Log in history
+            cur.execute("""
+                INSERT INTO resource_history (resource_id, action, action_by, ip_address)
+                VALUES (%s, 'released', %s, %s)
+            """, (resource_id, session['username'], get_client_ip()))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('UPDATE', 'resource_pool', None,
+             new_values={'released': released},
+             additional_info=f"{released} Ressourcen aus Quarantäne freigegeben")
+    
+    flash(f'{released} Ressourcen freigegeben', 'success')
+    
+    # Redirect mit allen aktuellen Filtern
+    return redirect(url_for('resources', 
+                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
+                          type=request.args.get('type', request.form.get('type', '')),
+                          status=request.args.get('status', request.form.get('status', '')),
+                          search=request.args.get('search', request.form.get('search', ''))))
+
+@app.route('/api/resources/allocate', methods=['POST'])
+@login_required
+def allocate_resources_api():
+    """API für Ressourcen-Zuweisung bei Lizenzerstellung"""
+    data = request.json
+    license_id = data.get('license_id')
+    domain_count = data.get('domain_count', 1)
+    ipv4_count = data.get('ipv4_count', 1)
+    phone_count = data.get('phone_count', 1)
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        allocated = {'domains': [], 'ipv4s': [], 'phones': []}
+        
+        # Allocate domains
+        if domain_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'domain' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (domain_count,))
+            domains = cur.fetchall()
+            
+            if len(domains) < domain_count:
+                raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {len(domains)})")
+            
+            for domain_id, domain_value in domains:
+                # Update resource status
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], domain_id))
+                
+                # Create assignment
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, domain_id, session['username']))
+                
+                # Log history
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (domain_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['domains'].append(domain_value)
+        
+        # Allocate IPv4s (similar logic)
+        if ipv4_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'ipv4' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (ipv4_count,))
+            ipv4s = cur.fetchall()
+            
+            if len(ipv4s) < ipv4_count:
+                raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar")
+            
+            for ipv4_id, ipv4_value in ipv4s:
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], ipv4_id))
+                
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, ipv4_id, session['username']))
+                
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (ipv4_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['ipv4s'].append(ipv4_value)
+        
+        # Allocate phones (similar logic)
+        if phone_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'phone' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (phone_count,))
+            phones = cur.fetchall()
+            
+            if len(phones) < phone_count:
+                raise ValueError(f"Nicht genügend Telefonnummern verfügbar")
+            
+            for phone_id, phone_value in phones:
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], phone_id))
+                
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, phone_id, session['username']))
+                
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (phone_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['phones'].append(phone_value)
+        
+        # Update license resource counts
+        cur.execute("""
+            UPDATE licenses
+            SET domain_count = %s,
+                ipv4_count = %s,
+                phone_count = %s
+            WHERE id = %s
+        """, (domain_count, ipv4_count, phone_count, license_id))
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'allocated': allocated
+        })
+        
+    except Exception as e:
+        conn.rollback()
+        cur.close()
+        conn.close()
+        return jsonify({
+            'success': False,
+            'error': str(e)
+        }), 400
+
+@app.route('/api/resources/check-availability', methods=['GET'])
+@login_required
+def check_resource_availability():
+    """Prüft verfügbare Ressourcen"""
+    resource_type = request.args.get('type', '')
+    count = request.args.get('count', 10, type=int)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if resource_type:
+        # Spezifische Ressourcen für einen Typ
+        cur.execute("""
+            SELECT id, resource_value
+            FROM resource_pools
+            WHERE status = 'available' 
+            AND resource_type = %s
+            AND is_test = %s
+            ORDER BY resource_value
+            LIMIT %s
+        """, (resource_type, show_test, count))
+        
+        resources = []
+        for row in cur.fetchall():
+            resources.append({
+                'id': row[0],
+                'value': row[1]
+            })
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'available': resources,
+            'type': resource_type,
+            'count': len(resources)
+        })
+    else:
+        # Zusammenfassung aller Typen
+        cur.execute("""
+            SELECT 
+                resource_type,
+                COUNT(*) as available
+            FROM resource_pools
+            WHERE status = 'available'
+            AND is_test = %s
+            GROUP BY resource_type
+        """, (show_test,))
+        
+        availability = {}
+        for row in cur.fetchall():
+            availability[row[0]] = row[1]
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify(availability)
+
+@app.route('/api/global-search', methods=['GET'])
+@login_required
+def global_search():
+    """Global search API endpoint for searching customers and licenses"""
+    query = request.args.get('q', '').strip()
+    
+    if not query or len(query) < 2:
+        return jsonify({'customers': [], 'licenses': []})
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Search pattern with wildcards
+    search_pattern = f'%{query}%'
+    
+    # Search customers
+    cur.execute("""
+        SELECT id, name, email, company_name
+        FROM customers
+        WHERE (LOWER(name) LIKE LOWER(%s) 
+               OR LOWER(email) LIKE LOWER(%s)
+               OR LOWER(company_name) LIKE LOWER(%s))
+        AND is_test = FALSE
+        ORDER BY name
+        LIMIT 5
+    """, (search_pattern, search_pattern, search_pattern))
+    
+    customers = []
+    for row in cur.fetchall():
+        customers.append({
+            'id': row[0],
+            'name': row[1],
+            'email': row[2],
+            'company_name': row[3]
+        })
+    
+    # Search licenses
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name as customer_name
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE LOWER(l.license_key) LIKE LOWER(%s)
+        AND l.is_test = FALSE
+        ORDER BY l.created_at DESC
+        LIMIT 5
+    """, (search_pattern,))
+    
+    licenses = []
+    for row in cur.fetchall():
+        licenses.append({
+            'id': row[0],
+            'license_key': row[1],
+            'customer_name': row[2]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'customers': customers,
+        'licenses': licenses
+    })
+
+@app.route('/resources/history/<int:resource_id>')
+@login_required
+def resource_history(resource_id):
+    """Zeigt die komplette Historie einer Ressource"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Get complete resource info using named columns
+    cur.execute("""
+        SELECT id, resource_type, resource_value, status, allocated_to_license,
+               status_changed_at, status_changed_by, quarantine_reason, 
+               quarantine_until, created_at, notes
+        FROM resource_pools
+        WHERE id = %s
+    """, (resource_id,))
+    row = cur.fetchone()
+    
+    if not row:
+        flash('Ressource nicht gefunden', 'error')
+        return redirect(url_for('resources'))
+    
+    # Create resource object with named attributes
+    resource = {
+        'id': row[0],
+        'resource_type': row[1],
+        'resource_value': row[2],
+        'status': row[3],
+        'allocated_to_license': row[4],
+        'status_changed_at': row[5],
+        'status_changed_by': row[6],
+        'quarantine_reason': row[7],
+        'quarantine_until': row[8],
+        'created_at': row[9],
+        'notes': row[10]
+    }
+    
+    # Get license info if allocated
+    license_info = None
+    if resource['allocated_to_license']:
+        cur.execute("SELECT license_key FROM licenses WHERE id = %s", 
+                   (resource['allocated_to_license'],))
+        lic = cur.fetchone()
+        if lic:
+            license_info = {'license_key': lic[0]}
+    
+    # Get history with named columns
+    cur.execute("""
+        SELECT 
+            rh.action,
+            rh.action_by,
+            rh.action_at,
+            rh.details,
+            rh.license_id,
+            rh.ip_address
+        FROM resource_history rh
+        WHERE rh.resource_id = %s
+        ORDER BY rh.action_at DESC
+    """, (resource_id,))
+    
+    history = []
+    for row in cur.fetchall():
+        history.append({
+            'action': row[0],
+            'action_by': row[1],
+            'action_at': row[2],
+            'details': row[3],
+            'license_id': row[4],
+            'ip_address': row[5]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    # Convert to object-like for template
+    class ResourceObj:
+        def __init__(self, data):
+            for key, value in data.items():
+                setattr(self, key, value)
+    
+    resource_obj = ResourceObj(resource)
+    history_objs = [ResourceObj(h) for h in history]
+    
+    return render_template('resource_history.html',
+                         resource=resource_obj,
+                         license_info=license_info,
+                         history=history_objs)
+
+@app.route('/resources/metrics')
+@login_required
+def resources_metrics():
+    """Dashboard für Resource Metrics und Reports"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Overall stats with fallback values
+    cur.execute("""
+        SELECT 
+            COUNT(DISTINCT resource_id) as total_resources,
+            COALESCE(AVG(performance_score), 0) as avg_performance,
+            COALESCE(SUM(cost), 0) as total_cost,
+            COALESCE(SUM(revenue), 0) as total_revenue,
+            COALESCE(SUM(issues_count), 0) as total_issues
+        FROM resource_metrics
+        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
+    """)
+    row = cur.fetchone()
+    
+    # Calculate ROI
+    roi = 0
+    if row[2] > 0:  # if total_cost > 0
+        roi = row[3] / row[2]  # revenue / cost
+    
+    stats = {
+        'total_resources': row[0] or 0,
+        'avg_performance': row[1] or 0,
+        'total_cost': row[2] or 0,
+        'total_revenue': row[3] or 0,
+        'total_issues': row[4] or 0,
+        'roi': roi
+    }
+    
+    # Performance by type
+    cur.execute("""
+        SELECT 
+            rp.resource_type,
+            COALESCE(AVG(rm.performance_score), 0) as avg_score,
+            COUNT(DISTINCT rp.id) as resource_count
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        GROUP BY rp.resource_type
+        ORDER BY rp.resource_type
+    """)
+    performance_by_type = cur.fetchall()
+    
+    # Utilization data
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) as total,
+            ROUND(COUNT(*) FILTER (WHERE status = 'allocated') * 100.0 / COUNT(*), 1) as allocated_percent
+        FROM resource_pools
+        GROUP BY resource_type
+    """)
+    utilization_rows = cur.fetchall()
+    utilization_data = [
+        {
+            'type': row[0].upper(),
+            'allocated': row[1],
+            'total': row[2],
+            'allocated_percent': row[3]
+        }
+        for row in utilization_rows
+    ]
+    
+    # Top performing resources
+    cur.execute("""
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            COALESCE(AVG(rm.performance_score), 0) as avg_score,
+            COALESCE(SUM(rm.revenue), 0) as total_revenue,
+            COALESCE(SUM(rm.cost), 1) as total_cost,
+            CASE 
+                WHEN COALESCE(SUM(rm.cost), 0) = 0 THEN 0
+                ELSE COALESCE(SUM(rm.revenue), 0) / COALESCE(SUM(rm.cost), 1)
+            END as roi
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        WHERE rp.status != 'quarantine'
+        GROUP BY rp.id, rp.resource_type, rp.resource_value
+        HAVING AVG(rm.performance_score) IS NOT NULL
+        ORDER BY avg_score DESC
+        LIMIT 10
+    """)
+    top_rows = cur.fetchall()
+    top_performers = [
+        {
+            'id': row[0],
+            'resource_type': row[1],
+            'resource_value': row[2],
+            'avg_score': row[3],
+            'roi': row[6]
+        }
+        for row in top_rows
+    ]
+    
+    # Resources with issues
+    cur.execute("""
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            rp.status,
+            COALESCE(SUM(rm.issues_count), 0) as total_issues
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        WHERE rm.issues_count > 0 OR rp.status = 'quarantine'
+        GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
+        HAVING SUM(rm.issues_count) > 0
+        ORDER BY total_issues DESC
+        LIMIT 10
+    """)
+    problem_rows = cur.fetchall()
+    problem_resources = [
+        {
+            'id': row[0],
+            'resource_type': row[1],
+            'resource_value': row[2],
+            'status': row[3],
+            'total_issues': row[4]
+        }
+        for row in problem_rows
+    ]
+    
+    # Daily metrics for trend chart (last 30 days)
+    cur.execute("""
+        SELECT 
+            metric_date,
+            COALESCE(AVG(performance_score), 0) as avg_performance,
+            COALESCE(SUM(issues_count), 0) as total_issues
+        FROM resource_metrics
+        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        GROUP BY metric_date
+        ORDER BY metric_date
+    """)
+    daily_rows = cur.fetchall()
+    daily_metrics = [
+        {
+            'date': row[0].strftime('%d.%m'),
+            'performance': float(row[1]),
+            'issues': int(row[2])
+        }
+        for row in daily_rows
+    ]
+    
+    cur.close()
+    conn.close()
+    
+    return render_template('resource_metrics.html',
+                         stats=stats,
+                         performance_by_type=performance_by_type,
+                         utilization_data=utilization_data,
+                         top_performers=top_performers,
+                         problem_resources=problem_resources,
+                         daily_metrics=daily_metrics)
+
+@app.route('/resources/report', methods=['GET'])
+@login_required
+def resources_report():
+    """Generiert Ressourcen-Reports oder zeigt Report-Formular"""
+    # Prüfe ob Download angefordert wurde
+    if request.args.get('download') == 'true':
+        report_type = request.args.get('type', 'usage')
+        format_type = request.args.get('format', 'excel')
+        date_from = request.args.get('from', (datetime.now(ZoneInfo("Europe/Berlin")) - timedelta(days=30)).strftime('%Y-%m-%d'))
+        date_to = request.args.get('to', datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        if report_type == 'usage':
+            # Auslastungsreport
+            query = """
+                SELECT 
+                    rp.resource_type,
+                    rp.resource_value,
+                    rp.status,
+                    COUNT(DISTINCT rh.license_id) as unique_licenses,
+                    COUNT(rh.id) as total_allocations,
+                    MIN(rh.action_at) as first_used,
+                    MAX(rh.action_at) as last_used
+                FROM resource_pools rp
+                LEFT JOIN resource_history rh ON rp.id = rh.resource_id 
+                    AND rh.action = 'allocated'
+                    AND rh.action_at BETWEEN %s AND %s
+                GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
+                ORDER BY rp.resource_type, total_allocations DESC
+            """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Typ', 'Ressource', 'Status', 'Unique Lizenzen', 'Gesamt Zuweisungen', 'Erste Nutzung', 'Letzte Nutzung']
+            
+        elif report_type == 'performance':
+            # Performance-Report
+            query = """
+                SELECT 
+                    rp.resource_type,
+                rp.resource_value,
+                AVG(rm.performance_score) as avg_performance,
+                SUM(rm.usage_count) as total_usage,
+                SUM(rm.revenue) as total_revenue,
+                SUM(rm.cost) as total_cost,
+                SUM(rm.revenue - rm.cost) as profit,
+                SUM(rm.issues_count) as total_issues
+            FROM resource_pools rp
+            JOIN resource_metrics rm ON rp.id = rm.resource_id
+            WHERE rm.metric_date BETWEEN %s AND %s
+            GROUP BY rp.id, rp.resource_type, rp.resource_value
+            ORDER BY profit DESC
+        """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Typ', 'Ressource', 'Durchschn. Performance', 'Gesamt Nutzung', 'Umsatz', 'Kosten', 'Gewinn', 'Issues']
+            
+        elif report_type == 'compliance':
+            # Compliance-Report
+            query = """
+            SELECT 
+                rh.action_at,
+                rh.action,
+                rh.action_by,
+                rp.resource_type,
+                rp.resource_value,
+                l.license_key,
+                c.name as customer_name,
+                rh.ip_address
+            FROM resource_history rh
+            JOIN resource_pools rp ON rh.resource_id = rp.id
+            LEFT JOIN licenses l ON rh.license_id = l.id
+            LEFT JOIN customers c ON l.customer_id = c.id
+            WHERE rh.action_at BETWEEN %s AND %s
+                ORDER BY rh.action_at DESC
+            """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Zeit', 'Aktion', 'Von', 'Typ', 'Ressource', 'Lizenz', 'Kunde', 'IP-Adresse']
+        
+        else:  # inventory report
+            # Inventar-Report
+            query = """
+                SELECT 
+                    resource_type,
+                    COUNT(*) FILTER (WHERE status = 'available') as available,
+                    COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+                    COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+                    COUNT(*) as total
+                FROM resource_pools
+                GROUP BY resource_type
+                ORDER BY resource_type
+            """
+            cur.execute(query)
+            columns = ['Typ', 'Verfügbar', 'Zugeteilt', 'Quarantäne', 'Gesamt']
+        
+        # Convert to DataFrame
+        data = cur.fetchall()
+        df = pd.DataFrame(data, columns=columns)
+        
+        cur.close()
+        conn.close()
+        
+        # Generate file
+        timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
+        filename = f"resource_report_{report_type}_{timestamp}"
+        
+        if format_type == 'excel':
+            output = io.BytesIO()
+            with pd.ExcelWriter(output, engine='openpyxl') as writer:
+                df.to_excel(writer, sheet_name='Report', index=False)
+                
+                # Auto-adjust columns width
+                worksheet = writer.sheets['Report']
+                for column in worksheet.columns:
+                    max_length = 0
+                    column = [cell for cell in column]
+                    for cell in column:
+                        try:
+                            if len(str(cell.value)) > max_length:
+                                max_length = len(str(cell.value))
+                        except:
+                            pass
+                    adjusted_width = (max_length + 2)
+                    worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
+            
+            output.seek(0)
+            
+            log_audit('EXPORT', 'resource_report', None,
+                     new_values={'type': report_type, 'format': 'excel', 'rows': len(df)},
+                     additional_info=f"Resource Report {report_type} exportiert")
+            
+            return send_file(output, 
+                            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+                            as_attachment=True,
+                            download_name=f'{filename}.xlsx')
+        
+        else:  # CSV
+            output = io.StringIO()
+            df.to_csv(output, index=False, sep=';', encoding='utf-8-sig')
+            output.seek(0)
+            
+            log_audit('EXPORT', 'resource_report', None,
+                     new_values={'type': report_type, 'format': 'csv', 'rows': len(df)},
+                     additional_info=f"Resource Report {report_type} exportiert")
+            
+            return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')),
+                            mimetype='text/csv',
+                            as_attachment=True,
+                            download_name=f'{filename}.csv')
+    
+    # Wenn kein Download, zeige Report-Formular
+    return render_template('resource_report.html',
+                         datetime=datetime,
+                         timedelta=timedelta,
+                         username=session.get('username'))
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=5000)
diff --git a/v2_adminpanel/app.py.old b/v2_adminpanel/app.py.old
index 3849500..34c3f29 100644
--- a/v2_adminpanel/app.py.old
+++ b/v2_adminpanel/app.py.old
@@ -1,5021 +1,5021 @@
-import os
-import time
-import json
-import logging
-import requests
-from io import BytesIO
-from datetime import datetime, timedelta
-from zoneinfo import ZoneInfo
-from pathlib import Path
-
-from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash
-from flask_session import Session
-from werkzeug.middleware.proxy_fix import ProxyFix
-from apscheduler.schedulers.background import BackgroundScheduler
-import pandas as pd
-from psycopg2.extras import Json
-
-# Import our new modules
-import config
-from db import get_connection, get_db_connection, get_db_cursor, execute_query
-from auth.decorators import login_required
-from auth.password import hash_password, verify_password
-from auth.two_factor import (
-    generate_totp_secret, generate_qr_code, verify_totp,
-    generate_backup_codes, hash_backup_code, verify_backup_code
-)
-from auth.rate_limiting import (
-    get_client_ip, check_ip_blocked, record_failed_attempt,
-    reset_login_attempts, get_login_attempts
-)
-from utils.audit import log_audit
-from utils.license import generate_license_key, validate_license_key
-from utils.backup import create_backup, restore_backup, get_or_create_encryption_key
-from utils.export import (
-    create_excel_export, format_datetime_for_export,
-    prepare_license_export_data, prepare_customer_export_data,
-    prepare_session_export_data, prepare_audit_export_data
-)
-
-app = Flask(__name__)
-# Load configuration from config module
-app.config['SECRET_KEY'] = config.SECRET_KEY
-app.config['SESSION_TYPE'] = config.SESSION_TYPE
-app.config['JSON_AS_ASCII'] = config.JSON_AS_ASCII
-app.config['JSONIFY_MIMETYPE'] = config.JSONIFY_MIMETYPE
-app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
-app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY
-app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE
-app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
-app.config['SESSION_COOKIE_NAME'] = config.SESSION_COOKIE_NAME
-app.config['SESSION_REFRESH_EACH_REQUEST'] = config.SESSION_REFRESH_EACH_REQUEST
-Session(app)
-
-# ProxyFix für korrekte IP-Adressen hinter Nginx
-app.wsgi_app = ProxyFix(
-    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
-)
-
-# Configuration is now loaded from config module
-
-# Scheduler für automatische Backups
-scheduler = BackgroundScheduler()
-scheduler.start()
-
-# Logging konfigurieren
-logging.basicConfig(level=logging.INFO)
-
-
-# Login decorator
-def login_required(f):
-    @wraps(f)
-    def decorated_function(*args, **kwargs):
-        if 'logged_in' not in session:
-            return redirect(url_for('login'))
-        
-        # Prüfe ob Session abgelaufen ist
-        if 'last_activity' in session:
-            last_activity = datetime.fromisoformat(session['last_activity'])
-            time_since_activity = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) - last_activity
-            
-            # Debug-Logging
-            app.logger.info(f"Session check for {session.get('username', 'unknown')}: "
-                          f"Last activity: {last_activity}, "
-                          f"Time since: {time_since_activity.total_seconds()} seconds")
-            
-            if time_since_activity > timedelta(minutes=5):
-                # Session abgelaufen - Logout
-                username = session.get('username', 'unbekannt')
-                app.logger.info(f"Session timeout for user {username} - auto logout")
-                # Audit-Log für automatischen Logout (vor session.clear()!)
-                try:
-                    log_audit('AUTO_LOGOUT', 'session', additional_info={'reason': 'Session timeout (5 minutes)', 'username': username})
-                except:
-                    pass
-                session.clear()
-                flash('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.', 'warning')
-                return redirect(url_for('login'))
-        
-        # Aktivität NICHT automatisch aktualisieren
-        # Nur bei expliziten Benutzeraktionen (wird vom Heartbeat gemacht)
-        return f(*args, **kwargs)
-    return decorated_function
-
-# DB-Verbindung mit UTF-8 Encoding
-def get_connection():
-    conn = psycopg2.connect(
-        host=os.getenv("POSTGRES_HOST", "postgres"),
-        port=os.getenv("POSTGRES_PORT", "5432"),
-        dbname=os.getenv("POSTGRES_DB"),
-        user=os.getenv("POSTGRES_USER"),
-        password=os.getenv("POSTGRES_PASSWORD"),
-        options='-c client_encoding=UTF8'
-    )
-    conn.set_client_encoding('UTF8')
-    return conn
-
-# User Authentication Helper Functions
-def hash_password(password):
-    """Hash a password using bcrypt"""
-    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
-
-def verify_password(password, hashed):
-    """Verify a password against its hash"""
-    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
-
-def get_user_by_username(username):
-    """Get user from database by username"""
-    conn = get_connection()
-    cur = conn.cursor()
-    try:
-        cur.execute("""
-            SELECT id, username, password_hash, email, totp_secret, totp_enabled, 
-                   backup_codes, last_password_change, failed_2fa_attempts
-            FROM users WHERE username = %s
-        """, (username,))
-        user = cur.fetchone()
-        if user:
-            return {
-                'id': user[0],
-                'username': user[1],
-                'password_hash': user[2],
-                'email': user[3],
-                'totp_secret': user[4],
-                'totp_enabled': user[5],
-                'backup_codes': user[6],
-                'last_password_change': user[7],
-                'failed_2fa_attempts': user[8]
-            }
-        return None
-    finally:
-        cur.close()
-        conn.close()
-
-def generate_totp_secret():
-    """Generate a new TOTP secret"""
-    return pyotp.random_base32()
-
-def generate_qr_code(username, totp_secret):
-    """Generate QR code for TOTP setup"""
-    totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
-        name=username,
-        issuer_name='V2 Admin Panel'
-    )
-    
-    qr = qrcode.QRCode(version=1, box_size=10, border=5)
-    qr.add_data(totp_uri)
-    qr.make(fit=True)
-    
-    img = qr.make_image(fill_color="black", back_color="white")
-    buf = BytesIO()
-    img.save(buf, format='PNG')
-    buf.seek(0)
-    
-    return base64.b64encode(buf.getvalue()).decode()
-
-def verify_totp(totp_secret, token):
-    """Verify a TOTP token"""
-    totp = pyotp.TOTP(totp_secret)
-    return totp.verify(token, valid_window=1)
-
-def generate_backup_codes(count=8):
-    """Generate backup codes for 2FA recovery"""
-    codes = []
-    for _ in range(count):
-        code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
-        codes.append(code)
-    return codes
-
-def hash_backup_code(code):
-    """Hash a backup code for storage"""
-    return hashlib.sha256(code.encode()).hexdigest()
-
-def verify_backup_code(code, hashed_codes):
-    """Verify a backup code against stored hashes"""
-    code_hash = hashlib.sha256(code.encode()).hexdigest()
-    return code_hash in hashed_codes
-
-# Audit-Log-Funktion
-def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=None, additional_info=None):
-    """Protokolliert Änderungen im Audit-Log"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        username = session.get('username', 'system')
-        ip_address = get_client_ip() if request else None
-        user_agent = request.headers.get('User-Agent') if request else None
-        
-        # Debug logging
-        app.logger.info(f"Audit log - IP address captured: {ip_address}, Action: {action}, User: {username}")
-        
-        # Konvertiere Dictionaries zu JSONB
-        old_json = Json(old_values) if old_values else None
-        new_json = Json(new_values) if new_values else None
-        
-        cur.execute("""
-            INSERT INTO audit_log 
-            (username, action, entity_type, entity_id, old_values, new_values, 
-             ip_address, user_agent, additional_info)
-            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
-        """, (username, action, entity_type, entity_id, old_json, new_json, 
-              ip_address, user_agent, additional_info))
-        
-        conn.commit()
-    except Exception as e:
-        print(f"Audit log error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-
-# Verschlüsselungs-Funktionen
-def get_or_create_encryption_key():
-    """Holt oder erstellt einen Verschlüsselungsschlüssel"""
-    key_file = BACKUP_DIR / ".backup_key"
-    
-    # Versuche Key aus Umgebungsvariable zu lesen
-    env_key = os.getenv("BACKUP_ENCRYPTION_KEY")
-    if env_key:
-        try:
-            # Validiere den Key
-            Fernet(env_key.encode())
-            return env_key.encode()
-        except:
-            pass
-    
-    # Wenn kein gültiger Key in ENV, prüfe Datei
-    if key_file.exists():
-        return key_file.read_bytes()
-    
-    # Erstelle neuen Key
-    key = Fernet.generate_key()
-    key_file.write_bytes(key)
-    logging.info("Neuer Backup-Verschlüsselungsschlüssel erstellt")
-    return key
-
-# Backup-Funktionen
-def create_backup(backup_type="manual", created_by=None):
-    """Erstellt ein verschlüsseltes Backup der Datenbank"""
-    start_time = time.time()
-    timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")
-    filename = f"backup_v2docker_{timestamp}_encrypted.sql.gz.enc"
-    filepath = BACKUP_DIR / filename
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Backup-Eintrag erstellen
-    cur.execute("""
-        INSERT INTO backup_history 
-        (filename, filepath, backup_type, status, created_by, is_encrypted)
-        VALUES (%s, %s, %s, %s, %s, %s)
-        RETURNING id
-    """, (filename, str(filepath), backup_type, 'in_progress', 
-          created_by or 'system', True))
-    backup_id = cur.fetchone()[0]
-    conn.commit()
-    
-    try:
-        # PostgreSQL Dump erstellen
-        dump_command = [
-            'pg_dump',
-            '-h', os.getenv("POSTGRES_HOST", "postgres"),
-            '-p', os.getenv("POSTGRES_PORT", "5432"),
-            '-U', os.getenv("POSTGRES_USER"),
-            '-d', os.getenv("POSTGRES_DB"),
-            '--no-password',
-            '--verbose'
-        ]
-        
-        # PGPASSWORD setzen
-        env = os.environ.copy()
-        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
-        
-        # Dump ausführen
-        result = subprocess.run(dump_command, capture_output=True, text=True, env=env)
-        
-        if result.returncode != 0:
-            raise Exception(f"pg_dump failed: {result.stderr}")
-        
-        dump_data = result.stdout.encode('utf-8')
-        
-        # Komprimieren
-        compressed_data = gzip.compress(dump_data)
-        
-        # Verschlüsseln
-        key = get_or_create_encryption_key()
-        f = Fernet(key)
-        encrypted_data = f.encrypt(compressed_data)
-        
-        # Speichern
-        filepath.write_bytes(encrypted_data)
-        
-        # Statistiken sammeln
-        cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'")
-        tables_count = cur.fetchone()[0]
-        
-        cur.execute("""
-            SELECT SUM(n_live_tup) 
-            FROM pg_stat_user_tables
-        """)
-        records_count = cur.fetchone()[0] or 0
-        
-        duration = time.time() - start_time
-        filesize = filepath.stat().st_size
-        
-        # Backup-Eintrag aktualisieren
-        cur.execute("""
-            UPDATE backup_history 
-            SET status = %s, filesize = %s, tables_count = %s, 
-                records_count = %s, duration_seconds = %s
-            WHERE id = %s
-        """, ('success', filesize, tables_count, records_count, duration, backup_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('BACKUP', 'database', backup_id,
-                 additional_info=f"Backup erstellt: {filename} ({filesize} bytes)")
-        
-        # E-Mail-Benachrichtigung (wenn konfiguriert)
-        send_backup_notification(True, filename, filesize, duration)
-        
-        logging.info(f"Backup erfolgreich erstellt: {filename}")
-        return True, filename
-        
-    except Exception as e:
-        # Fehler protokollieren
-        cur.execute("""
-            UPDATE backup_history 
-            SET status = %s, error_message = %s, duration_seconds = %s
-            WHERE id = %s
-        """, ('failed', str(e), time.time() - start_time, backup_id))
-        conn.commit()
-        
-        logging.error(f"Backup fehlgeschlagen: {e}")
-        send_backup_notification(False, filename, error=str(e))
-        
-        return False, str(e)
-    
-    finally:
-        cur.close()
-        conn.close()
-
-def restore_backup(backup_id, encryption_key=None):
-    """Stellt ein Backup wieder her"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Backup-Info abrufen
-        cur.execute("""
-            SELECT filename, filepath, is_encrypted 
-            FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        backup_info = cur.fetchone()
-        
-        if not backup_info:
-            raise Exception("Backup nicht gefunden")
-        
-        filename, filepath, is_encrypted = backup_info
-        filepath = Path(filepath)
-        
-        if not filepath.exists():
-            raise Exception("Backup-Datei nicht gefunden")
-        
-        # Datei lesen
-        encrypted_data = filepath.read_bytes()
-        
-        # Entschlüsseln
-        if is_encrypted:
-            key = encryption_key.encode() if encryption_key else get_or_create_encryption_key()
-            try:
-                f = Fernet(key)
-                compressed_data = f.decrypt(encrypted_data)
-            except:
-                raise Exception("Entschlüsselung fehlgeschlagen. Falsches Passwort?")
-        else:
-            compressed_data = encrypted_data
-        
-        # Dekomprimieren
-        dump_data = gzip.decompress(compressed_data)
-        sql_commands = dump_data.decode('utf-8')
-        
-        # Bestehende Verbindungen schließen
-        cur.close()
-        conn.close()
-        
-        # Datenbank wiederherstellen
-        restore_command = [
-            'psql',
-            '-h', os.getenv("POSTGRES_HOST", "postgres"),
-            '-p', os.getenv("POSTGRES_PORT", "5432"),
-            '-U', os.getenv("POSTGRES_USER"),
-            '-d', os.getenv("POSTGRES_DB"),
-            '--no-password'
-        ]
-        
-        env = os.environ.copy()
-        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
-        
-        result = subprocess.run(restore_command, input=sql_commands, 
-                              capture_output=True, text=True, env=env)
-        
-        if result.returncode != 0:
-            raise Exception(f"Wiederherstellung fehlgeschlagen: {result.stderr}")
-        
-        # Audit-Log (neue Verbindung)
-        log_audit('RESTORE', 'database', backup_id,
-                 additional_info=f"Backup wiederhergestellt: {filename}")
-        
-        return True, "Backup erfolgreich wiederhergestellt"
-        
-    except Exception as e:
-        logging.error(f"Wiederherstellung fehlgeschlagen: {e}")
-        return False, str(e)
-
-def send_backup_notification(success, filename, filesize=None, duration=None, error=None):
-    """Sendet E-Mail-Benachrichtigung (wenn konfiguriert)"""
-    if not os.getenv("EMAIL_ENABLED", "false").lower() == "true":
-        return
-    
-    # E-Mail-Funktion vorbereitet aber deaktiviert
-    # TODO: Implementieren wenn E-Mail-Server konfiguriert ist
-    logging.info(f"E-Mail-Benachrichtigung vorbereitet: Backup {'erfolgreich' if success else 'fehlgeschlagen'}")
-
-# Scheduled Backup Job
-def scheduled_backup():
-    """Führt ein geplantes Backup aus"""
-    logging.info("Starte geplantes Backup...")
-    create_backup(backup_type="scheduled", created_by="scheduler")
-
-# Scheduler konfigurieren - täglich um 3:00 Uhr
-scheduler.add_job(
-    scheduled_backup,
-    'cron',
-    hour=3,
-    minute=0,
-    id='daily_backup',
-    replace_existing=True
-)
-
-# Rate-Limiting Funktionen
-def get_client_ip():
-    """Ermittelt die echte IP-Adresse des Clients"""
-    # Debug logging
-    app.logger.info(f"Headers - X-Real-IP: {request.headers.get('X-Real-IP')}, X-Forwarded-For: {request.headers.get('X-Forwarded-For')}, Remote-Addr: {request.remote_addr}")
-    
-    # Try X-Real-IP first (set by nginx)
-    if request.headers.get('X-Real-IP'):
-        return request.headers.get('X-Real-IP')
-    # Then X-Forwarded-For
-    elif request.headers.get('X-Forwarded-For'):
-        # X-Forwarded-For can contain multiple IPs, take the first one
-        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
-    # Fallback to remote_addr
-    else:
-        return request.remote_addr
-
-def check_ip_blocked(ip_address):
-    """Prüft ob eine IP-Adresse gesperrt ist"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT blocked_until FROM login_attempts
-        WHERE ip_address = %s AND blocked_until IS NOT NULL
-    """, (ip_address,))
-    
-    result = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    if result and result[0]:
-        if result[0] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None):
-            return True, result[0]
-    return False, None
-
-def record_failed_attempt(ip_address, username):
-    """Zeichnet einen fehlgeschlagenen Login-Versuch auf"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Random Fehlermeldung
-    error_message = random.choice(FAIL_MESSAGES)
-    
-    try:
-        # Prüfen ob IP bereits existiert
-        cur.execute("""
-            SELECT attempt_count FROM login_attempts
-            WHERE ip_address = %s
-        """, (ip_address,))
-        
-        result = cur.fetchone()
-        
-        if result:
-            # Update bestehenden Eintrag
-            new_count = result[0] + 1
-            blocked_until = None
-            
-            if new_count >= MAX_LOGIN_ATTEMPTS:
-                blocked_until = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) + timedelta(hours=BLOCK_DURATION_HOURS)
-                # E-Mail-Benachrichtigung (wenn aktiviert)
-                if os.getenv("EMAIL_ENABLED", "false").lower() == "true":
-                    send_security_alert_email(ip_address, username, new_count)
-            
-            cur.execute("""
-                UPDATE login_attempts
-                SET attempt_count = %s,
-                    last_attempt = CURRENT_TIMESTAMP,
-                    blocked_until = %s,
-                    last_username_tried = %s,
-                    last_error_message = %s
-                WHERE ip_address = %s
-            """, (new_count, blocked_until, username, error_message, ip_address))
-        else:
-            # Neuen Eintrag erstellen
-            cur.execute("""
-                INSERT INTO login_attempts 
-                (ip_address, attempt_count, last_username_tried, last_error_message)
-                VALUES (%s, 1, %s, %s)
-            """, (ip_address, username, error_message))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('LOGIN_FAILED', 'user', 
-                 additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
-        
-    except Exception as e:
-        print(f"Rate limiting error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-    
-    return error_message
-
-def reset_login_attempts(ip_address):
-    """Setzt die Login-Versuche für eine IP zurück"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        cur.execute("""
-            DELETE FROM login_attempts
-            WHERE ip_address = %s
-        """, (ip_address,))
-        conn.commit()
-    except Exception as e:
-        print(f"Reset attempts error: {e}")
-        conn.rollback()
-    finally:
-        cur.close()
-        conn.close()
-
-def get_login_attempts(ip_address):
-    """Gibt die Anzahl der Login-Versuche für eine IP zurück"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT attempt_count FROM login_attempts
-        WHERE ip_address = %s
-    """, (ip_address,))
-    
-    result = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    return result[0] if result else 0
-
-def send_security_alert_email(ip_address, username, attempt_count):
-    """Sendet eine Sicherheitswarnung per E-Mail"""
-    subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
-    body = f"""
-    WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
-    
-    IP-Adresse: {ip_address}
-    Versuchter Benutzername: {username}
-    Anzahl Versuche: {attempt_count}
-    Zeit: {datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d %H:%M:%S')}
-    
-    Die IP-Adresse wurde für 24 Stunden gesperrt.
-    
-    Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
-    """
-    
-    # TODO: E-Mail-Versand implementieren wenn SMTP konfiguriert
-    logging.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
-    print(f"E-Mail würde gesendet: {subject}")
-
-def verify_recaptcha(response):
-    """Verifiziert die reCAPTCHA v2 Response mit Google"""
-    secret_key = os.getenv('RECAPTCHA_SECRET_KEY')
-    
-    # Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC)
-    if not secret_key:
-        logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen")
-        return True
-    
-    # Verifizierung bei Google
-    try:
-        verify_url = 'https://www.google.com/recaptcha/api/siteverify'
-        data = {
-            'secret': secret_key,
-            'response': response
-        }
-        
-        # Timeout für Request setzen
-        r = requests.post(verify_url, data=data, timeout=5)
-        result = r.json()
-        
-        # Log für Debugging
-        if not result.get('success'):
-            logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}")
-        
-        return result.get('success', False)
-    
-    except requests.exceptions.RequestException as e:
-        logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}")
-        # Bei Netzwerkfehlern CAPTCHA als bestanden werten
-        return True
-    except Exception as e:
-        logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
-        return False
-
-def generate_license_key(license_type='full'):
-    """
-    Generiert einen Lizenzschlüssel im Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ
-    
-    AF = Account Factory (Produktkennung)
-    F/T = F für Fullversion, T für Testversion
-    YYYY = Jahr
-    MM = Monat
-    XXXX-YYYY-ZZZZ = Zufällige alphanumerische Zeichen
-    """
-    # Erlaubte Zeichen (ohne verwirrende wie 0/O, 1/I/l)
-    chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
-    
-    # Datum-Teil
-    now = datetime.now(ZoneInfo("Europe/Berlin"))
-    date_part = now.strftime('%Y%m')
-    type_char = 'F' if license_type == 'full' else 'T'
-    
-    # Zufällige Teile generieren (3 Blöcke à 4 Zeichen)
-    parts = []
-    for _ in range(3):
-        part = ''.join(secrets.choice(chars) for _ in range(4))
-        parts.append(part)
-    
-    # Key zusammensetzen
-    key = f"AF-{type_char}-{date_part}-{parts[0]}-{parts[1]}-{parts[2]}"
-    
-    return key
-
-def validate_license_key(key):
-    """
-    Validiert das License Key Format
-    Erwartet: AF-F-YYYYMM-XXXX-YYYY-ZZZZ oder AF-T-YYYYMM-XXXX-YYYY-ZZZZ
-    """
-    if not key:
-        return False
-    
-    # Pattern für das neue Format
-    # AF- (fest) + F oder T + - + 6 Ziffern (YYYYMM) + - + 4 Zeichen + - + 4 Zeichen + - + 4 Zeichen
-    pattern = r'^AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
-    
-    # Großbuchstaben für Vergleich
-    return bool(re.match(pattern, key.upper()))
-
-@app.route("/login", methods=["GET", "POST"])
-def login():
-    # Timing-Attack Schutz - Start Zeit merken
-    start_time = time.time()
-    
-    # IP-Adresse ermitteln
-    ip_address = get_client_ip()
-    
-    # Prüfen ob IP gesperrt ist
-    is_blocked, blocked_until = check_ip_blocked(ip_address)
-    if is_blocked:
-        time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
-        error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
-        return render_template("login.html", error=error_msg, error_type="blocked")
-    
-    # Anzahl bisheriger Versuche
-    attempt_count = get_login_attempts(ip_address)
-    
-    if request.method == "POST":
-        username = request.form.get("username")
-        password = request.form.get("password")
-        captcha_response = request.form.get("g-recaptcha-response")
-        
-        # CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
-        recaptcha_site_key = os.getenv('RECAPTCHA_SITE_KEY')
-        if attempt_count >= CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
-            if not captcha_response:
-                # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA ERFORDERLICH!", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
-            
-            # CAPTCHA validieren
-            if not verify_recaptcha(captcha_response):
-                # Timing-Attack Schutz
-                elapsed = time.time() - start_time
-                if elapsed < 1.0:
-                    time.sleep(1.0 - elapsed)
-                return render_template("login.html", 
-                                     error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
-                                     show_captcha=True,
-                                     error_type="captcha",
-                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                                     recaptcha_site_key=recaptcha_site_key)
-        
-        # Check user in database first, fallback to env vars
-        user = get_user_by_username(username)
-        login_success = False
-        needs_2fa = False
-        
-        if user:
-            # Database user authentication
-            if verify_password(password, user['password_hash']):
-                login_success = True
-                needs_2fa = user['totp_enabled']
-        else:
-            # Fallback to environment variables for backward compatibility
-            admin1_user = os.getenv("ADMIN1_USERNAME")
-            admin1_pass = os.getenv("ADMIN1_PASSWORD")
-            admin2_user = os.getenv("ADMIN2_USERNAME")
-            admin2_pass = os.getenv("ADMIN2_PASSWORD")
-            
-            if ((username == admin1_user and password == admin1_pass) or 
-                (username == admin2_user and password == admin2_pass)):
-                login_success = True
-        
-        # Timing-Attack Schutz - Mindestens 1 Sekunde warten
-        elapsed = time.time() - start_time
-        if elapsed < 1.0:
-            time.sleep(1.0 - elapsed)
-        
-        if login_success:
-            # Erfolgreicher Login
-            if needs_2fa:
-                # Store temporary session for 2FA verification
-                session['temp_username'] = username
-                session['temp_user_id'] = user['id']
-                session['awaiting_2fa'] = True
-                return redirect(url_for('verify_2fa'))
-            else:
-                # Complete login without 2FA
-                session.permanent = True  # Aktiviert das Timeout
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user['id'] if user else None
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                reset_login_attempts(ip_address)
-                log_audit('LOGIN_SUCCESS', 'user', 
-                         additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
-                return redirect(url_for('dashboard'))
-        else:
-            # Fehlgeschlagener Login
-            error_message = record_failed_attempt(ip_address, username)
-            new_attempt_count = get_login_attempts(ip_address)
-            
-            # Prüfen ob jetzt gesperrt
-            is_now_blocked, _ = check_ip_blocked(ip_address)
-            if is_now_blocked:
-                log_audit('LOGIN_BLOCKED', 'security', 
-                         additional_info=f"IP {ip_address} wurde nach {MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
-            
-            return render_template("login.html", 
-                                 error=error_message,
-                                 show_captcha=(new_attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
-                                 error_type="failed",
-                                 attempts_left=max(0, MAX_LOGIN_ATTEMPTS - new_attempt_count),
-                                 recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
-    
-    # GET Request
-    return render_template("login.html", 
-                         show_captcha=(attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
-                         attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
-                         recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
-
-@app.route("/logout")
-def logout():
-    username = session.get('username', 'unknown')
-    log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
-    session.pop('logged_in', None)
-    session.pop('username', None)
-    session.pop('user_id', None)
-    session.pop('temp_username', None)
-    session.pop('temp_user_id', None)
-    session.pop('awaiting_2fa', None)
-    return redirect(url_for('login'))
-
-@app.route("/verify-2fa", methods=["GET", "POST"])
-def verify_2fa():
-    if not session.get('awaiting_2fa'):
-        return redirect(url_for('login'))
-    
-    if request.method == "POST":
-        token = request.form.get('token', '').replace(' ', '')
-        username = session.get('temp_username')
-        user_id = session.get('temp_user_id')
-        
-        if not username or not user_id:
-            flash('Session expired. Please login again.', 'error')
-            return redirect(url_for('login'))
-        
-        user = get_user_by_username(username)
-        if not user:
-            flash('User not found.', 'error')
-            return redirect(url_for('login'))
-        
-        # Check if it's a backup code
-        if len(token) == 8 and token.isupper():
-            # Try backup code
-            backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
-            if verify_backup_code(token, backup_codes):
-                # Remove used backup code
-                code_hash = hash_backup_code(token)
-                backup_codes.remove(code_hash)
-                
-                conn = get_connection()
-                cur = conn.cursor()
-                cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
-                          (json.dumps(backup_codes), user_id))
-                conn.commit()
-                cur.close()
-                conn.close()
-                
-                # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
-                
-                flash('Login successful using backup code. Please generate new backup codes.', 'warning')
-                log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
-                return redirect(url_for('dashboard'))
-        else:
-            # Try TOTP token
-            if verify_totp(user['totp_secret'], token):
-                # Complete login
-                session.permanent = True
-                session['logged_in'] = True
-                session['username'] = username
-                session['user_id'] = user_id
-                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-                session.pop('temp_username', None)
-                session.pop('temp_user_id', None)
-                session.pop('awaiting_2fa', None)
-                
-                log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
-                return redirect(url_for('dashboard'))
-        
-        # Failed verification
-        conn = get_connection()
-        cur = conn.cursor()
-        cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
-                  (datetime.now(), user_id))
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        flash('Invalid authentication code. Please try again.', 'error')
-        log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
-    
-    return render_template('verify_2fa.html')
-
-@app.route("/profile")
-@login_required
-def profile():
-    user = get_user_by_username(session['username'])
-    if not user:
-        # For environment-based users, redirect with message
-        flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info')
-        return redirect(url_for('dashboard'))
-    return render_template('profile.html', user=user)
-
-@app.route("/profile/change-password", methods=["POST"])
-@login_required
-def change_password():
-    current_password = request.form.get('current_password')
-    new_password = request.form.get('new_password')
-    confirm_password = request.form.get('confirm_password')
-    
-    user = get_user_by_username(session['username'])
-    
-    # Verify current password
-    if not verify_password(current_password, user['password_hash']):
-        flash('Current password is incorrect.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Check new password
-    if new_password != confirm_password:
-        flash('New passwords do not match.', 'error')
-        return redirect(url_for('profile'))
-    
-    if len(new_password) < 8:
-        flash('Password must be at least 8 characters long.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Update password
-    new_hash = hash_password(new_password)
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s",
-              (new_hash, datetime.now(), user['id']))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], 
-             additional_info="Password changed successfully")
-    flash('Password changed successfully.', 'success')
-    return redirect(url_for('profile'))
-
-@app.route("/profile/setup-2fa")
-@login_required
-def setup_2fa():
-    user = get_user_by_username(session['username'])
-    
-    if user['totp_enabled']:
-        flash('2FA is already enabled for your account.', 'info')
-        return redirect(url_for('profile'))
-    
-    # Generate new TOTP secret
-    totp_secret = generate_totp_secret()
-    session['temp_totp_secret'] = totp_secret
-    
-    # Generate QR code
-    qr_code = generate_qr_code(user['username'], totp_secret)
-    
-    return render_template('setup_2fa.html', 
-                         totp_secret=totp_secret, 
-                         qr_code=qr_code)
-
-@app.route("/profile/enable-2fa", methods=["POST"])
-@login_required
-def enable_2fa():
-    token = request.form.get('token', '').replace(' ', '')
-    totp_secret = session.get('temp_totp_secret')
-    
-    if not totp_secret:
-        flash('2FA setup session expired. Please try again.', 'error')
-        return redirect(url_for('setup_2fa'))
-    
-    # Verify the token
-    if not verify_totp(totp_secret, token):
-        flash('Invalid authentication code. Please try again.', 'error')
-        return redirect(url_for('setup_2fa'))
-    
-    # Generate backup codes
-    backup_codes = generate_backup_codes()
-    hashed_codes = [hash_backup_code(code) for code in backup_codes]
-    
-    # Enable 2FA
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("""
-        UPDATE users 
-        SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s 
-        WHERE username = %s
-    """, (totp_secret, json.dumps(hashed_codes), session['username']))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    session.pop('temp_totp_secret', None)
-    
-    log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully")
-    
-    # Show backup codes
-    return render_template('backup_codes.html', backup_codes=backup_codes)
-
-@app.route("/profile/disable-2fa", methods=["POST"])
-@login_required
-def disable_2fa():
-    password = request.form.get('password')
-    user = get_user_by_username(session['username'])
-    
-    # Verify password
-    if not verify_password(password, user['password_hash']):
-        flash('Incorrect password.', 'error')
-        return redirect(url_for('profile'))
-    
-    # Disable 2FA
-    conn = get_connection()
-    cur = conn.cursor()
-    cur.execute("""
-        UPDATE users 
-        SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL 
-        WHERE username = %s
-    """, (session['username'],))
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled")
-    flash('2FA has been disabled for your account.', 'success')
-    return redirect(url_for('profile'))
-
-@app.route("/heartbeat", methods=['POST'])
-@login_required
-def heartbeat():
-    """Endpoint für Session Keep-Alive - aktualisiert last_activity"""
-    # Aktualisiere last_activity nur wenn explizit angefordert
-    session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-    # Force session save
-    session.modified = True
-    
-    return jsonify({
-        'status': 'ok',
-        'last_activity': session['last_activity'],
-        'username': session.get('username')
-    })
-
-@app.route("/api/generate-license-key", methods=['POST'])
-@login_required
-def api_generate_key():
-    """API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
-    try:
-        # Lizenztyp aus Request holen (default: full)
-        data = request.get_json() or {}
-        license_type = data.get('type', 'full')
-        
-        # Key generieren
-        key = generate_license_key(license_type)
-        
-        # Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Wiederhole bis eindeutiger Key gefunden
-        attempts = 0
-        while attempts < 10:  # Max 10 Versuche
-            cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
-            if not cur.fetchone():
-                break  # Key ist eindeutig
-            key = generate_license_key(license_type)
-            attempts += 1
-        
-        cur.close()
-        conn.close()
-        
-        # Log für Audit
-        log_audit('GENERATE_KEY', 'license', 
-                 additional_info={'type': license_type, 'key': key})
-        
-        return jsonify({
-            'success': True,
-            'key': key,
-            'type': license_type
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Key-Generierung: {str(e)}")
-        return jsonify({
-            'success': False,
-            'error': 'Fehler bei der Key-Generierung'
-        }), 500
-
-@app.route("/api/customers", methods=['GET'])
-@login_required
-def api_customers():
-    """API Endpoint für die Kundensuche mit Select2"""
-    try:
-        # Suchparameter
-        search = request.args.get('q', '').strip()
-        page = request.args.get('page', 1, type=int)
-        per_page = 20
-        customer_id = request.args.get('id', type=int)
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Einzelnen Kunden per ID abrufen
-        if customer_id:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                WHERE c.id = %s
-                GROUP BY c.id, c.name, c.email
-            """, (customer_id,))
-            
-            customer = cur.fetchone()
-            results = []
-            if customer:
-                results.append({
-                    'id': customer[0],
-                    'text': f"{customer[1]} ({customer[2]})",
-                    'name': customer[1],
-                    'email': customer[2],
-                    'license_count': customer[3]
-                })
-            
-            cur.close()
-            conn.close()
-            
-            return jsonify({
-                'results': results,
-                'pagination': {'more': False}
-            })
-        
-        # SQL Query mit optionaler Suche
-        elif search:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                WHERE LOWER(c.name) LIKE LOWER(%s)
-                   OR LOWER(c.email) LIKE LOWER(%s)
-                GROUP BY c.id, c.name, c.email
-                ORDER BY c.name
-                LIMIT %s OFFSET %s
-            """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
-        else:
-            cur.execute("""
-                SELECT c.id, c.name, c.email,
-                       COUNT(l.id) as license_count
-                FROM customers c
-                LEFT JOIN licenses l ON c.id = l.customer_id
-                GROUP BY c.id, c.name, c.email
-                ORDER BY c.name
-                LIMIT %s OFFSET %s
-            """, (per_page, (page - 1) * per_page))
-        
-        customers = cur.fetchall()
-        
-        # Format für Select2
-        results = []
-        for customer in customers:
-            results.append({
-                'id': customer[0],
-                'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
-                'name': customer[1],
-                'email': customer[2],
-                'license_count': customer[3]
-            })
-        
-        # Gesamtanzahl für Pagination
-        if search:
-            cur.execute("""
-                SELECT COUNT(*) FROM customers
-                WHERE LOWER(name) LIKE LOWER(%s)
-                   OR LOWER(email) LIKE LOWER(%s)
-            """, (f'%{search}%', f'%{search}%'))
-        else:
-            cur.execute("SELECT COUNT(*) FROM customers")
-        
-        total_count = cur.fetchone()[0]
-        
-        cur.close()
-        conn.close()
-        
-        # Select2 Response Format
-        return jsonify({
-            'results': results,
-            'pagination': {
-                'more': (page * per_page) < total_count
-            }
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Kundensuche: {str(e)}")
-        return jsonify({
-            'results': [],
-            'error': 'Fehler bei der Kundensuche'
-        }), 500
-
-@app.route("/")
-@login_required
-def dashboard():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Statistiken abrufen
-    # Gesamtanzahl Kunden (ohne Testdaten)
-    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE")
-    total_customers = cur.fetchone()[0]
-    
-    # Gesamtanzahl Lizenzen (ohne Testdaten)
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE")
-    total_licenses = cur.fetchone()[0]
-    
-    # Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE
-    """)
-    active_licenses = cur.fetchone()[0]
-    
-    # Aktive Sessions
-    cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
-    active_sessions_count = cur.fetchone()[0]
-    
-    # Abgelaufene Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until < CURRENT_DATE AND is_test = FALSE
-    """)
-    expired_licenses = cur.fetchone()[0]
-    
-    # Deaktivierte Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE is_active = FALSE AND is_test = FALSE
-    """)
-    inactive_licenses = cur.fetchone()[0]
-    
-    # Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten)
-    cur.execute("""
-        SELECT COUNT(*) FROM licenses 
-        WHERE valid_until >= CURRENT_DATE 
-        AND valid_until < CURRENT_DATE + INTERVAL '30 days'
-        AND is_active = TRUE
-        AND is_test = FALSE
-    """)
-    expiring_soon = cur.fetchone()[0]
-    
-    # Testlizenzen vs Vollversionen (ohne Testdaten)
-    cur.execute("""
-        SELECT license_type, COUNT(*) 
-        FROM licenses 
-        WHERE is_test = FALSE
-        GROUP BY license_type
-    """)
-    license_types = dict(cur.fetchall())
-    
-    # Anzahl Testdaten
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE")
-    test_data_count = cur.fetchone()[0]
-    
-    # Anzahl Test-Kunden
-    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE")
-    test_customers_count = cur.fetchone()[0]
-    
-    # Anzahl Test-Ressourcen
-    cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE")
-    test_resources_count = cur.fetchone()[0]
-    
-    # Letzte 5 erstellten Lizenzen (ohne Testdaten)
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, l.valid_until,
-               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 as status
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.is_test = FALSE
-        ORDER BY l.id DESC
-        LIMIT 5
-    """)
-    recent_licenses = cur.fetchall()
-    
-    # Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten)
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, l.valid_until,
-               l.valid_until - CURRENT_DATE as days_left
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.valid_until >= CURRENT_DATE 
-        AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
-        AND l.is_active = TRUE
-        AND l.is_test = FALSE
-        ORDER BY l.valid_until
-        LIMIT 10
-    """)
-    expiring_licenses = cur.fetchall()
-    
-    # Letztes Backup
-    cur.execute("""
-        SELECT created_at, filesize, duration_seconds, backup_type, status
-        FROM backup_history 
-        ORDER BY created_at DESC 
-        LIMIT 1
-    """)
-    last_backup_info = cur.fetchone()
-    
-    # Sicherheitsstatistiken
-    # Gesperrte IPs
-    cur.execute("""
-        SELECT COUNT(*) FROM login_attempts 
-        WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP
-    """)
-    blocked_ips_count = cur.fetchone()[0]
-    
-    # Fehlversuche heute
-    cur.execute("""
-        SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts
-        WHERE last_attempt::date = CURRENT_DATE
-    """)
-    failed_attempts_today = cur.fetchone()[0]
-    
-    # Letzte 5 Sicherheitsereignisse
-    cur.execute("""
-        SELECT 
-            la.ip_address, 
-            la.attempt_count, 
-            la.last_attempt, 
-            la.blocked_until,
-            la.last_username_tried,
-            la.last_error_message
-        FROM login_attempts la
-        ORDER BY la.last_attempt DESC
-        LIMIT 5
-    """)
-    recent_security_events = []
-    for event in cur.fetchall():
-        recent_security_events.append({
-            'ip_address': event[0],
-            'attempt_count': event[1],
-            'last_attempt': event[2].strftime('%d.%m %H:%M'),
-            'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) else None,
-            'username_tried': event[4],
-            'error_message': event[5]
-        })
-    
-    # Sicherheitslevel berechnen
-    if blocked_ips_count > 5 or failed_attempts_today > 50:
-        security_level = 'danger'
-        security_level_text = 'KRITISCH'
-    elif blocked_ips_count > 2 or failed_attempts_today > 20:
-        security_level = 'warning'
-        security_level_text = 'ERHÖHT'
-    else:
-        security_level = 'success'
-        security_level_text = 'NORMAL'
-    
-    # Resource Pool Statistiken (nur Live-Daten, keine Testdaten)
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'available') as available,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-            COUNT(*) as total
-        FROM resource_pools
-        WHERE is_test = FALSE
-        GROUP BY resource_type
-    """)
-    
-    resource_stats = {}
-    resource_warning = None
-    
-    for row in cur.fetchall():
-        available_percent = round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
-        resource_stats[row[0]] = {
-            'available': row[1],
-            'allocated': row[2],
-            'quarantine': row[3],
-            'total': row[4],
-            'available_percent': available_percent,
-            'warning_level': 'danger' if row[1] < 50 else 'warning' if row[1] < 100 else 'success'
-        }
-        
-        # Warnung bei niedrigem Bestand
-        if row[1] < 50:
-            if not resource_warning:
-                resource_warning = f"Niedriger Bestand bei {row[0].upper()}: nur noch {row[1]} verfügbar!"
-            else:
-                resource_warning += f" | {row[0].upper()}: {row[1]}"
-    
-    cur.close()
-    conn.close()
-    
-    stats = {
-        'total_customers': total_customers,
-        'total_licenses': total_licenses,
-        'active_licenses': active_licenses,
-        'expired_licenses': expired_licenses,
-        'inactive_licenses': inactive_licenses,
-        'expiring_soon': expiring_soon,
-        'full_licenses': license_types.get('full', 0),
-        'test_licenses': license_types.get('test', 0),
-        'test_data_count': test_data_count,
-        'test_customers_count': test_customers_count,
-        'test_resources_count': test_resources_count,
-        'recent_licenses': recent_licenses,
-        'expiring_licenses': expiring_licenses,
-        'active_sessions': active_sessions_count,
-        'last_backup': last_backup_info,
-        # Sicherheitsstatistiken
-        'blocked_ips_count': blocked_ips_count,
-        'failed_attempts_today': failed_attempts_today,
-        'recent_security_events': recent_security_events,
-        'security_level': security_level,
-        'security_level_text': security_level_text,
-        'resource_stats': resource_stats
-    }
-    
-    return render_template("dashboard.html", 
-                         stats=stats, 
-                         resource_stats=resource_stats,
-                         resource_warning=resource_warning,
-                         username=session.get('username'))
-
-@app.route("/create", methods=["GET", "POST"])
-@login_required
-def create_license():
-    if request.method == "POST":
-        customer_id = request.form.get("customer_id")
-        license_key = request.form["license_key"].upper()  # Immer Großbuchstaben
-        license_type = request.form["license_type"]
-        valid_from = request.form["valid_from"]
-        is_test = request.form.get("is_test") == "on"  # Checkbox value
-        
-        # Berechne valid_until basierend auf Laufzeit
-        duration = int(request.form.get("duration", 1))
-        duration_type = request.form.get("duration_type", "years")
-        
-        from datetime import datetime, timedelta
-        from dateutil.relativedelta import relativedelta
-        
-        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
-        
-        if duration_type == "days":
-            end_date = start_date + timedelta(days=duration)
-        elif duration_type == "months":
-            end_date = start_date + relativedelta(months=duration)
-        else:  # years
-            end_date = start_date + relativedelta(years=duration)
-        
-        # Ein Tag abziehen, da der Starttag mitgezählt wird
-        end_date = end_date - timedelta(days=1)
-        valid_until = end_date.strftime("%Y-%m-%d")
-
-        # Validiere License Key Format
-        if not validate_license_key(license_key):
-            flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error')
-            return redirect(url_for('create_license'))
-        
-        # Resource counts
-        domain_count = int(request.form.get("domain_count", 1))
-        ipv4_count = int(request.form.get("ipv4_count", 1))
-        phone_count = int(request.form.get("phone_count", 1))
-        device_limit = int(request.form.get("device_limit", 3))
-
-        conn = get_connection()
-        cur = conn.cursor()
-
-        try:
-            # Prüfe ob neuer Kunde oder bestehender
-            if customer_id == "new":
-                # Neuer Kunde
-                name = request.form.get("customer_name")
-                email = request.form.get("email")
-                
-                if not name:
-                    flash('Kundenname ist erforderlich!', 'error')
-                    return redirect(url_for('create_license'))
-                
-                # Prüfe ob E-Mail bereits existiert
-                if email:
-                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
-                    existing = cur.fetchone()
-                    if existing:
-                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
-                        return redirect(url_for('create_license'))
-                
-                # Kunde einfügen (erbt Test-Status von Lizenz)
-                cur.execute("""
-                    INSERT INTO customers (name, email, is_test, created_at)
-                    VALUES (%s, %s, %s, NOW())
-                    RETURNING id
-                """, (name, email, is_test))
-                customer_id = cur.fetchone()[0]
-                customer_info = {'name': name, 'email': email, 'is_test': is_test}
-                
-                # Audit-Log für neuen Kunden
-                log_audit('CREATE', 'customer', customer_id, 
-                         new_values={'name': name, 'email': email, 'is_test': is_test})
-            else:
-                # Bestehender Kunde - hole Infos für Audit-Log
-                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-                customer_data = cur.fetchone()
-                if not customer_data:
-                    flash('Kunde nicht gefunden!', 'error')
-                    return redirect(url_for('create_license'))
-                customer_info = {'name': customer_data[0], 'email': customer_data[1]}
-                
-                # Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
-                if customer_data[2]:  # is_test des Kunden
-                    is_test = True
-
-            # Lizenz hinzufügen
-            cur.execute("""
-                INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active, 
-                                    domain_count, ipv4_count, phone_count, device_limit, is_test)
-                VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
-                RETURNING id
-            """, (license_key, customer_id, license_type, valid_from, valid_until, 
-                  domain_count, ipv4_count, phone_count, device_limit, is_test))
-            license_id = cur.fetchone()[0]
-            
-            # Ressourcen zuweisen
-            try:
-                # Prüfe Verfügbarkeit
-                cur.execute("""
-                    SELECT 
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
-                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
-                """, (is_test, is_test, is_test))
-                available = cur.fetchone()
-                
-                if available[0] < domain_count:
-                    raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})")
-                if available[1] < ipv4_count:
-                    raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})")
-                if available[2] < phone_count:
-                    raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})")
-                
-                # Domains zuweisen
-                if domain_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, domain_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # IPv4s zuweisen
-                if ipv4_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, ipv4_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # Telefonnummern zuweisen
-                if phone_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, phone_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-            except ValueError as e:
-                conn.rollback()
-                flash(str(e), 'error')
-                return redirect(url_for('create_license'))
-
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE', 'license', license_id, 
-                     new_values={
-                         'license_key': license_key,
-                         'customer_name': customer_info['name'],
-                         'customer_email': customer_info['email'],
-                         'license_type': license_type,
-                         'valid_from': valid_from,
-                         'valid_until': valid_until,
-                         'device_limit': device_limit,
-                         'is_test': is_test
-                     })
-            
-            flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
-            
-        except Exception as e:
-            conn.rollback()
-            logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}")
-            flash('Fehler beim Erstellen der Lizenz!', 'error')
-        finally:
-            cur.close()
-            conn.close()
-
-        # Preserve show_test parameter if present
-        redirect_url = "/create"
-        if request.args.get('show_test') == 'true':
-            redirect_url += "?show_test=true"
-        return redirect(redirect_url)
-
-    # Unterstützung für vorausgewählten Kunden
-    preselected_customer_id = request.args.get('customer_id', type=int)
-    return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
-
-@app.route("/batch", methods=["GET", "POST"])
-@login_required
-def batch_licenses():
-    """Batch-Generierung mehrerer Lizenzen für einen Kunden"""
-    if request.method == "POST":
-        # Formulardaten
-        customer_id = request.form.get("customer_id")
-        license_type = request.form["license_type"]
-        quantity = int(request.form["quantity"])
-        valid_from = request.form["valid_from"]
-        is_test = request.form.get("is_test") == "on"  # Checkbox value
-        
-        # Berechne valid_until basierend auf Laufzeit
-        duration = int(request.form.get("duration", 1))
-        duration_type = request.form.get("duration_type", "years")
-        
-        from datetime import datetime, timedelta
-        from dateutil.relativedelta import relativedelta
-        
-        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
-        
-        if duration_type == "days":
-            end_date = start_date + timedelta(days=duration)
-        elif duration_type == "months":
-            end_date = start_date + relativedelta(months=duration)
-        else:  # years
-            end_date = start_date + relativedelta(years=duration)
-        
-        # Ein Tag abziehen, da der Starttag mitgezählt wird
-        end_date = end_date - timedelta(days=1)
-        valid_until = end_date.strftime("%Y-%m-%d")
-        
-        # Resource counts
-        domain_count = int(request.form.get("domain_count", 1))
-        ipv4_count = int(request.form.get("ipv4_count", 1))
-        phone_count = int(request.form.get("phone_count", 1))
-        device_limit = int(request.form.get("device_limit", 3))
-        
-        # Sicherheitslimit
-        if quantity < 1 or quantity > 100:
-            flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
-            return redirect(url_for('batch_licenses'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        try:
-            # Prüfe ob neuer Kunde oder bestehender
-            if customer_id == "new":
-                # Neuer Kunde
-                name = request.form.get("customer_name")
-                email = request.form.get("email")
-                
-                if not name:
-                    flash('Kundenname ist erforderlich!', 'error')
-                    return redirect(url_for('batch_licenses'))
-                
-                # Prüfe ob E-Mail bereits existiert
-                if email:
-                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
-                    existing = cur.fetchone()
-                    if existing:
-                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
-                        return redirect(url_for('batch_licenses'))
-                
-                # Kunde einfügen (erbt Test-Status von Lizenz)
-                cur.execute("""
-                    INSERT INTO customers (name, email, is_test, created_at)
-                    VALUES (%s, %s, %s, NOW())
-                    RETURNING id
-                """, (name, email, is_test))
-                customer_id = cur.fetchone()[0]
-                
-                # Audit-Log für neuen Kunden
-                log_audit('CREATE', 'customer', customer_id, 
-                         new_values={'name': name, 'email': email, 'is_test': is_test})
-            else:
-                # Bestehender Kunde - hole Infos
-                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-                customer_data = cur.fetchone()
-                if not customer_data:
-                    flash('Kunde nicht gefunden!', 'error')
-                    return redirect(url_for('batch_licenses'))
-                name = customer_data[0]
-                email = customer_data[1]
-                
-                # Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren
-                if customer_data[2]:  # is_test des Kunden
-                    is_test = True
-            
-            # Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
-            total_domains_needed = domain_count * quantity
-            total_ipv4s_needed = ipv4_count * quantity
-            total_phones_needed = phone_count * quantity
-            
-            cur.execute("""
-                SELECT 
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
-                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
-            """, (is_test, is_test, is_test))
-            available = cur.fetchone()
-            
-            if available[0] < total_domains_needed:
-                flash(f"Nicht genügend Domains verfügbar (benötigt: {total_domains_needed}, verfügbar: {available[0]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            if available[1] < total_ipv4s_needed:
-                flash(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {total_ipv4s_needed}, verfügbar: {available[1]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            if available[2] < total_phones_needed:
-                flash(f"Nicht genügend Telefonnummern verfügbar (benötigt: {total_phones_needed}, verfügbar: {available[2]})", 'error')
-                return redirect(url_for('batch_licenses'))
-            
-            # Lizenzen generieren und speichern
-            generated_licenses = []
-            for i in range(quantity):
-                # Eindeutigen Key generieren
-                attempts = 0
-                while attempts < 10:
-                    license_key = generate_license_key(license_type)
-                    cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
-                    if not cur.fetchone():
-                        break
-                    attempts += 1
-                
-                # Lizenz einfügen
-                cur.execute("""
-                    INSERT INTO licenses (license_key, customer_id, license_type, is_test, 
-                                        valid_from, valid_until, is_active,
-                                        domain_count, ipv4_count, phone_count, device_limit)
-                    VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
-                    RETURNING id
-                """, (license_key, customer_id, license_type, is_test, valid_from, valid_until,
-                      domain_count, ipv4_count, phone_count, device_limit))
-                license_id = cur.fetchone()[0]
-                
-                # Ressourcen für diese Lizenz zuweisen
-                # Domains
-                if domain_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, domain_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # IPv4s
-                if ipv4_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, ipv4_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                # Telefonnummern
-                if phone_count > 0:
-                    cur.execute("""
-                        SELECT id FROM resource_pools
-                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
-                        LIMIT %s FOR UPDATE
-                    """, (is_test, phone_count))
-                    for (resource_id,) in cur.fetchall():
-                        cur.execute("""
-                            UPDATE resource_pools
-                            SET status = 'allocated', allocated_to_license = %s,
-                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
-                            WHERE id = %s
-                        """, (license_id, session['username'], resource_id))
-                        
-                        cur.execute("""
-                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                            VALUES (%s, %s, %s)
-                        """, (license_id, resource_id, session['username']))
-                        
-                        cur.execute("""
-                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                            VALUES (%s, %s, 'allocated', %s, %s)
-                        """, (resource_id, license_id, session['username'], get_client_ip()))
-                
-                generated_licenses.append({
-                    'id': license_id,
-                    'key': license_key,
-                    'type': license_type
-                })
-            
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE_BATCH', 'license', 
-                     new_values={'customer': name, 'quantity': quantity, 'type': license_type},
-                     additional_info=f"Batch-Generierung von {quantity} Lizenzen")
-            
-            # Session für Export speichern
-            session['batch_export'] = {
-                'customer': name,
-                'email': email,
-                'licenses': generated_licenses,
-                'valid_from': valid_from,
-                'valid_until': valid_until,
-                'timestamp': datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
-            }
-            
-            flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
-            return render_template("batch_result.html", 
-                                 customer=name,
-                                 email=email,
-                                 licenses=generated_licenses,
-                                 valid_from=valid_from,
-                                 valid_until=valid_until)
-            
-        except Exception as e:
-            conn.rollback()
-            logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
-            flash('Fehler bei der Batch-Generierung!', 'error')
-            return redirect(url_for('batch_licenses'))
-        finally:
-            cur.close()
-            conn.close()
-    
-    # GET Request
-    return render_template("batch_form.html")
-
-@app.route("/batch/export")
-@login_required
-def export_batch():
-    """Exportiert die zuletzt generierten Batch-Lizenzen"""
-    batch_data = session.get('batch_export')
-    if not batch_data:
-        flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
-        return redirect(url_for('batch_licenses'))
-    
-    # CSV generieren
-    output = io.StringIO()
-    output.write('\ufeff')  # UTF-8 BOM für Excel
-    
-    # Header
-    output.write(f"Kunde: {batch_data['customer']}\n")
-    output.write(f"E-Mail: {batch_data['email']}\n")
-    output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
-    output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
-    output.write("\n")
-    output.write("Nr;Lizenzschlüssel;Typ\n")
-    
-    # Lizenzen
-    for i, license in enumerate(batch_data['licenses'], 1):
-        typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
-        output.write(f"{i};{license['key']};{typ_text}\n")
-    
-    output.seek(0)
-    
-    # Audit-Log
-    log_audit('EXPORT', 'batch_licenses', 
-             additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
-    
-    return send_file(
-        io.BytesIO(output.getvalue().encode('utf-8-sig')),
-        mimetype='text/csv',
-        as_attachment=True,
-        download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv"
-    )
-
-@app.route("/licenses")
-@login_required
-def licenses():
-    # Redirect zur kombinierten Ansicht
-    return redirect("/customers-licenses")
-
-@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
-@login_required
-def edit_license(license_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if request.method == "POST":
-        # Alte Werte für Audit-Log abrufen
-        cur.execute("""
-            SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit 
-            FROM licenses WHERE id = %s
-        """, (license_id,))
-        old_license = cur.fetchone()
-        
-        # Update license
-        license_key = request.form["license_key"]
-        license_type = request.form["license_type"]
-        valid_from = request.form["valid_from"]
-        valid_until = request.form["valid_until"]
-        is_active = request.form.get("is_active") == "on"
-        is_test = request.form.get("is_test") == "on"
-        device_limit = int(request.form.get("device_limit", 3))
-        
-        cur.execute("""
-            UPDATE licenses 
-            SET license_key = %s, license_type = %s, valid_from = %s, 
-                valid_until = %s, is_active = %s, is_test = %s, device_limit = %s
-            WHERE id = %s
-        """, (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('UPDATE', 'license', license_id,
-                 old_values={
-                     'license_key': old_license[0],
-                     'license_type': old_license[1],
-                     'valid_from': str(old_license[2]),
-                     'valid_until': str(old_license[3]),
-                     'is_active': old_license[4],
-                     'is_test': old_license[5],
-                     'device_limit': old_license[6]
-                 },
-                 new_values={
-                     'license_key': license_key,
-                     'license_type': license_type,
-                     'valid_from': valid_from,
-                     'valid_until': valid_until,
-                     'is_active': is_active,
-                     'is_test': is_test,
-                     'device_limit': device_limit
-                 })
-        
-        cur.close()
-        conn.close()
-        
-        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
-        redirect_url = "/customers-licenses"
-        
-        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
-        show_test = request.form.get('show_test') or request.args.get('show_test')
-        if show_test == 'true':
-            redirect_url += "?show_test=true"
-            
-        # Behalte customer_id bei wenn vorhanden
-        if request.referrer and 'customer_id=' in request.referrer:
-            import re
-            match = re.search(r'customer_id=(\d+)', request.referrer)
-            if match:
-                connector = "&" if "?" in redirect_url else "?"
-                redirect_url += f"{connector}customer_id={match.group(1)}"
-                
-        return redirect(redirect_url)
-    
-    # Get license data
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name, c.email, l.license_type, 
-               l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE l.id = %s
-    """, (license_id,))
-    
-    license = cur.fetchone()
-    cur.close()
-    conn.close()
-    
-    if not license:
-        return redirect("/licenses")
-    
-    return render_template("edit_license.html", license=license, username=session.get('username'))
-
-@app.route("/license/delete/<int:license_id>", methods=["POST"])
-@login_required
-def delete_license(license_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Lizenzdetails für Audit-Log abrufen
-    cur.execute("""
-        SELECT l.license_key, c.name, l.license_type 
-        FROM licenses l 
-        JOIN customers c ON l.customer_id = c.id 
-        WHERE l.id = %s
-    """, (license_id,))
-    license_info = cur.fetchone()
-    
-    cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
-    
-    conn.commit()
-    
-    # Audit-Log
-    if license_info:
-        log_audit('DELETE', 'license', license_id,
-                 old_values={
-                     'license_key': license_info[0],
-                     'customer_name': license_info[1],
-                     'license_type': license_info[2]
-                 })
-    
-    cur.close()
-    conn.close()
-    
-    return redirect("/licenses")
-
-@app.route("/customers")
-@login_required
-def customers():
-    # Redirect zur kombinierten Ansicht
-    return redirect("/customers-licenses")
-
-@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
-@login_required
-def edit_customer(customer_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if request.method == "POST":
-        # Alte Werte für Audit-Log abrufen
-        cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
-        old_customer = cur.fetchone()
-        
-        # Update customer
-        name = request.form["name"]
-        email = request.form["email"]
-        is_test = request.form.get("is_test") == "on"
-        
-        cur.execute("""
-            UPDATE customers 
-            SET name = %s, email = %s, is_test = %s
-            WHERE id = %s
-        """, (name, email, is_test, customer_id))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('UPDATE', 'customer', customer_id,
-                 old_values={
-                     'name': old_customer[0],
-                     'email': old_customer[1],
-                     'is_test': old_customer[2]
-                 },
-                 new_values={
-                     'name': name,
-                     'email': email,
-                     'is_test': is_test
-                 })
-        
-        cur.close()
-        conn.close()
-        
-        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
-        redirect_url = "/customers-licenses"
-        
-        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
-        show_test = request.form.get('show_test') or request.args.get('show_test')
-        if show_test == 'true':
-            redirect_url += "?show_test=true"
-            
-        # Behalte customer_id bei (immer der aktuelle Kunde)
-        connector = "&" if "?" in redirect_url else "?"
-        redirect_url += f"{connector}customer_id={customer_id}"
-                
-        return redirect(redirect_url)
-    
-    # Get customer data with licenses
-    cur.execute("""
-        SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s
-    """, (customer_id,))
-    
-    customer = cur.fetchone()
-    if not customer:
-        cur.close()
-        conn.close()
-        return "Kunde nicht gefunden", 404
-    
-    
-    # Get customer's licenses
-    cur.execute("""
-        SELECT id, license_key, license_type, valid_from, valid_until, is_active
-        FROM licenses
-        WHERE customer_id = %s
-        ORDER BY valid_until DESC
-    """, (customer_id,))
-    
-    licenses = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    if not customer:
-        return redirect("/customers-licenses")
-    
-    return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
-
-@app.route("/customer/create", methods=["GET", "POST"])
-@login_required
-def create_customer():
-    """Erstellt einen neuen Kunden ohne Lizenz"""
-    if request.method == "POST":
-        name = request.form.get('name')
-        email = request.form.get('email')
-        is_test = request.form.get('is_test') == 'on'
-        
-        if not name or not email:
-            flash("Name und E-Mail sind Pflichtfelder!", "error")
-            return render_template("create_customer.html", username=session.get('username'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        try:
-            # Prüfen ob E-Mail bereits existiert
-            cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
-            existing = cur.fetchone()
-            if existing:
-                flash(f"Ein Kunde mit der E-Mail '{email}' existiert bereits: {existing[1]}", "error")
-                return render_template("create_customer.html", username=session.get('username'))
-            
-            # Kunde erstellen
-            cur.execute("""
-                INSERT INTO customers (name, email, created_at, is_test) 
-                VALUES (%s, %s, %s, %s) RETURNING id
-            """, (name, email, datetime.now(), is_test))
-            
-            customer_id = cur.fetchone()[0]
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('CREATE', 'customer', customer_id,
-                     new_values={
-                         'name': name,
-                         'email': email,
-                         'is_test': is_test
-                     })
-            
-            flash(f"Kunde '{name}' wurde erfolgreich angelegt!", "success")
-            return redirect(f"/customer/edit/{customer_id}")
-            
-        except Exception as e:
-            conn.rollback()
-            flash(f"Fehler beim Anlegen des Kunden: {str(e)}", "error")
-            return render_template("create_customer.html", username=session.get('username'))
-        finally:
-            cur.close()
-            conn.close()
-    
-    # GET Request - Formular anzeigen
-    return render_template("create_customer.html", username=session.get('username'))
-
-@app.route("/customer/delete/<int:customer_id>", methods=["POST"])
-@login_required
-def delete_customer(customer_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Prüfen ob Kunde Lizenzen hat
-    cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
-    license_count = cur.fetchone()[0]
-    
-    if license_count > 0:
-        # Kunde hat Lizenzen - nicht löschen
-        cur.close()
-        conn.close()
-        return redirect("/customers")
-    
-    # Kundendetails für Audit-Log abrufen
-    cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
-    customer_info = cur.fetchone()
-    
-    # Kunde löschen wenn keine Lizenzen vorhanden
-    cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
-    
-    conn.commit()
-    
-    # Audit-Log
-    if customer_info:
-        log_audit('DELETE', 'customer', customer_id,
-                 old_values={
-                     'name': customer_info[0],
-                     'email': customer_info[1]
-                 })
-    
-    cur.close()
-    conn.close()
-    
-    return redirect("/customers")
-
-@app.route("/customers-licenses")
-@login_required
-def customers_licenses():
-    """Kombinierte Ansicht für Kunden und deren Lizenzen"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    query = """
-        SELECT 
-            c.id,
-            c.name,
-            c.email,
-            c.created_at,
-            COUNT(l.id) as total_licenses,
-            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
-        FROM customers c
-        LEFT JOIN licenses l ON c.id = l.customer_id
-    """
-    
-    if not show_test:
-        query += " WHERE c.is_test = FALSE"
-    
-    query += """
-        GROUP BY c.id, c.name, c.email, c.created_at
-        ORDER BY c.name
-    """
-    
-    cur.execute(query)
-    customers = cur.fetchall()
-    
-    # Hole ausgewählten Kunden nur wenn explizit in URL angegeben
-    selected_customer_id = request.args.get('customer_id', type=int)
-    licenses = []
-    selected_customer = None
-    
-    if customers and selected_customer_id:
-        # Hole Daten des ausgewählten Kunden
-        for customer in customers:
-            if customer[0] == selected_customer_id:
-                selected_customer = customer
-                break
-        
-        # Hole Lizenzen des ausgewählten Kunden
-        if selected_customer:
-            cur.execute("""
-                SELECT 
-                    l.id,
-                    l.license_key,
-                    l.license_type,
-                    l.valid_from,
-                    l.valid_until,
-                    l.is_active,
-                    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 as status,
-                    l.domain_count,
-                    l.ipv4_count,
-                    l.phone_count,
-                    l.device_limit,
-                    (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
-                    -- Actual resource counts
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
-                    (SELECT COUNT(*) FROM license_resources lr 
-                     JOIN resource_pools rp ON lr.resource_id = rp.id 
-                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
-                FROM licenses l
-                WHERE l.customer_id = %s
-                ORDER BY l.created_at DESC, l.id DESC
-            """, (selected_customer_id,))
-            licenses = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("customers_licenses.html",
-                         customers=customers,
-                         selected_customer=selected_customer,
-                         selected_customer_id=selected_customer_id,
-                         licenses=licenses,
-                         show_test=show_test)
-
-@app.route("/api/customer/<int:customer_id>/licenses")
-@login_required
-def api_customer_licenses(customer_id):
-    """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole Lizenzen des Kunden
-    cur.execute("""
-        SELECT 
-            l.id,
-            l.license_key,
-            l.license_type,
-            l.valid_from,
-            l.valid_until,
-            l.is_active,
-            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 as status,
-            l.domain_count,
-            l.ipv4_count,
-            l.phone_count,
-            l.device_limit,
-            (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
-            -- Actual resource counts
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
-            (SELECT COUNT(*) FROM license_resources lr 
-             JOIN resource_pools rp ON lr.resource_id = rp.id 
-             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
-        FROM licenses l
-        WHERE l.customer_id = %s
-        ORDER BY l.created_at DESC, l.id DESC
-    """, (customer_id,))
-    
-    licenses = []
-    for row in cur.fetchall():
-        license_id = row[0]
-        
-        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
-        cur.execute("""
-            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
-            FROM resource_pools rp
-            JOIN license_resources lr ON rp.id = lr.resource_id
-            WHERE lr.license_id = %s AND lr.is_active = true
-            ORDER BY rp.resource_type, rp.resource_value
-        """, (license_id,))
-        
-        resources = {
-            'domains': [],
-            'ipv4s': [],
-            'phones': []
-        }
-        
-        for res_row in cur.fetchall():
-            resource_info = {
-                'id': res_row[0],
-                'value': res_row[2],
-                'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else ''
-            }
-            
-            if res_row[1] == 'domain':
-                resources['domains'].append(resource_info)
-            elif res_row[1] == 'ipv4':
-                resources['ipv4s'].append(resource_info)
-            elif res_row[1] == 'phone':
-                resources['phones'].append(resource_info)
-        
-        licenses.append({
-            'id': row[0],
-            'license_key': row[1],
-            'license_type': row[2],
-            'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '',
-            'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '',
-            'is_active': row[5],
-            'status': row[6],
-            'domain_count': row[7],  # limit
-            'ipv4_count': row[8],     # limit
-            'phone_count': row[9],    # limit
-            'device_limit': row[10],
-            'active_devices': row[11],
-            'actual_domain_count': row[12],  # actual count
-            'actual_ipv4_count': row[13],    # actual count
-            'actual_phone_count': row[14],   # actual count
-            'resources': resources
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'success': True,
-        'licenses': licenses,
-        'count': len(licenses)
-    })
-
-@app.route("/api/customer/<int:customer_id>/quick-stats")
-@login_required
-def api_customer_quick_stats(customer_id):
-    """API-Endpoint für Schnellstatistiken eines Kunden"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Hole Kundenstatistiken
-    cur.execute("""
-        SELECT 
-            COUNT(l.id) as total_licenses,
-            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses,
-            COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon
-        FROM licenses l
-        WHERE l.customer_id = %s
-    """, (customer_id,))
-    
-    stats = cur.fetchone()
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'success': True,
-        'stats': {
-            'total': stats[0],
-            'active': stats[1],
-            'expired': stats[2],
-            'expiring_soon': stats[3]
-        }
-    })
-
-@app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
-@login_required
-def api_license_quick_edit(license_id):
-    """API-Endpoint für schnelle Lizenz-Bearbeitung"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        data = request.get_json()
-        
-        # Hole alte Werte für Audit-Log
-        cur.execute("""
-            SELECT is_active, valid_until, license_type 
-            FROM licenses WHERE id = %s
-        """, (license_id,))
-        old_values = cur.fetchone()
-        
-        if not old_values:
-            return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404
-        
-        # Update-Felder vorbereiten
-        updates = []
-        params = []
-        new_values = {}
-        
-        if 'is_active' in data:
-            updates.append("is_active = %s")
-            params.append(data['is_active'])
-            new_values['is_active'] = data['is_active']
-        
-        if 'valid_until' in data:
-            updates.append("valid_until = %s")
-            params.append(data['valid_until'])
-            new_values['valid_until'] = data['valid_until']
-        
-        if 'license_type' in data:
-            updates.append("license_type = %s")
-            params.append(data['license_type'])
-            new_values['license_type'] = data['license_type']
-        
-        if updates:
-            params.append(license_id)
-            cur.execute(f"""
-                UPDATE licenses 
-                SET {', '.join(updates)}
-                WHERE id = %s
-            """, params)
-            
-            conn.commit()
-            
-            # Audit-Log
-            log_audit('UPDATE', 'license', license_id,
-                     old_values={
-                         'is_active': old_values[0],
-                         'valid_until': old_values[1].isoformat() if old_values[1] else None,
-                         'license_type': old_values[2]
-                     },
-                     new_values=new_values)
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True})
-    
-    except Exception as e:
-        conn.rollback()
-        cur.close()
-        conn.close()
-        return jsonify({'success': False, 'error': str(e)}), 500
-
-@app.route("/api/license/<int:license_id>/resources")
-@login_required
-def api_license_resources(license_id):
-    """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
-        cur.execute("""
-            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
-            FROM resource_pools rp
-            JOIN license_resources lr ON rp.id = lr.resource_id
-            WHERE lr.license_id = %s AND lr.is_active = true
-            ORDER BY rp.resource_type, rp.resource_value
-        """, (license_id,))
-        
-        resources = {
-            'domains': [],
-            'ipv4s': [],
-            'phones': []
-        }
-        
-        for row in cur.fetchall():
-            resource_info = {
-                'id': row[0],
-                'value': row[2],
-                'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else ''
-            }
-            
-            if row[1] == 'domain':
-                resources['domains'].append(resource_info)
-            elif row[1] == 'ipv4':
-                resources['ipv4s'].append(resource_info)
-            elif row[1] == 'phone':
-                resources['phones'].append(resource_info)
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'resources': resources
-        })
-        
-    except Exception as e:
-        cur.close()
-        conn.close()
-        return jsonify({'success': False, 'error': str(e)}), 500
-
-@app.route("/sessions")
-@login_required
-def sessions():
-    conn = get_connection()
-    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
-    cur.execute(f"""
-        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
-               s.user_agent, s.started_at, s.last_heartbeat,
-               EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
-        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 {active_sort_fields[active_sort]} {active_order.upper()}
-    """)
-    active_sessions = cur.fetchall()
-    
-    # Inaktive Sessions der letzten 24 Stunden
-    cur.execute(f"""
-        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
-               s.started_at, s.ended_at, 
-               EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
-        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 > NOW() - INTERVAL '24 hours'
-        ORDER BY {ended_sort_fields[ended_sort]} {ended_order.upper()}
-        LIMIT 50
-    """)
-    recent_sessions = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("sessions.html", 
-                         active_sessions=active_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'))
-
-@app.route("/session/end/<int:session_id>", methods=["POST"])
-@login_required
-def end_session(session_id):
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Session beenden
-    cur.execute("""
-        UPDATE sessions 
-        SET is_active = FALSE, ended_at = NOW()
-        WHERE id = %s AND is_active = TRUE
-    """, (session_id,))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    return redirect("/sessions")
-
-@app.route("/export/licenses")
-@login_required
-def export_licenses():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
-    include_test = request.args.get('include_test', 'false').lower() == 'true'
-    customer_id = request.args.get('customer_id', type=int)
-    
-    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.is_test,
-               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 as status
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-    """
-    
-    # Build WHERE clause
-    where_conditions = []
-    params = []
-    
-    if not include_test:
-        where_conditions.append("l.is_test = FALSE")
-    
-    if customer_id:
-        where_conditions.append("l.customer_id = %s")
-        params.append(customer_id)
-    
-    if where_conditions:
-        query += " WHERE " + " AND ".join(where_conditions)
-    
-    query += " ORDER BY l.id"
-    
-    cur.execute(query, params)
-    
-    # Spaltennamen
-    columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 
-               'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status']
-    
-    # Daten in DataFrame
-    data = cur.fetchall()
-    df = pd.DataFrame(data, columns=columns)
-    
-    # Datumsformatierung
-    df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y')
-    df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y')
-    
-    # Typ und Aktiv Status anpassen
-    df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
-    df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
-    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
-    
-    cur.close()
-    conn.close()
-    
-    # Export Format
-    export_format = request.args.get('format', 'excel')
-    
-    # Audit-Log
-    log_audit('EXPORT', 'license', 
-             additional_info=f"Export aller Lizenzen als {export_format.upper()}")
-    filename = f'lizenzen_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
-    
-    if export_format == 'csv':
-        # CSV Export
-        output = io.StringIO()
-        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
-        output.seek(0)
-        
-        return send_file(
-            io.BytesIO(output.getvalue().encode('utf-8-sig')),
-            mimetype='text/csv',
-            as_attachment=True,
-            download_name=f'{filename}.csv'
-        )
-    else:
-        # Excel Export
-        output = io.BytesIO()
-        with pd.ExcelWriter(output, engine='openpyxl') as writer:
-            df.to_excel(writer, sheet_name='Lizenzen', index=False)
-            
-            # Formatierung
-            worksheet = writer.sheets['Lizenzen']
-            for column in worksheet.columns:
-                max_length = 0
-                column_letter = column[0].column_letter
-                for cell in column:
-                    try:
-                        if len(str(cell.value)) > max_length:
-                            max_length = len(str(cell.value))
-                    except:
-                        pass
-                adjusted_width = min(max_length + 2, 50)
-                worksheet.column_dimensions[column_letter].width = adjusted_width
-        
-        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/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():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Check if test data should be included
-    include_test = request.args.get('include_test', 'false').lower() == 'true'
-    
-    # Build query based on test data filter
-    if include_test:
-        # Include all customers
-        query = """
-            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
-                   COUNT(l.id) as total_licenses,
-                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
-                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
-            FROM customers c
-            LEFT JOIN licenses l ON c.id = l.customer_id
-            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
-            ORDER BY c.id
-        """
-    else:
-        # Exclude test customers and test licenses
-        query = """
-            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
-                   COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
-                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
-                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
-            FROM customers c
-            LEFT JOIN licenses l ON c.id = l.customer_id
-            WHERE c.is_test = FALSE
-            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
-            ORDER BY c.id
-        """
-    
-    cur.execute(query)
-    
-    # Spaltennamen
-    columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten',
-               'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
-    
-    # Daten in DataFrame
-    data = cur.fetchall()
-    df = pd.DataFrame(data, columns=columns)
-    
-    # Datumsformatierung
-    df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
-    
-    # Testdaten formatting
-    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
-    
-    cur.close()
-    conn.close()
-    
-    # Export Format
-    export_format = request.args.get('format', 'excel')
-    
-    # Audit-Log
-    log_audit('EXPORT', 'customer', 
-             additional_info=f"Export aller Kunden als {export_format.upper()}")
-    filename = f'kunden_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
-    
-    if export_format == 'csv':
-        # CSV Export
-        output = io.StringIO()
-        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
-        output.seek(0)
-        
-        return send_file(
-            io.BytesIO(output.getvalue().encode('utf-8-sig')),
-            mimetype='text/csv',
-            as_attachment=True,
-            download_name=f'{filename}.csv'
-        )
-    else:
-        # Excel Export
-        output = io.BytesIO()
-        with pd.ExcelWriter(output, engine='openpyxl') as writer:
-            df.to_excel(writer, sheet_name='Kunden', index=False)
-            
-            # Formatierung
-            worksheet = writer.sheets['Kunden']
-            for column in worksheet.columns:
-                max_length = 0
-                column_letter = column[0].column_letter
-                for cell in column:
-                    try:
-                        if len(str(cell.value)) > max_length:
-                            max_length = len(str(cell.value))
-                    except:
-                        pass
-                adjusted_width = min(max_length + 2, 50)
-                worksheet.column_dimensions[column_letter].width = adjusted_width
-        
-        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/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.resource_type, r.resource_value, r.status, r.allocated_to_license, 
-               r.created_at, r.status_changed_at,
-               l.license_key, c.name as customer_name, c.email as customer_email,
-               l.license_type
-        FROM resource_pools r
-        LEFT JOIN licenses l ON r.allocated_to_license = 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 (r.is_test = false OR r.is_test IS NULL)"
-    
-    # Filter für Ressourcentyp
-    if filter_type:
-        query += " AND r.resource_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.resource_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():
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Parameter
-    filter_user = request.args.get('user', '').strip()
-    filter_action = request.args.get('action', '').strip()
-    filter_entity = request.args.get('entity', '').strip()
-    page = request.args.get('page', 1, type=int)
-    sort = request.args.get('sort', 'timestamp')
-    order = request.args.get('order', 'desc')
-    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
-    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 = []
-    
-    # Filter
-    if filter_user:
-        query += " AND LOWER(username) LIKE LOWER(%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)
-    
-    # Gesamtanzahl für Pagination
-    count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table"
-    cur.execute(count_query, params)
-    total = cur.fetchone()[0]
-    
-    # Pagination
-    offset = (page - 1) * per_page
-    query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
-    params.extend([per_page, offset])
-    
-    cur.execute(query, params)
-    logs = cur.fetchall()
-    
-    # JSON-Werte parsen
-    parsed_logs = []
-    for log in logs:
-        parsed_log = list(log)
-        # old_values und new_values sind bereits Dictionaries (JSONB)
-        # Keine Konvertierung nötig
-        parsed_logs.append(parsed_log)
-    
-    # Pagination Info
-    total_pages = (total + per_page - 1) // per_page
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("audit_log.html", 
-                         logs=parsed_logs,
-                         filter_user=filter_user,
-                         filter_action=filter_action,
-                         filter_entity=filter_entity,
-                         page=page,
-                         total_pages=total_pages,
-                         total=total,
-                         sort=sort,
-                         order=order,
-                         username=session.get('username'))
-
-@app.route("/backups")
-@login_required
-def backups():
-    """Zeigt die Backup-Historie an"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Letztes erfolgreiches Backup für Dashboard
-    cur.execute("""
-        SELECT created_at, filesize, duration_seconds 
-        FROM backup_history 
-        WHERE status = 'success' 
-        ORDER BY created_at DESC 
-        LIMIT 1
-    """)
-    last_backup = cur.fetchone()
-    
-    # Alle Backups abrufen
-    cur.execute("""
-        SELECT id, filename, filesize, backup_type, status, error_message,
-               created_at, created_by, tables_count, records_count, 
-               duration_seconds, is_encrypted
-        FROM backup_history
-        ORDER BY created_at DESC
-    """)
-    backups = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("backups.html", 
-                         backups=backups,
-                         last_backup=last_backup,
-                         username=session.get('username'))
-
-@app.route("/backup/create", methods=["POST"])
-@login_required
-def create_backup_route():
-    """Erstellt ein manuelles Backup"""
-    username = session.get('username')
-    success, result = create_backup(backup_type="manual", created_by=username)
-    
-    if success:
-        return jsonify({
-            'success': True,
-            'message': f'Backup erfolgreich erstellt: {result}'
-        })
-    else:
-        return jsonify({
-            'success': False,
-            'message': f'Backup fehlgeschlagen: {result}'
-        }), 500
-
-@app.route("/backup/restore/<int:backup_id>", methods=["POST"])
-@login_required
-def restore_backup_route(backup_id):
-    """Stellt ein Backup wieder her"""
-    encryption_key = request.form.get('encryption_key')
-    
-    success, message = restore_backup(backup_id, encryption_key)
-    
-    if success:
-        return jsonify({
-            'success': True,
-            'message': message
-        })
-    else:
-        return jsonify({
-            'success': False,
-            'message': message
-        }), 500
-
-@app.route("/backup/download/<int:backup_id>")
-@login_required
-def download_backup(backup_id):
-    """Lädt eine Backup-Datei herunter"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT filename, filepath 
-        FROM backup_history 
-        WHERE id = %s
-    """, (backup_id,))
-    backup_info = cur.fetchone()
-    
-    cur.close()
-    conn.close()
-    
-    if not backup_info:
-        return "Backup nicht gefunden", 404
-    
-    filename, filepath = backup_info
-    filepath = Path(filepath)
-    
-    if not filepath.exists():
-        return "Backup-Datei nicht gefunden", 404
-    
-    # Audit-Log
-    log_audit('DOWNLOAD', 'backup', backup_id,
-             additional_info=f"Backup heruntergeladen: {filename}")
-    
-    return send_file(filepath, as_attachment=True, download_name=filename)
-
-@app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
-@login_required
-def delete_backup(backup_id):
-    """Löscht ein Backup"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        # Backup-Informationen abrufen
-        cur.execute("""
-            SELECT filename, filepath 
-            FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        backup_info = cur.fetchone()
-        
-        if not backup_info:
-            return jsonify({
-                'success': False,
-                'message': 'Backup nicht gefunden'
-            }), 404
-        
-        filename, filepath = backup_info
-        filepath = Path(filepath)
-        
-        # Datei löschen, wenn sie existiert
-        if filepath.exists():
-            filepath.unlink()
-        
-        # Aus Datenbank löschen
-        cur.execute("""
-            DELETE FROM backup_history 
-            WHERE id = %s
-        """, (backup_id,))
-        
-        conn.commit()
-        
-        # Audit-Log
-        log_audit('DELETE', 'backup', backup_id,
-                 additional_info=f"Backup gelöscht: {filename}")
-        
-        return jsonify({
-            'success': True,
-            'message': f'Backup "{filename}" wurde erfolgreich gelöscht'
-        })
-        
-    except Exception as e:
-        conn.rollback()
-        return jsonify({
-            'success': False,
-            'message': f'Fehler beim Löschen des Backups: {str(e)}'
-        }), 500
-    finally:
-        cur.close()
-        conn.close()
-
-@app.route("/security/blocked-ips")
-@login_required
-def blocked_ips():
-    """Zeigt alle gesperrten IPs an"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    cur.execute("""
-        SELECT 
-            ip_address,
-            attempt_count,
-            first_attempt,
-            last_attempt,
-            blocked_until,
-            last_username_tried,
-            last_error_message
-        FROM login_attempts
-        WHERE blocked_until IS NOT NULL
-        ORDER BY blocked_until DESC
-    """)
-    
-    blocked_ips_list = []
-    for ip in cur.fetchall():
-        blocked_ips_list.append({
-            'ip_address': ip[0],
-            'attempt_count': ip[1],
-            'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'),
-            'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'),
-            'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'),
-            'is_active': ip[4] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None),
-            'last_username': ip[5],
-            'last_error': ip[6]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return render_template("blocked_ips.html", 
-                         blocked_ips=blocked_ips_list,
-                         username=session.get('username'))
-
-@app.route("/security/unblock-ip", methods=["POST"])
-@login_required
-def unblock_ip():
-    """Entsperrt eine IP-Adresse"""
-    ip_address = request.form.get('ip_address')
-    
-    if ip_address:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        cur.execute("""
-            UPDATE login_attempts
-            SET blocked_until = NULL
-            WHERE ip_address = %s
-        """, (ip_address,))
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        # Audit-Log
-        log_audit('UNBLOCK_IP', 'security', 
-                 additional_info=f"IP {ip_address} manuell entsperrt")
-    
-    return redirect(url_for('blocked_ips'))
-
-@app.route("/security/clear-attempts", methods=["POST"])
-@login_required
-def clear_attempts():
-    """Löscht alle Login-Versuche für eine IP"""
-    ip_address = request.form.get('ip_address')
-    
-    if ip_address:
-        reset_login_attempts(ip_address)
-        
-        # Audit-Log
-        log_audit('CLEAR_ATTEMPTS', 'security', 
-                 additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt")
-    
-    return redirect(url_for('blocked_ips'))
-
-# API Endpoints for License Management
-@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
-@login_required
-def toggle_license_api(license_id):
-    """Toggle license active status via API"""
-    try:
-        data = request.get_json()
-        is_active = data.get('is_active', False)
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update license status
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = %s
-            WHERE id = %s
-        """, (is_active, license_id))
-        
-        conn.commit()
-        
-        # Log the action
-        log_audit('UPDATE', 'license', license_id,
-                 new_values={'is_active': is_active},
-                 additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Status erfolgreich geändert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/licenses/bulk-activate", methods=["POST"])
-@login_required
-def bulk_activate_licenses():
-    """Activate multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update all selected licenses (nur Live-Daten)
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = TRUE
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_UPDATE', 'licenses', None,
-                 new_values={'is_active': True, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen aktiviert")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
-@login_required
-def bulk_deactivate_licenses():
-    """Deactivate multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Update all selected licenses (nur Live-Daten)
-        cur.execute("""
-            UPDATE licenses 
-            SET is_active = FALSE
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_UPDATE', 'licenses', None,
-                 new_values={'is_active': False, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen deaktiviert")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-@app.route("/api/license/<int:license_id>/devices")
-@login_required
-def get_license_devices(license_id):
-    """Hole alle registrierten Geräte einer Lizenz"""
-    try:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob Lizenz existiert und hole device_limit
-        cur.execute("""
-            SELECT device_limit FROM licenses WHERE id = %s
-        """, (license_id,))
-        license_data = cur.fetchone()
-        
-        if not license_data:
-            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
-        
-        device_limit = license_data[0]
-        
-        # Hole alle Geräte für diese Lizenz
-        cur.execute("""
-            SELECT id, hardware_id, device_name, operating_system,
-                   first_seen, last_seen, is_active, ip_address
-            FROM device_registrations
-            WHERE license_id = %s
-            ORDER BY is_active DESC, last_seen DESC
-        """, (license_id,))
-        
-        devices = []
-        for row in cur.fetchall():
-            devices.append({
-                'id': row[0],
-                'hardware_id': row[1],
-                'device_name': row[2] or 'Unbekanntes Gerät',
-                'operating_system': row[3] or 'Unbekannt',
-                'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '',
-                'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '',
-                'is_active': row[6],
-                'ip_address': row[7] or '-'
-            })
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'devices': devices,
-            'device_limit': device_limit,
-            'active_count': sum(1 for d in devices if d['is_active'])
-        })
-        
-    except Exception as e:
-        logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
-
-@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
-def register_device(license_id):
-    """Registriere ein neues Gerät für eine Lizenz"""
-    try:
-        data = request.get_json()
-        hardware_id = data.get('hardware_id')
-        device_name = data.get('device_name', '')
-        operating_system = data.get('operating_system', '')
-        
-        if not hardware_id:
-            return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob Lizenz existiert und aktiv ist
-        cur.execute("""
-            SELECT device_limit, is_active, valid_until
-            FROM licenses 
-            WHERE id = %s
-        """, (license_id,))
-        license_data = cur.fetchone()
-        
-        if not license_data:
-            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
-        
-        device_limit, is_active, valid_until = license_data
-        
-        # Prüfe ob Lizenz aktiv und gültig ist
-        if not is_active:
-            return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
-        
-        if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
-            return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
-        
-        # Prüfe ob Gerät bereits registriert ist
-        cur.execute("""
-            SELECT id, is_active FROM device_registrations
-            WHERE license_id = %s AND hardware_id = %s
-        """, (license_id, hardware_id))
-        existing_device = cur.fetchone()
-        
-        if existing_device:
-            device_id, is_device_active = existing_device
-            if is_device_active:
-                # Gerät ist bereits aktiv, update last_seen
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET last_seen = CURRENT_TIMESTAMP,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
-            else:
-                # Gerät war deaktiviert, prüfe ob wir es reaktivieren können
-                cur.execute("""
-                    SELECT COUNT(*) FROM device_registrations
-                    WHERE license_id = %s AND is_active = TRUE
-                """, (license_id,))
-                active_count = cur.fetchone()[0]
-                
-                if active_count >= device_limit:
-                    return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
-                
-                # Reaktiviere das Gerät
-                cur.execute("""
-                    UPDATE device_registrations
-                    SET is_active = TRUE,
-                        last_seen = CURRENT_TIMESTAMP,
-                        deactivated_at = NULL,
-                        deactivated_by = NULL,
-                        ip_address = %s,
-                        user_agent = %s
-                    WHERE id = %s
-                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
-                conn.commit()
-                return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
-        
-        # Neues Gerät - prüfe Gerätelimit
-        cur.execute("""
-            SELECT COUNT(*) FROM device_registrations
-            WHERE license_id = %s AND is_active = TRUE
-        """, (license_id,))
-        active_count = cur.fetchone()[0]
-        
-        if active_count >= device_limit:
-            return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
-        
-        # Registriere neues Gerät
-        cur.execute("""
-            INSERT INTO device_registrations 
-            (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
-            VALUES (%s, %s, %s, %s, %s, %s)
-            RETURNING id
-        """, (license_id, hardware_id, device_name, operating_system, 
-              get_client_ip(), request.headers.get('User-Agent', '')))
-        device_id = cur.fetchone()[0]
-        
-        conn.commit()
-        
-        # Audit Log
-        log_audit('DEVICE_REGISTER', 'device', device_id,
-                 new_values={'license_id': license_id, 'hardware_id': hardware_id})
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
-        
-    except Exception as e:
-        logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
-
-@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
-@login_required
-def deactivate_device(license_id, device_id):
-    """Deaktiviere ein registriertes Gerät"""
-    try:
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Prüfe ob das Gerät zu dieser Lizenz gehört
-        cur.execute("""
-            SELECT id FROM device_registrations
-            WHERE id = %s AND license_id = %s AND is_active = TRUE
-        """, (device_id, license_id))
-        
-        if not cur.fetchone():
-            return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404
-        
-        # Deaktiviere das Gerät
-        cur.execute("""
-            UPDATE device_registrations
-            SET is_active = FALSE,
-                deactivated_at = CURRENT_TIMESTAMP,
-                deactivated_by = %s
-            WHERE id = %s
-        """, (session['username'], device_id))
-        
-        conn.commit()
-        
-        # Audit Log
-        log_audit('DEVICE_DEACTIVATE', 'device', device_id,
-                 old_values={'is_active': True},
-                 new_values={'is_active': False})
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'})
-        
-    except Exception as e:
-        logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
-        return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
-
-@app.route("/api/licenses/bulk-delete", methods=["POST"])
-@login_required
-def bulk_delete_licenses():
-    """Delete multiple licenses at once"""
-    try:
-        data = request.get_json()
-        license_ids = data.get('ids', [])
-        
-        if not license_ids:
-            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        # Get license info for audit log (nur Live-Daten)
-        cur.execute("""
-            SELECT license_key 
-            FROM licenses 
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        license_keys = [row[0] for row in cur.fetchall()]
-        
-        # Delete all selected licenses (nur Live-Daten)
-        cur.execute("""
-            DELETE FROM licenses 
-            WHERE id = ANY(%s) AND is_test = FALSE
-        """, (license_ids,))
-        
-        affected_rows = cur.rowcount
-        conn.commit()
-        
-        # Log the bulk action
-        log_audit('BULK_DELETE', 'licenses', None,
-                 old_values={'license_keys': license_keys, 'count': affected_rows},
-                 additional_info=f"{affected_rows} Lizenzen gelöscht")
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'})
-    except Exception as e:
-        return jsonify({'success': False, 'message': str(e)}), 500
-
-# ===================== RESOURCE POOL MANAGEMENT =====================
-
-@app.route('/resources')
-@login_required
-def resources():
-    """Resource Pool Hauptübersicht"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Prüfe ob Testdaten angezeigt werden sollen (gleiche Logik wie bei Kunden)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    # Statistiken abrufen
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'available') as available,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-            COUNT(*) as total
-        FROM resource_pools
-        WHERE is_test = %s
-        GROUP BY resource_type
-    """, (show_test,))
-    
-    stats = {}
-    for row in cur.fetchall():
-        stats[row[0]] = {
-            'available': row[1],
-            'allocated': row[2],
-            'quarantine': row[3],
-            'total': row[4],
-            'available_percent': round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
-        }
-    
-    # Letzte Aktivitäten (gefiltert nach Test/Live)
-    cur.execute("""
-        SELECT 
-            rh.action,
-            rh.action_by,
-            rh.action_at,
-            rp.resource_type,
-            rp.resource_value,
-            rh.details
-        FROM resource_history rh
-        JOIN resource_pools rp ON rh.resource_id = rp.id
-        WHERE rp.is_test = %s
-        ORDER BY rh.action_at DESC
-        LIMIT 10
-    """, (show_test,))
-    recent_activities = cur.fetchall()
-    
-    # Ressourcen-Liste mit Pagination
-    page = request.args.get('page', 1, type=int)
-    per_page = 50
-    offset = (page - 1) * per_page
-    
-    resource_type = request.args.get('type', '')
-    status_filter = request.args.get('status', '')
-    search = request.args.get('search', '')
-    
-    # Sortierung
-    sort_by = request.args.get('sort', 'id')
-    sort_order = request.args.get('order', 'desc')
-    
-    # Base Query
-    query = """
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            rp.status,
-            rp.allocated_to_license,
-            l.license_key,
-            c.name as customer_name,
-            rp.status_changed_at,
-            rp.quarantine_reason,
-            rp.quarantine_until,
-            c.id as customer_id
-        FROM resource_pools rp
-        LEFT JOIN licenses l ON rp.allocated_to_license = l.id
-        LEFT JOIN customers c ON l.customer_id = c.id
-        WHERE rp.is_test = %s
-    """
-    params = [show_test]
-    
-    if resource_type:
-        query += " AND rp.resource_type = %s"
-        params.append(resource_type)
-    
-    if status_filter:
-        query += " AND rp.status = %s"
-        params.append(status_filter)
-    
-    if search:
-        query += " AND rp.resource_value ILIKE %s"
-        params.append(f'%{search}%')
-    
-    # Count total
-    count_query = f"SELECT COUNT(*) FROM ({query}) as cnt"
-    cur.execute(count_query, params)
-    total = cur.fetchone()[0]
-    total_pages = (total + per_page - 1) // per_page
-    
-    # Get paginated results with dynamic sorting
-    sort_column_map = {
-        'id': 'rp.id',
-        'type': 'rp.resource_type',
-        'resource': 'rp.resource_value',
-        'status': 'rp.status',
-        'assigned': 'c.name',
-        'changed': 'rp.status_changed_at'
-    }
-    
-    sort_column = sort_column_map.get(sort_by, 'rp.id')
-    sort_direction = 'ASC' if sort_order == 'asc' else 'DESC'
-    
-    query += f" ORDER BY {sort_column} {sort_direction} LIMIT %s OFFSET %s"
-    params.extend([per_page, offset])
-    
-    cur.execute(query, params)
-    resources = cur.fetchall()
-    
-    cur.close()
-    conn.close()
-    
-    return render_template('resources.html',
-                         stats=stats,
-                         resources=resources,
-                         recent_activities=recent_activities,
-                         page=page,
-                         total_pages=total_pages,
-                         total=total,
-                         resource_type=resource_type,
-                         status_filter=status_filter,
-                         search=search,
-                         show_test=show_test,
-                         sort_by=sort_by,
-                         sort_order=sort_order,
-                         datetime=datetime,
-                         timedelta=timedelta)
-
-@app.route('/resources/add', methods=['GET', 'POST'])
-@login_required
-def add_resources():
-    """Ressourcen zum Pool hinzufügen"""
-    # Hole show_test Parameter für die Anzeige
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    if request.method == 'POST':
-        resource_type = request.form.get('resource_type')
-        resources_text = request.form.get('resources_text', '')
-        is_test = request.form.get('is_test') == 'on'  # Checkbox für Testdaten
-        
-        # Parse resources (one per line)
-        resources = [r.strip() for r in resources_text.split('\n') if r.strip()]
-        
-        if not resources:
-            flash('Keine Ressourcen angegeben', 'error')
-            return redirect(url_for('add_resources', show_test=show_test))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        added = 0
-        duplicates = 0
-        
-        for resource_value in resources:
-            try:
-                cur.execute("""
-                    INSERT INTO resource_pools (resource_type, resource_value, status_changed_by, is_test)
-                    VALUES (%s, %s, %s, %s)
-                    ON CONFLICT (resource_type, resource_value) DO NOTHING
-                """, (resource_type, resource_value, session['username'], is_test))
-                
-                if cur.rowcount > 0:
-                    added += 1
-                    # Get the inserted ID
-                    cur.execute("SELECT id FROM resource_pools WHERE resource_type = %s AND resource_value = %s", 
-                              (resource_type, resource_value))
-                    resource_id = cur.fetchone()[0]
-                    
-                    # Log in history
-                    cur.execute("""
-                        INSERT INTO resource_history (resource_id, action, action_by, ip_address)
-                        VALUES (%s, 'created', %s, %s)
-                    """, (resource_id, session['username'], get_client_ip()))
-                else:
-                    duplicates += 1
-                    
-            except Exception as e:
-                app.logger.error(f"Error adding resource {resource_value}: {e}")
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        log_audit('CREATE', 'resource_pool', None,
-                 new_values={'type': resource_type, 'added': added, 'duplicates': duplicates, 'is_test': is_test},
-                 additional_info=f"{added} {'Test-' if is_test else ''}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen")
-        
-        flash(f'{added} {"Test-" if is_test else ""}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen', 'success')
-        return redirect(url_for('resources', show_test=show_test))
-    
-    return render_template('add_resources.html', show_test=show_test)
-
-@app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
-@login_required
-def quarantine_resource(resource_id):
-    """Ressource in Quarantäne setzen"""
-    reason = request.form.get('reason', 'review')
-    until_date = request.form.get('until_date')
-    notes = request.form.get('notes', '')
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Get current resource info
-    cur.execute("SELECT resource_type, resource_value, status FROM resource_pools WHERE id = %s", (resource_id,))
-    resource = cur.fetchone()
-    
-    if not resource:
-        flash('Ressource nicht gefunden', 'error')
-        return redirect(url_for('resources'))
-    
-    old_status = resource[2]
-    
-    # Update resource
-    cur.execute("""
-        UPDATE resource_pools
-        SET status = 'quarantine',
-            quarantine_reason = %s,
-            quarantine_until = %s,
-            notes = %s,
-            status_changed_at = CURRENT_TIMESTAMP,
-            status_changed_by = %s
-        WHERE id = %s
-    """, (reason, until_date if until_date else None, notes, session['username'], resource_id))
-    
-    # Log in history
-    cur.execute("""
-        INSERT INTO resource_history (resource_id, action, action_by, ip_address, details)
-        VALUES (%s, 'quarantined', %s, %s, %s)
-    """, (resource_id, session['username'], get_client_ip(), 
-          Json({'reason': reason, 'until': until_date, 'notes': notes, 'old_status': old_status})))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('UPDATE', 'resource', resource_id,
-             old_values={'status': old_status},
-             new_values={'status': 'quarantine', 'reason': reason},
-             additional_info=f"Ressource {resource[0]}: {resource[1]} in Quarantäne")
-    
-    flash('Ressource in Quarantäne gesetzt', 'success')
-    
-    # Redirect mit allen aktuellen Filtern
-    return redirect(url_for('resources', 
-                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
-                          type=request.args.get('type', request.form.get('type', '')),
-                          status=request.args.get('status', request.form.get('status', '')),
-                          search=request.args.get('search', request.form.get('search', ''))))
-
-@app.route('/resources/release', methods=['POST'])
-@login_required
-def release_resources():
-    """Ressourcen aus Quarantäne freigeben"""
-    resource_ids = request.form.getlist('resource_ids')
-    
-    if not resource_ids:
-        flash('Keine Ressourcen ausgewählt', 'error')
-        return redirect(url_for('resources'))
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    released = 0
-    for resource_id in resource_ids:
-        cur.execute("""
-            UPDATE resource_pools
-            SET status = 'available',
-                quarantine_reason = NULL,
-                quarantine_until = NULL,
-                allocated_to_license = NULL,
-                status_changed_at = CURRENT_TIMESTAMP,
-                status_changed_by = %s
-            WHERE id = %s AND status = 'quarantine'
-        """, (session['username'], resource_id))
-        
-        if cur.rowcount > 0:
-            released += 1
-            # Log in history
-            cur.execute("""
-                INSERT INTO resource_history (resource_id, action, action_by, ip_address)
-                VALUES (%s, 'released', %s, %s)
-            """, (resource_id, session['username'], get_client_ip()))
-    
-    conn.commit()
-    cur.close()
-    conn.close()
-    
-    log_audit('UPDATE', 'resource_pool', None,
-             new_values={'released': released},
-             additional_info=f"{released} Ressourcen aus Quarantäne freigegeben")
-    
-    flash(f'{released} Ressourcen freigegeben', 'success')
-    
-    # Redirect mit allen aktuellen Filtern
-    return redirect(url_for('resources', 
-                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
-                          type=request.args.get('type', request.form.get('type', '')),
-                          status=request.args.get('status', request.form.get('status', '')),
-                          search=request.args.get('search', request.form.get('search', ''))))
-
-@app.route('/api/resources/allocate', methods=['POST'])
-@login_required
-def allocate_resources_api():
-    """API für Ressourcen-Zuweisung bei Lizenzerstellung"""
-    data = request.json
-    license_id = data.get('license_id')
-    domain_count = data.get('domain_count', 1)
-    ipv4_count = data.get('ipv4_count', 1)
-    phone_count = data.get('phone_count', 1)
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    try:
-        allocated = {'domains': [], 'ipv4s': [], 'phones': []}
-        
-        # Allocate domains
-        if domain_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'domain' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (domain_count,))
-            domains = cur.fetchall()
-            
-            if len(domains) < domain_count:
-                raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {len(domains)})")
-            
-            for domain_id, domain_value in domains:
-                # Update resource status
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], domain_id))
-                
-                # Create assignment
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, domain_id, session['username']))
-                
-                # Log history
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (domain_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['domains'].append(domain_value)
-        
-        # Allocate IPv4s (similar logic)
-        if ipv4_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'ipv4' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (ipv4_count,))
-            ipv4s = cur.fetchall()
-            
-            if len(ipv4s) < ipv4_count:
-                raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar")
-            
-            for ipv4_id, ipv4_value in ipv4s:
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], ipv4_id))
-                
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, ipv4_id, session['username']))
-                
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (ipv4_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['ipv4s'].append(ipv4_value)
-        
-        # Allocate phones (similar logic)
-        if phone_count > 0:
-            cur.execute("""
-                SELECT id, resource_value FROM resource_pools
-                WHERE resource_type = 'phone' AND status = 'available'
-                LIMIT %s FOR UPDATE
-            """, (phone_count,))
-            phones = cur.fetchall()
-            
-            if len(phones) < phone_count:
-                raise ValueError(f"Nicht genügend Telefonnummern verfügbar")
-            
-            for phone_id, phone_value in phones:
-                cur.execute("""
-                    UPDATE resource_pools
-                    SET status = 'allocated',
-                        allocated_to_license = %s,
-                        status_changed_at = CURRENT_TIMESTAMP,
-                        status_changed_by = %s
-                    WHERE id = %s
-                """, (license_id, session['username'], phone_id))
-                
-                cur.execute("""
-                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
-                    VALUES (%s, %s, %s)
-                """, (license_id, phone_id, session['username']))
-                
-                cur.execute("""
-                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
-                    VALUES (%s, %s, 'allocated', %s, %s)
-                """, (phone_id, license_id, session['username'], get_client_ip()))
-                
-                allocated['phones'].append(phone_value)
-        
-        # Update license resource counts
-        cur.execute("""
-            UPDATE licenses
-            SET domain_count = %s,
-                ipv4_count = %s,
-                phone_count = %s
-            WHERE id = %s
-        """, (domain_count, ipv4_count, phone_count, license_id))
-        
-        conn.commit()
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'success': True,
-            'allocated': allocated
-        })
-        
-    except Exception as e:
-        conn.rollback()
-        cur.close()
-        conn.close()
-        return jsonify({
-            'success': False,
-            'error': str(e)
-        }), 400
-
-@app.route('/api/resources/check-availability', methods=['GET'])
-@login_required
-def check_resource_availability():
-    """Prüft verfügbare Ressourcen"""
-    resource_type = request.args.get('type', '')
-    count = request.args.get('count', 10, type=int)
-    show_test = request.args.get('show_test', 'false').lower() == 'true'
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    if resource_type:
-        # Spezifische Ressourcen für einen Typ
-        cur.execute("""
-            SELECT id, resource_value
-            FROM resource_pools
-            WHERE status = 'available' 
-            AND resource_type = %s
-            AND is_test = %s
-            ORDER BY resource_value
-            LIMIT %s
-        """, (resource_type, show_test, count))
-        
-        resources = []
-        for row in cur.fetchall():
-            resources.append({
-                'id': row[0],
-                'value': row[1]
-            })
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify({
-            'available': resources,
-            'type': resource_type,
-            'count': len(resources)
-        })
-    else:
-        # Zusammenfassung aller Typen
-        cur.execute("""
-            SELECT 
-                resource_type,
-                COUNT(*) as available
-            FROM resource_pools
-            WHERE status = 'available'
-            AND is_test = %s
-            GROUP BY resource_type
-        """, (show_test,))
-        
-        availability = {}
-        for row in cur.fetchall():
-            availability[row[0]] = row[1]
-        
-        cur.close()
-        conn.close()
-        
-        return jsonify(availability)
-
-@app.route('/api/global-search', methods=['GET'])
-@login_required
-def global_search():
-    """Global search API endpoint for searching customers and licenses"""
-    query = request.args.get('q', '').strip()
-    
-    if not query or len(query) < 2:
-        return jsonify({'customers': [], 'licenses': []})
-    
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Search pattern with wildcards
-    search_pattern = f'%{query}%'
-    
-    # Search customers
-    cur.execute("""
-        SELECT id, name, email, company_name
-        FROM customers
-        WHERE (LOWER(name) LIKE LOWER(%s) 
-               OR LOWER(email) LIKE LOWER(%s)
-               OR LOWER(company_name) LIKE LOWER(%s))
-        AND is_test = FALSE
-        ORDER BY name
-        LIMIT 5
-    """, (search_pattern, search_pattern, search_pattern))
-    
-    customers = []
-    for row in cur.fetchall():
-        customers.append({
-            'id': row[0],
-            'name': row[1],
-            'email': row[2],
-            'company_name': row[3]
-        })
-    
-    # Search licenses
-    cur.execute("""
-        SELECT l.id, l.license_key, c.name as customer_name
-        FROM licenses l
-        JOIN customers c ON l.customer_id = c.id
-        WHERE LOWER(l.license_key) LIKE LOWER(%s)
-        AND l.is_test = FALSE
-        ORDER BY l.created_at DESC
-        LIMIT 5
-    """, (search_pattern,))
-    
-    licenses = []
-    for row in cur.fetchall():
-        licenses.append({
-            'id': row[0],
-            'license_key': row[1],
-            'customer_name': row[2]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    return jsonify({
-        'customers': customers,
-        'licenses': licenses
-    })
-
-@app.route('/resources/history/<int:resource_id>')
-@login_required
-def resource_history(resource_id):
-    """Zeigt die komplette Historie einer Ressource"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Get complete resource info using named columns
-    cur.execute("""
-        SELECT id, resource_type, resource_value, status, allocated_to_license,
-               status_changed_at, status_changed_by, quarantine_reason, 
-               quarantine_until, created_at, notes
-        FROM resource_pools
-        WHERE id = %s
-    """, (resource_id,))
-    row = cur.fetchone()
-    
-    if not row:
-        flash('Ressource nicht gefunden', 'error')
-        return redirect(url_for('resources'))
-    
-    # Create resource object with named attributes
-    resource = {
-        'id': row[0],
-        'resource_type': row[1],
-        'resource_value': row[2],
-        'status': row[3],
-        'allocated_to_license': row[4],
-        'status_changed_at': row[5],
-        'status_changed_by': row[6],
-        'quarantine_reason': row[7],
-        'quarantine_until': row[8],
-        'created_at': row[9],
-        'notes': row[10]
-    }
-    
-    # Get license info if allocated
-    license_info = None
-    if resource['allocated_to_license']:
-        cur.execute("SELECT license_key FROM licenses WHERE id = %s", 
-                   (resource['allocated_to_license'],))
-        lic = cur.fetchone()
-        if lic:
-            license_info = {'license_key': lic[0]}
-    
-    # Get history with named columns
-    cur.execute("""
-        SELECT 
-            rh.action,
-            rh.action_by,
-            rh.action_at,
-            rh.details,
-            rh.license_id,
-            rh.ip_address
-        FROM resource_history rh
-        WHERE rh.resource_id = %s
-        ORDER BY rh.action_at DESC
-    """, (resource_id,))
-    
-    history = []
-    for row in cur.fetchall():
-        history.append({
-            'action': row[0],
-            'action_by': row[1],
-            'action_at': row[2],
-            'details': row[3],
-            'license_id': row[4],
-            'ip_address': row[5]
-        })
-    
-    cur.close()
-    conn.close()
-    
-    # Convert to object-like for template
-    class ResourceObj:
-        def __init__(self, data):
-            for key, value in data.items():
-                setattr(self, key, value)
-    
-    resource_obj = ResourceObj(resource)
-    history_objs = [ResourceObj(h) for h in history]
-    
-    return render_template('resource_history.html',
-                         resource=resource_obj,
-                         license_info=license_info,
-                         history=history_objs)
-
-@app.route('/resources/metrics')
-@login_required
-def resources_metrics():
-    """Dashboard für Resource Metrics und Reports"""
-    conn = get_connection()
-    cur = conn.cursor()
-    
-    # Overall stats with fallback values
-    cur.execute("""
-        SELECT 
-            COUNT(DISTINCT resource_id) as total_resources,
-            COALESCE(AVG(performance_score), 0) as avg_performance,
-            COALESCE(SUM(cost), 0) as total_cost,
-            COALESCE(SUM(revenue), 0) as total_revenue,
-            COALESCE(SUM(issues_count), 0) as total_issues
-        FROM resource_metrics
-        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
-    """)
-    row = cur.fetchone()
-    
-    # Calculate ROI
-    roi = 0
-    if row[2] > 0:  # if total_cost > 0
-        roi = row[3] / row[2]  # revenue / cost
-    
-    stats = {
-        'total_resources': row[0] or 0,
-        'avg_performance': row[1] or 0,
-        'total_cost': row[2] or 0,
-        'total_revenue': row[3] or 0,
-        'total_issues': row[4] or 0,
-        'roi': roi
-    }
-    
-    # Performance by type
-    cur.execute("""
-        SELECT 
-            rp.resource_type,
-            COALESCE(AVG(rm.performance_score), 0) as avg_score,
-            COUNT(DISTINCT rp.id) as resource_count
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        GROUP BY rp.resource_type
-        ORDER BY rp.resource_type
-    """)
-    performance_by_type = cur.fetchall()
-    
-    # Utilization data
-    cur.execute("""
-        SELECT 
-            resource_type,
-            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-            COUNT(*) as total,
-            ROUND(COUNT(*) FILTER (WHERE status = 'allocated') * 100.0 / COUNT(*), 1) as allocated_percent
-        FROM resource_pools
-        GROUP BY resource_type
-    """)
-    utilization_rows = cur.fetchall()
-    utilization_data = [
-        {
-            'type': row[0].upper(),
-            'allocated': row[1],
-            'total': row[2],
-            'allocated_percent': row[3]
-        }
-        for row in utilization_rows
-    ]
-    
-    # Top performing resources
-    cur.execute("""
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            COALESCE(AVG(rm.performance_score), 0) as avg_score,
-            COALESCE(SUM(rm.revenue), 0) as total_revenue,
-            COALESCE(SUM(rm.cost), 1) as total_cost,
-            CASE 
-                WHEN COALESCE(SUM(rm.cost), 0) = 0 THEN 0
-                ELSE COALESCE(SUM(rm.revenue), 0) / COALESCE(SUM(rm.cost), 1)
-            END as roi
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        WHERE rp.status != 'quarantine'
-        GROUP BY rp.id, rp.resource_type, rp.resource_value
-        HAVING AVG(rm.performance_score) IS NOT NULL
-        ORDER BY avg_score DESC
-        LIMIT 10
-    """)
-    top_rows = cur.fetchall()
-    top_performers = [
-        {
-            'id': row[0],
-            'resource_type': row[1],
-            'resource_value': row[2],
-            'avg_score': row[3],
-            'roi': row[6]
-        }
-        for row in top_rows
-    ]
-    
-    # Resources with issues
-    cur.execute("""
-        SELECT 
-            rp.id,
-            rp.resource_type,
-            rp.resource_value,
-            rp.status,
-            COALESCE(SUM(rm.issues_count), 0) as total_issues
-        FROM resource_pools rp
-        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
-            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        WHERE rm.issues_count > 0 OR rp.status = 'quarantine'
-        GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
-        HAVING SUM(rm.issues_count) > 0
-        ORDER BY total_issues DESC
-        LIMIT 10
-    """)
-    problem_rows = cur.fetchall()
-    problem_resources = [
-        {
-            'id': row[0],
-            'resource_type': row[1],
-            'resource_value': row[2],
-            'status': row[3],
-            'total_issues': row[4]
-        }
-        for row in problem_rows
-    ]
-    
-    # Daily metrics for trend chart (last 30 days)
-    cur.execute("""
-        SELECT 
-            metric_date,
-            COALESCE(AVG(performance_score), 0) as avg_performance,
-            COALESCE(SUM(issues_count), 0) as total_issues
-        FROM resource_metrics
-        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
-        GROUP BY metric_date
-        ORDER BY metric_date
-    """)
-    daily_rows = cur.fetchall()
-    daily_metrics = [
-        {
-            'date': row[0].strftime('%d.%m'),
-            'performance': float(row[1]),
-            'issues': int(row[2])
-        }
-        for row in daily_rows
-    ]
-    
-    cur.close()
-    conn.close()
-    
-    return render_template('resource_metrics.html',
-                         stats=stats,
-                         performance_by_type=performance_by_type,
-                         utilization_data=utilization_data,
-                         top_performers=top_performers,
-                         problem_resources=problem_resources,
-                         daily_metrics=daily_metrics)
-
-@app.route('/resources/report', methods=['GET'])
-@login_required
-def resources_report():
-    """Generiert Ressourcen-Reports oder zeigt Report-Formular"""
-    # Prüfe ob Download angefordert wurde
-    if request.args.get('download') == 'true':
-        report_type = request.args.get('type', 'usage')
-        format_type = request.args.get('format', 'excel')
-        date_from = request.args.get('from', (datetime.now(ZoneInfo("Europe/Berlin")) - timedelta(days=30)).strftime('%Y-%m-%d'))
-        date_to = request.args.get('to', datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d'))
-        
-        conn = get_connection()
-        cur = conn.cursor()
-        
-        if report_type == 'usage':
-            # Auslastungsreport
-            query = """
-                SELECT 
-                    rp.resource_type,
-                    rp.resource_value,
-                    rp.status,
-                    COUNT(DISTINCT rh.license_id) as unique_licenses,
-                    COUNT(rh.id) as total_allocations,
-                    MIN(rh.action_at) as first_used,
-                    MAX(rh.action_at) as last_used
-                FROM resource_pools rp
-                LEFT JOIN resource_history rh ON rp.id = rh.resource_id 
-                    AND rh.action = 'allocated'
-                    AND rh.action_at BETWEEN %s AND %s
-                GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
-                ORDER BY rp.resource_type, total_allocations DESC
-            """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Typ', 'Ressource', 'Status', 'Unique Lizenzen', 'Gesamt Zuweisungen', 'Erste Nutzung', 'Letzte Nutzung']
-            
-        elif report_type == 'performance':
-            # Performance-Report
-            query = """
-                SELECT 
-                    rp.resource_type,
-                rp.resource_value,
-                AVG(rm.performance_score) as avg_performance,
-                SUM(rm.usage_count) as total_usage,
-                SUM(rm.revenue) as total_revenue,
-                SUM(rm.cost) as total_cost,
-                SUM(rm.revenue - rm.cost) as profit,
-                SUM(rm.issues_count) as total_issues
-            FROM resource_pools rp
-            JOIN resource_metrics rm ON rp.id = rm.resource_id
-            WHERE rm.metric_date BETWEEN %s AND %s
-            GROUP BY rp.id, rp.resource_type, rp.resource_value
-            ORDER BY profit DESC
-        """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Typ', 'Ressource', 'Durchschn. Performance', 'Gesamt Nutzung', 'Umsatz', 'Kosten', 'Gewinn', 'Issues']
-            
-        elif report_type == 'compliance':
-            # Compliance-Report
-            query = """
-            SELECT 
-                rh.action_at,
-                rh.action,
-                rh.action_by,
-                rp.resource_type,
-                rp.resource_value,
-                l.license_key,
-                c.name as customer_name,
-                rh.ip_address
-            FROM resource_history rh
-            JOIN resource_pools rp ON rh.resource_id = rp.id
-            LEFT JOIN licenses l ON rh.license_id = l.id
-            LEFT JOIN customers c ON l.customer_id = c.id
-            WHERE rh.action_at BETWEEN %s AND %s
-                ORDER BY rh.action_at DESC
-            """
-            cur.execute(query, (date_from, date_to))
-            columns = ['Zeit', 'Aktion', 'Von', 'Typ', 'Ressource', 'Lizenz', 'Kunde', 'IP-Adresse']
-        
-        else:  # inventory report
-            # Inventar-Report
-            query = """
-                SELECT 
-                    resource_type,
-                    COUNT(*) FILTER (WHERE status = 'available') as available,
-                    COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
-                    COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
-                    COUNT(*) as total
-                FROM resource_pools
-                GROUP BY resource_type
-                ORDER BY resource_type
-            """
-            cur.execute(query)
-            columns = ['Typ', 'Verfügbar', 'Zugeteilt', 'Quarantäne', 'Gesamt']
-        
-        # Convert to DataFrame
-        data = cur.fetchall()
-        df = pd.DataFrame(data, columns=columns)
-        
-        cur.close()
-        conn.close()
-        
-        # Generate file
-        timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
-        filename = f"resource_report_{report_type}_{timestamp}"
-        
-        if format_type == 'excel':
-            output = io.BytesIO()
-            with pd.ExcelWriter(output, engine='openpyxl') as writer:
-                df.to_excel(writer, sheet_name='Report', index=False)
-                
-                # Auto-adjust columns width
-                worksheet = writer.sheets['Report']
-                for column in worksheet.columns:
-                    max_length = 0
-                    column = [cell for cell in column]
-                    for cell in column:
-                        try:
-                            if len(str(cell.value)) > max_length:
-                                max_length = len(str(cell.value))
-                        except:
-                            pass
-                    adjusted_width = (max_length + 2)
-                    worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
-            
-            output.seek(0)
-            
-            log_audit('EXPORT', 'resource_report', None,
-                     new_values={'type': report_type, 'format': 'excel', 'rows': len(df)},
-                     additional_info=f"Resource Report {report_type} exportiert")
-            
-            return send_file(output, 
-                            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-                            as_attachment=True,
-                            download_name=f'{filename}.xlsx')
-        
-        else:  # CSV
-            output = io.StringIO()
-            df.to_csv(output, index=False, sep=';', encoding='utf-8-sig')
-            output.seek(0)
-            
-            log_audit('EXPORT', 'resource_report', None,
-                     new_values={'type': report_type, 'format': 'csv', 'rows': len(df)},
-                     additional_info=f"Resource Report {report_type} exportiert")
-            
-            return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')),
-                            mimetype='text/csv',
-                            as_attachment=True,
-                            download_name=f'{filename}.csv')
-    
-    # Wenn kein Download, zeige Report-Formular
-    return render_template('resource_report.html',
-                         datetime=datetime,
-                         timedelta=timedelta,
-                         username=session.get('username'))
-
-if __name__ == "__main__":
-    app.run(host="0.0.0.0", port=5000)
+import os
+import time
+import json
+import logging
+import requests
+from io import BytesIO
+from datetime import datetime, timedelta
+from zoneinfo import ZoneInfo
+from pathlib import Path
+
+from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash
+from flask_session import Session
+from werkzeug.middleware.proxy_fix import ProxyFix
+from apscheduler.schedulers.background import BackgroundScheduler
+import pandas as pd
+from psycopg2.extras import Json
+
+# Import our new modules
+import config
+from db import get_connection, get_db_connection, get_db_cursor, execute_query
+from auth.decorators import login_required
+from auth.password import hash_password, verify_password
+from auth.two_factor import (
+    generate_totp_secret, generate_qr_code, verify_totp,
+    generate_backup_codes, hash_backup_code, verify_backup_code
+)
+from auth.rate_limiting import (
+    get_client_ip, check_ip_blocked, record_failed_attempt,
+    reset_login_attempts, get_login_attempts
+)
+from utils.audit import log_audit
+from utils.license import generate_license_key, validate_license_key
+from utils.backup import create_backup, restore_backup, get_or_create_encryption_key
+from utils.export import (
+    create_excel_export, format_datetime_for_export,
+    prepare_license_export_data, prepare_customer_export_data,
+    prepare_session_export_data, prepare_audit_export_data
+)
+
+app = Flask(__name__)
+# Load configuration from config module
+app.config['SECRET_KEY'] = config.SECRET_KEY
+app.config['SESSION_TYPE'] = config.SESSION_TYPE
+app.config['JSON_AS_ASCII'] = config.JSON_AS_ASCII
+app.config['JSONIFY_MIMETYPE'] = config.JSONIFY_MIMETYPE
+app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
+app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY
+app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE
+app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
+app.config['SESSION_COOKIE_NAME'] = config.SESSION_COOKIE_NAME
+app.config['SESSION_REFRESH_EACH_REQUEST'] = config.SESSION_REFRESH_EACH_REQUEST
+Session(app)
+
+# ProxyFix für korrekte IP-Adressen hinter Nginx
+app.wsgi_app = ProxyFix(
+    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
+)
+
+# Configuration is now loaded from config module
+
+# Scheduler für automatische Backups
+scheduler = BackgroundScheduler()
+scheduler.start()
+
+# Logging konfigurieren
+logging.basicConfig(level=logging.INFO)
+
+
+# Login decorator
+def login_required(f):
+    @wraps(f)
+    def decorated_function(*args, **kwargs):
+        if 'logged_in' not in session:
+            return redirect(url_for('login'))
+        
+        # Prüfe ob Session abgelaufen ist
+        if 'last_activity' in session:
+            last_activity = datetime.fromisoformat(session['last_activity'])
+            time_since_activity = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) - last_activity
+            
+            # Debug-Logging
+            app.logger.info(f"Session check for {session.get('username', 'unknown')}: "
+                          f"Last activity: {last_activity}, "
+                          f"Time since: {time_since_activity.total_seconds()} seconds")
+            
+            if time_since_activity > timedelta(minutes=5):
+                # Session abgelaufen - Logout
+                username = session.get('username', 'unbekannt')
+                app.logger.info(f"Session timeout for user {username} - auto logout")
+                # Audit-Log für automatischen Logout (vor session.clear()!)
+                try:
+                    log_audit('AUTO_LOGOUT', 'session', additional_info={'reason': 'Session timeout (5 minutes)', 'username': username})
+                except:
+                    pass
+                session.clear()
+                flash('Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.', 'warning')
+                return redirect(url_for('login'))
+        
+        # Aktivität NICHT automatisch aktualisieren
+        # Nur bei expliziten Benutzeraktionen (wird vom Heartbeat gemacht)
+        return f(*args, **kwargs)
+    return decorated_function
+
+# DB-Verbindung mit UTF-8 Encoding
+def get_connection():
+    conn = psycopg2.connect(
+        host=os.getenv("POSTGRES_HOST", "postgres"),
+        port=os.getenv("POSTGRES_PORT", "5432"),
+        dbname=os.getenv("POSTGRES_DB"),
+        user=os.getenv("POSTGRES_USER"),
+        password=os.getenv("POSTGRES_PASSWORD"),
+        options='-c client_encoding=UTF8'
+    )
+    conn.set_client_encoding('UTF8')
+    return conn
+
+# User Authentication Helper Functions
+def hash_password(password):
+    """Hash a password using bcrypt"""
+    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+
+def verify_password(password, hashed):
+    """Verify a password against its hash"""
+    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
+
+def get_user_by_username(username):
+    """Get user from database by username"""
+    conn = get_connection()
+    cur = conn.cursor()
+    try:
+        cur.execute("""
+            SELECT id, username, password_hash, email, totp_secret, totp_enabled, 
+                   backup_codes, last_password_change, failed_2fa_attempts
+            FROM users WHERE username = %s
+        """, (username,))
+        user = cur.fetchone()
+        if user:
+            return {
+                'id': user[0],
+                'username': user[1],
+                'password_hash': user[2],
+                'email': user[3],
+                'totp_secret': user[4],
+                'totp_enabled': user[5],
+                'backup_codes': user[6],
+                'last_password_change': user[7],
+                'failed_2fa_attempts': user[8]
+            }
+        return None
+    finally:
+        cur.close()
+        conn.close()
+
+def generate_totp_secret():
+    """Generate a new TOTP secret"""
+    return pyotp.random_base32()
+
+def generate_qr_code(username, totp_secret):
+    """Generate QR code for TOTP setup"""
+    totp_uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
+        name=username,
+        issuer_name='V2 Admin Panel'
+    )
+    
+    qr = qrcode.QRCode(version=1, box_size=10, border=5)
+    qr.add_data(totp_uri)
+    qr.make(fit=True)
+    
+    img = qr.make_image(fill_color="black", back_color="white")
+    buf = BytesIO()
+    img.save(buf, format='PNG')
+    buf.seek(0)
+    
+    return base64.b64encode(buf.getvalue()).decode()
+
+def verify_totp(totp_secret, token):
+    """Verify a TOTP token"""
+    totp = pyotp.TOTP(totp_secret)
+    return totp.verify(token, valid_window=1)
+
+def generate_backup_codes(count=8):
+    """Generate backup codes for 2FA recovery"""
+    codes = []
+    for _ in range(count):
+        code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+        codes.append(code)
+    return codes
+
+def hash_backup_code(code):
+    """Hash a backup code for storage"""
+    return hashlib.sha256(code.encode()).hexdigest()
+
+def verify_backup_code(code, hashed_codes):
+    """Verify a backup code against stored hashes"""
+    code_hash = hashlib.sha256(code.encode()).hexdigest()
+    return code_hash in hashed_codes
+
+# Audit-Log-Funktion
+def log_audit(action, entity_type, entity_id=None, old_values=None, new_values=None, additional_info=None):
+    """Protokolliert Änderungen im Audit-Log"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        username = session.get('username', 'system')
+        ip_address = get_client_ip() if request else None
+        user_agent = request.headers.get('User-Agent') if request else None
+        
+        # Debug logging
+        app.logger.info(f"Audit log - IP address captured: {ip_address}, Action: {action}, User: {username}")
+        
+        # Konvertiere Dictionaries zu JSONB
+        old_json = Json(old_values) if old_values else None
+        new_json = Json(new_values) if new_values else None
+        
+        cur.execute("""
+            INSERT INTO audit_log 
+            (username, action, entity_type, entity_id, old_values, new_values, 
+             ip_address, user_agent, additional_info)
+            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+        """, (username, action, entity_type, entity_id, old_json, new_json, 
+              ip_address, user_agent, additional_info))
+        
+        conn.commit()
+    except Exception as e:
+        print(f"Audit log error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+
+# Verschlüsselungs-Funktionen
+def get_or_create_encryption_key():
+    """Holt oder erstellt einen Verschlüsselungsschlüssel"""
+    key_file = BACKUP_DIR / ".backup_key"
+    
+    # Versuche Key aus Umgebungsvariable zu lesen
+    env_key = os.getenv("BACKUP_ENCRYPTION_KEY")
+    if env_key:
+        try:
+            # Validiere den Key
+            Fernet(env_key.encode())
+            return env_key.encode()
+        except:
+            pass
+    
+    # Wenn kein gültiger Key in ENV, prüfe Datei
+    if key_file.exists():
+        return key_file.read_bytes()
+    
+    # Erstelle neuen Key
+    key = Fernet.generate_key()
+    key_file.write_bytes(key)
+    logging.info("Neuer Backup-Verschlüsselungsschlüssel erstellt")
+    return key
+
+# Backup-Funktionen
+def create_backup(backup_type="manual", created_by=None):
+    """Erstellt ein verschlüsseltes Backup der Datenbank"""
+    start_time = time.time()
+    timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")
+    filename = f"backup_v2docker_{timestamp}_encrypted.sql.gz.enc"
+    filepath = BACKUP_DIR / filename
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Backup-Eintrag erstellen
+    cur.execute("""
+        INSERT INTO backup_history 
+        (filename, filepath, backup_type, status, created_by, is_encrypted)
+        VALUES (%s, %s, %s, %s, %s, %s)
+        RETURNING id
+    """, (filename, str(filepath), backup_type, 'in_progress', 
+          created_by or 'system', True))
+    backup_id = cur.fetchone()[0]
+    conn.commit()
+    
+    try:
+        # PostgreSQL Dump erstellen
+        dump_command = [
+            'pg_dump',
+            '-h', os.getenv("POSTGRES_HOST", "postgres"),
+            '-p', os.getenv("POSTGRES_PORT", "5432"),
+            '-U', os.getenv("POSTGRES_USER"),
+            '-d', os.getenv("POSTGRES_DB"),
+            '--no-password',
+            '--verbose'
+        ]
+        
+        # PGPASSWORD setzen
+        env = os.environ.copy()
+        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
+        
+        # Dump ausführen
+        result = subprocess.run(dump_command, capture_output=True, text=True, env=env)
+        
+        if result.returncode != 0:
+            raise Exception(f"pg_dump failed: {result.stderr}")
+        
+        dump_data = result.stdout.encode('utf-8')
+        
+        # Komprimieren
+        compressed_data = gzip.compress(dump_data)
+        
+        # Verschlüsseln
+        key = get_or_create_encryption_key()
+        f = Fernet(key)
+        encrypted_data = f.encrypt(compressed_data)
+        
+        # Speichern
+        filepath.write_bytes(encrypted_data)
+        
+        # Statistiken sammeln
+        cur.execute("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'")
+        tables_count = cur.fetchone()[0]
+        
+        cur.execute("""
+            SELECT SUM(n_live_tup) 
+            FROM pg_stat_user_tables
+        """)
+        records_count = cur.fetchone()[0] or 0
+        
+        duration = time.time() - start_time
+        filesize = filepath.stat().st_size
+        
+        # Backup-Eintrag aktualisieren
+        cur.execute("""
+            UPDATE backup_history 
+            SET status = %s, filesize = %s, tables_count = %s, 
+                records_count = %s, duration_seconds = %s
+            WHERE id = %s
+        """, ('success', filesize, tables_count, records_count, duration, backup_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('BACKUP', 'database', backup_id,
+                 additional_info=f"Backup erstellt: {filename} ({filesize} bytes)")
+        
+        # E-Mail-Benachrichtigung (wenn konfiguriert)
+        send_backup_notification(True, filename, filesize, duration)
+        
+        logging.info(f"Backup erfolgreich erstellt: {filename}")
+        return True, filename
+        
+    except Exception as e:
+        # Fehler protokollieren
+        cur.execute("""
+            UPDATE backup_history 
+            SET status = %s, error_message = %s, duration_seconds = %s
+            WHERE id = %s
+        """, ('failed', str(e), time.time() - start_time, backup_id))
+        conn.commit()
+        
+        logging.error(f"Backup fehlgeschlagen: {e}")
+        send_backup_notification(False, filename, error=str(e))
+        
+        return False, str(e)
+    
+    finally:
+        cur.close()
+        conn.close()
+
+def restore_backup(backup_id, encryption_key=None):
+    """Stellt ein Backup wieder her"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Backup-Info abrufen
+        cur.execute("""
+            SELECT filename, filepath, is_encrypted 
+            FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        backup_info = cur.fetchone()
+        
+        if not backup_info:
+            raise Exception("Backup nicht gefunden")
+        
+        filename, filepath, is_encrypted = backup_info
+        filepath = Path(filepath)
+        
+        if not filepath.exists():
+            raise Exception("Backup-Datei nicht gefunden")
+        
+        # Datei lesen
+        encrypted_data = filepath.read_bytes()
+        
+        # Entschlüsseln
+        if is_encrypted:
+            key = encryption_key.encode() if encryption_key else get_or_create_encryption_key()
+            try:
+                f = Fernet(key)
+                compressed_data = f.decrypt(encrypted_data)
+            except:
+                raise Exception("Entschlüsselung fehlgeschlagen. Falsches Passwort?")
+        else:
+            compressed_data = encrypted_data
+        
+        # Dekomprimieren
+        dump_data = gzip.decompress(compressed_data)
+        sql_commands = dump_data.decode('utf-8')
+        
+        # Bestehende Verbindungen schließen
+        cur.close()
+        conn.close()
+        
+        # Datenbank wiederherstellen
+        restore_command = [
+            'psql',
+            '-h', os.getenv("POSTGRES_HOST", "postgres"),
+            '-p', os.getenv("POSTGRES_PORT", "5432"),
+            '-U', os.getenv("POSTGRES_USER"),
+            '-d', os.getenv("POSTGRES_DB"),
+            '--no-password'
+        ]
+        
+        env = os.environ.copy()
+        env['PGPASSWORD'] = os.getenv("POSTGRES_PASSWORD")
+        
+        result = subprocess.run(restore_command, input=sql_commands, 
+                              capture_output=True, text=True, env=env)
+        
+        if result.returncode != 0:
+            raise Exception(f"Wiederherstellung fehlgeschlagen: {result.stderr}")
+        
+        # Audit-Log (neue Verbindung)
+        log_audit('RESTORE', 'database', backup_id,
+                 additional_info=f"Backup wiederhergestellt: {filename}")
+        
+        return True, "Backup erfolgreich wiederhergestellt"
+        
+    except Exception as e:
+        logging.error(f"Wiederherstellung fehlgeschlagen: {e}")
+        return False, str(e)
+
+def send_backup_notification(success, filename, filesize=None, duration=None, error=None):
+    """Sendet E-Mail-Benachrichtigung (wenn konfiguriert)"""
+    if not os.getenv("EMAIL_ENABLED", "false").lower() == "true":
+        return
+    
+    # E-Mail-Funktion vorbereitet aber deaktiviert
+    # TODO: Implementieren wenn E-Mail-Server konfiguriert ist
+    logging.info(f"E-Mail-Benachrichtigung vorbereitet: Backup {'erfolgreich' if success else 'fehlgeschlagen'}")
+
+# Scheduled Backup Job
+def scheduled_backup():
+    """Führt ein geplantes Backup aus"""
+    logging.info("Starte geplantes Backup...")
+    create_backup(backup_type="scheduled", created_by="scheduler")
+
+# Scheduler konfigurieren - täglich um 3:00 Uhr
+scheduler.add_job(
+    scheduled_backup,
+    'cron',
+    hour=3,
+    minute=0,
+    id='daily_backup',
+    replace_existing=True
+)
+
+# Rate-Limiting Funktionen
+def get_client_ip():
+    """Ermittelt die echte IP-Adresse des Clients"""
+    # Debug logging
+    app.logger.info(f"Headers - X-Real-IP: {request.headers.get('X-Real-IP')}, X-Forwarded-For: {request.headers.get('X-Forwarded-For')}, Remote-Addr: {request.remote_addr}")
+    
+    # Try X-Real-IP first (set by nginx)
+    if request.headers.get('X-Real-IP'):
+        return request.headers.get('X-Real-IP')
+    # Then X-Forwarded-For
+    elif request.headers.get('X-Forwarded-For'):
+        # X-Forwarded-For can contain multiple IPs, take the first one
+        return request.headers.get('X-Forwarded-For').split(',')[0].strip()
+    # Fallback to remote_addr
+    else:
+        return request.remote_addr
+
+def check_ip_blocked(ip_address):
+    """Prüft ob eine IP-Adresse gesperrt ist"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT blocked_until FROM login_attempts
+        WHERE ip_address = %s AND blocked_until IS NOT NULL
+    """, (ip_address,))
+    
+    result = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    if result and result[0]:
+        if result[0] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None):
+            return True, result[0]
+    return False, None
+
+def record_failed_attempt(ip_address, username):
+    """Zeichnet einen fehlgeschlagenen Login-Versuch auf"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Random Fehlermeldung
+    error_message = random.choice(FAIL_MESSAGES)
+    
+    try:
+        # Prüfen ob IP bereits existiert
+        cur.execute("""
+            SELECT attempt_count FROM login_attempts
+            WHERE ip_address = %s
+        """, (ip_address,))
+        
+        result = cur.fetchone()
+        
+        if result:
+            # Update bestehenden Eintrag
+            new_count = result[0] + 1
+            blocked_until = None
+            
+            if new_count >= MAX_LOGIN_ATTEMPTS:
+                blocked_until = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) + timedelta(hours=BLOCK_DURATION_HOURS)
+                # E-Mail-Benachrichtigung (wenn aktiviert)
+                if os.getenv("EMAIL_ENABLED", "false").lower() == "true":
+                    send_security_alert_email(ip_address, username, new_count)
+            
+            cur.execute("""
+                UPDATE login_attempts
+                SET attempt_count = %s,
+                    last_attempt = CURRENT_TIMESTAMP,
+                    blocked_until = %s,
+                    last_username_tried = %s,
+                    last_error_message = %s
+                WHERE ip_address = %s
+            """, (new_count, blocked_until, username, error_message, ip_address))
+        else:
+            # Neuen Eintrag erstellen
+            cur.execute("""
+                INSERT INTO login_attempts 
+                (ip_address, attempt_count, last_username_tried, last_error_message)
+                VALUES (%s, 1, %s, %s)
+            """, (ip_address, username, error_message))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('LOGIN_FAILED', 'user', 
+                 additional_info=f"IP: {ip_address}, User: {username}, Message: {error_message}")
+        
+    except Exception as e:
+        print(f"Rate limiting error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+    
+    return error_message
+
+def reset_login_attempts(ip_address):
+    """Setzt die Login-Versuche für eine IP zurück"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        cur.execute("""
+            DELETE FROM login_attempts
+            WHERE ip_address = %s
+        """, (ip_address,))
+        conn.commit()
+    except Exception as e:
+        print(f"Reset attempts error: {e}")
+        conn.rollback()
+    finally:
+        cur.close()
+        conn.close()
+
+def get_login_attempts(ip_address):
+    """Gibt die Anzahl der Login-Versuche für eine IP zurück"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT attempt_count FROM login_attempts
+        WHERE ip_address = %s
+    """, (ip_address,))
+    
+    result = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    return result[0] if result else 0
+
+def send_security_alert_email(ip_address, username, attempt_count):
+    """Sendet eine Sicherheitswarnung per E-Mail"""
+    subject = f"⚠️ SICHERHEITSWARNUNG: {attempt_count} fehlgeschlagene Login-Versuche"
+    body = f"""
+    WARNUNG: Mehrere fehlgeschlagene Login-Versuche erkannt!
+    
+    IP-Adresse: {ip_address}
+    Versuchter Benutzername: {username}
+    Anzahl Versuche: {attempt_count}
+    Zeit: {datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d %H:%M:%S')}
+    
+    Die IP-Adresse wurde für 24 Stunden gesperrt.
+    
+    Dies ist eine automatische Nachricht vom v2-Docker Admin Panel.
+    """
+    
+    # TODO: E-Mail-Versand implementieren wenn SMTP konfiguriert
+    logging.warning(f"Sicherheitswarnung: {attempt_count} fehlgeschlagene Versuche von IP {ip_address}")
+    print(f"E-Mail würde gesendet: {subject}")
+
+def verify_recaptcha(response):
+    """Verifiziert die reCAPTCHA v2 Response mit Google"""
+    secret_key = os.getenv('RECAPTCHA_SECRET_KEY')
+    
+    # Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC)
+    if not secret_key:
+        logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen")
+        return True
+    
+    # Verifizierung bei Google
+    try:
+        verify_url = 'https://www.google.com/recaptcha/api/siteverify'
+        data = {
+            'secret': secret_key,
+            'response': response
+        }
+        
+        # Timeout für Request setzen
+        r = requests.post(verify_url, data=data, timeout=5)
+        result = r.json()
+        
+        # Log für Debugging
+        if not result.get('success'):
+            logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}")
+        
+        return result.get('success', False)
+    
+    except requests.exceptions.RequestException as e:
+        logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}")
+        # Bei Netzwerkfehlern CAPTCHA als bestanden werten
+        return True
+    except Exception as e:
+        logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}")
+        return False
+
+def generate_license_key(license_type='full'):
+    """
+    Generiert einen Lizenzschlüssel im Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ
+    
+    AF = Account Factory (Produktkennung)
+    F/T = F für Fullversion, T für Testversion
+    YYYY = Jahr
+    MM = Monat
+    XXXX-YYYY-ZZZZ = Zufällige alphanumerische Zeichen
+    """
+    # Erlaubte Zeichen (ohne verwirrende wie 0/O, 1/I/l)
+    chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
+    
+    # Datum-Teil
+    now = datetime.now(ZoneInfo("Europe/Berlin"))
+    date_part = now.strftime('%Y%m')
+    type_char = 'F' if license_type == 'full' else 'T'
+    
+    # Zufällige Teile generieren (3 Blöcke à 4 Zeichen)
+    parts = []
+    for _ in range(3):
+        part = ''.join(secrets.choice(chars) for _ in range(4))
+        parts.append(part)
+    
+    # Key zusammensetzen
+    key = f"AF-{type_char}-{date_part}-{parts[0]}-{parts[1]}-{parts[2]}"
+    
+    return key
+
+def validate_license_key(key):
+    """
+    Validiert das License Key Format
+    Erwartet: AF-F-YYYYMM-XXXX-YYYY-ZZZZ oder AF-T-YYYYMM-XXXX-YYYY-ZZZZ
+    """
+    if not key:
+        return False
+    
+    # Pattern für das neue Format
+    # AF- (fest) + F oder T + - + 6 Ziffern (YYYYMM) + - + 4 Zeichen + - + 4 Zeichen + - + 4 Zeichen
+    pattern = r'^AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$'
+    
+    # Großbuchstaben für Vergleich
+    return bool(re.match(pattern, key.upper()))
+
+@app.route("/login", methods=["GET", "POST"])
+def login():
+    # Timing-Attack Schutz - Start Zeit merken
+    start_time = time.time()
+    
+    # IP-Adresse ermitteln
+    ip_address = get_client_ip()
+    
+    # Prüfen ob IP gesperrt ist
+    is_blocked, blocked_until = check_ip_blocked(ip_address)
+    if is_blocked:
+        time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
+        error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
+        return render_template("login.html", error=error_msg, error_type="blocked")
+    
+    # Anzahl bisheriger Versuche
+    attempt_count = get_login_attempts(ip_address)
+    
+    if request.method == "POST":
+        username = request.form.get("username")
+        password = request.form.get("password")
+        captcha_response = request.form.get("g-recaptcha-response")
+        
+        # CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
+        recaptcha_site_key = os.getenv('RECAPTCHA_SITE_KEY')
+        if attempt_count >= CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
+            if not captcha_response:
+                # Timing-Attack Schutz
+                elapsed = time.time() - start_time
+                if elapsed < 1.0:
+                    time.sleep(1.0 - elapsed)
+                return render_template("login.html", 
+                                     error="CAPTCHA ERFORDERLICH!", 
+                                     show_captcha=True,
+                                     error_type="captcha",
+                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     recaptcha_site_key=recaptcha_site_key)
+            
+            # CAPTCHA validieren
+            if not verify_recaptcha(captcha_response):
+                # Timing-Attack Schutz
+                elapsed = time.time() - start_time
+                if elapsed < 1.0:
+                    time.sleep(1.0 - elapsed)
+                return render_template("login.html", 
+                                     error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.", 
+                                     show_captcha=True,
+                                     error_type="captcha",
+                                     attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                                     recaptcha_site_key=recaptcha_site_key)
+        
+        # Check user in database first, fallback to env vars
+        user = get_user_by_username(username)
+        login_success = False
+        needs_2fa = False
+        
+        if user:
+            # Database user authentication
+            if verify_password(password, user['password_hash']):
+                login_success = True
+                needs_2fa = user['totp_enabled']
+        else:
+            # Fallback to environment variables for backward compatibility
+            admin1_user = os.getenv("ADMIN1_USERNAME")
+            admin1_pass = os.getenv("ADMIN1_PASSWORD")
+            admin2_user = os.getenv("ADMIN2_USERNAME")
+            admin2_pass = os.getenv("ADMIN2_PASSWORD")
+            
+            if ((username == admin1_user and password == admin1_pass) or 
+                (username == admin2_user and password == admin2_pass)):
+                login_success = True
+        
+        # Timing-Attack Schutz - Mindestens 1 Sekunde warten
+        elapsed = time.time() - start_time
+        if elapsed < 1.0:
+            time.sleep(1.0 - elapsed)
+        
+        if login_success:
+            # Erfolgreicher Login
+            if needs_2fa:
+                # Store temporary session for 2FA verification
+                session['temp_username'] = username
+                session['temp_user_id'] = user['id']
+                session['awaiting_2fa'] = True
+                return redirect(url_for('verify_2fa'))
+            else:
+                # Complete login without 2FA
+                session.permanent = True  # Aktiviert das Timeout
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user['id'] if user else None
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                reset_login_attempts(ip_address)
+                log_audit('LOGIN_SUCCESS', 'user', 
+                         additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
+                return redirect(url_for('dashboard'))
+        else:
+            # Fehlgeschlagener Login
+            error_message = record_failed_attempt(ip_address, username)
+            new_attempt_count = get_login_attempts(ip_address)
+            
+            # Prüfen ob jetzt gesperrt
+            is_now_blocked, _ = check_ip_blocked(ip_address)
+            if is_now_blocked:
+                log_audit('LOGIN_BLOCKED', 'security', 
+                         additional_info=f"IP {ip_address} wurde nach {MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
+            
+            return render_template("login.html", 
+                                 error=error_message,
+                                 show_captcha=(new_attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
+                                 error_type="failed",
+                                 attempts_left=max(0, MAX_LOGIN_ATTEMPTS - new_attempt_count),
+                                 recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
+    
+    # GET Request
+    return render_template("login.html", 
+                         show_captcha=(attempt_count >= CAPTCHA_AFTER_ATTEMPTS and os.getenv('RECAPTCHA_SITE_KEY')),
+                         attempts_left=max(0, MAX_LOGIN_ATTEMPTS - attempt_count),
+                         recaptcha_site_key=os.getenv('RECAPTCHA_SITE_KEY'))
+
+@app.route("/logout")
+def logout():
+    username = session.get('username', 'unknown')
+    log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
+    session.pop('logged_in', None)
+    session.pop('username', None)
+    session.pop('user_id', None)
+    session.pop('temp_username', None)
+    session.pop('temp_user_id', None)
+    session.pop('awaiting_2fa', None)
+    return redirect(url_for('login'))
+
+@app.route("/verify-2fa", methods=["GET", "POST"])
+def verify_2fa():
+    if not session.get('awaiting_2fa'):
+        return redirect(url_for('login'))
+    
+    if request.method == "POST":
+        token = request.form.get('token', '').replace(' ', '')
+        username = session.get('temp_username')
+        user_id = session.get('temp_user_id')
+        
+        if not username or not user_id:
+            flash('Session expired. Please login again.', 'error')
+            return redirect(url_for('login'))
+        
+        user = get_user_by_username(username)
+        if not user:
+            flash('User not found.', 'error')
+            return redirect(url_for('login'))
+        
+        # Check if it's a backup code
+        if len(token) == 8 and token.isupper():
+            # Try backup code
+            backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
+            if verify_backup_code(token, backup_codes):
+                # Remove used backup code
+                code_hash = hash_backup_code(token)
+                backup_codes.remove(code_hash)
+                
+                conn = get_connection()
+                cur = conn.cursor()
+                cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
+                          (json.dumps(backup_codes), user_id))
+                conn.commit()
+                cur.close()
+                conn.close()
+                
+                # Complete login
+                session.permanent = True
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user_id
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                session.pop('temp_username', None)
+                session.pop('temp_user_id', None)
+                session.pop('awaiting_2fa', None)
+                
+                flash('Login successful using backup code. Please generate new backup codes.', 'warning')
+                log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
+                return redirect(url_for('dashboard'))
+        else:
+            # Try TOTP token
+            if verify_totp(user['totp_secret'], token):
+                # Complete login
+                session.permanent = True
+                session['logged_in'] = True
+                session['username'] = username
+                session['user_id'] = user_id
+                session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+                session.pop('temp_username', None)
+                session.pop('temp_user_id', None)
+                session.pop('awaiting_2fa', None)
+                
+                log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
+                return redirect(url_for('dashboard'))
+        
+        # Failed verification
+        conn = get_connection()
+        cur = conn.cursor()
+        cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
+                  (datetime.now(), user_id))
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        flash('Invalid authentication code. Please try again.', 'error')
+        log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
+    
+    return render_template('verify_2fa.html')
+
+@app.route("/profile")
+@login_required
+def profile():
+    user = get_user_by_username(session['username'])
+    if not user:
+        # For environment-based users, redirect with message
+        flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info')
+        return redirect(url_for('dashboard'))
+    return render_template('profile.html', user=user)
+
+@app.route("/profile/change-password", methods=["POST"])
+@login_required
+def change_password():
+    current_password = request.form.get('current_password')
+    new_password = request.form.get('new_password')
+    confirm_password = request.form.get('confirm_password')
+    
+    user = get_user_by_username(session['username'])
+    
+    # Verify current password
+    if not verify_password(current_password, user['password_hash']):
+        flash('Current password is incorrect.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Check new password
+    if new_password != confirm_password:
+        flash('New passwords do not match.', 'error')
+        return redirect(url_for('profile'))
+    
+    if len(new_password) < 8:
+        flash('Password must be at least 8 characters long.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Update password
+    new_hash = hash_password(new_password)
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s",
+              (new_hash, datetime.now(), user['id']))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], 
+             additional_info="Password changed successfully")
+    flash('Password changed successfully.', 'success')
+    return redirect(url_for('profile'))
+
+@app.route("/profile/setup-2fa")
+@login_required
+def setup_2fa():
+    user = get_user_by_username(session['username'])
+    
+    if user['totp_enabled']:
+        flash('2FA is already enabled for your account.', 'info')
+        return redirect(url_for('profile'))
+    
+    # Generate new TOTP secret
+    totp_secret = generate_totp_secret()
+    session['temp_totp_secret'] = totp_secret
+    
+    # Generate QR code
+    qr_code = generate_qr_code(user['username'], totp_secret)
+    
+    return render_template('setup_2fa.html', 
+                         totp_secret=totp_secret, 
+                         qr_code=qr_code)
+
+@app.route("/profile/enable-2fa", methods=["POST"])
+@login_required
+def enable_2fa():
+    token = request.form.get('token', '').replace(' ', '')
+    totp_secret = session.get('temp_totp_secret')
+    
+    if not totp_secret:
+        flash('2FA setup session expired. Please try again.', 'error')
+        return redirect(url_for('setup_2fa'))
+    
+    # Verify the token
+    if not verify_totp(totp_secret, token):
+        flash('Invalid authentication code. Please try again.', 'error')
+        return redirect(url_for('setup_2fa'))
+    
+    # Generate backup codes
+    backup_codes = generate_backup_codes()
+    hashed_codes = [hash_backup_code(code) for code in backup_codes]
+    
+    # Enable 2FA
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("""
+        UPDATE users 
+        SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s 
+        WHERE username = %s
+    """, (totp_secret, json.dumps(hashed_codes), session['username']))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    session.pop('temp_totp_secret', None)
+    
+    log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully")
+    
+    # Show backup codes
+    return render_template('backup_codes.html', backup_codes=backup_codes)
+
+@app.route("/profile/disable-2fa", methods=["POST"])
+@login_required
+def disable_2fa():
+    password = request.form.get('password')
+    user = get_user_by_username(session['username'])
+    
+    # Verify password
+    if not verify_password(password, user['password_hash']):
+        flash('Incorrect password.', 'error')
+        return redirect(url_for('profile'))
+    
+    # Disable 2FA
+    conn = get_connection()
+    cur = conn.cursor()
+    cur.execute("""
+        UPDATE users 
+        SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL 
+        WHERE username = %s
+    """, (session['username'],))
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled")
+    flash('2FA has been disabled for your account.', 'success')
+    return redirect(url_for('profile'))
+
+@app.route("/heartbeat", methods=['POST'])
+@login_required
+def heartbeat():
+    """Endpoint für Session Keep-Alive - aktualisiert last_activity"""
+    # Aktualisiere last_activity nur wenn explizit angefordert
+    session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+    # Force session save
+    session.modified = True
+    
+    return jsonify({
+        'status': 'ok',
+        'last_activity': session['last_activity'],
+        'username': session.get('username')
+    })
+
+@app.route("/api/generate-license-key", methods=['POST'])
+@login_required
+def api_generate_key():
+    """API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
+    try:
+        # Lizenztyp aus Request holen (default: full)
+        data = request.get_json() or {}
+        license_type = data.get('type', 'full')
+        
+        # Key generieren
+        key = generate_license_key(license_type)
+        
+        # Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Wiederhole bis eindeutiger Key gefunden
+        attempts = 0
+        while attempts < 10:  # Max 10 Versuche
+            cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
+            if not cur.fetchone():
+                break  # Key ist eindeutig
+            key = generate_license_key(license_type)
+            attempts += 1
+        
+        cur.close()
+        conn.close()
+        
+        # Log für Audit
+        log_audit('GENERATE_KEY', 'license', 
+                 additional_info={'type': license_type, 'key': key})
+        
+        return jsonify({
+            'success': True,
+            'key': key,
+            'type': license_type
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Key-Generierung: {str(e)}")
+        return jsonify({
+            'success': False,
+            'error': 'Fehler bei der Key-Generierung'
+        }), 500
+
+@app.route("/api/customers", methods=['GET'])
+@login_required
+def api_customers():
+    """API Endpoint für die Kundensuche mit Select2"""
+    try:
+        # Suchparameter
+        search = request.args.get('q', '').strip()
+        page = request.args.get('page', 1, type=int)
+        per_page = 20
+        customer_id = request.args.get('id', type=int)
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Einzelnen Kunden per ID abrufen
+        if customer_id:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                WHERE c.id = %s
+                GROUP BY c.id, c.name, c.email
+            """, (customer_id,))
+            
+            customer = cur.fetchone()
+            results = []
+            if customer:
+                results.append({
+                    'id': customer[0],
+                    'text': f"{customer[1]} ({customer[2]})",
+                    'name': customer[1],
+                    'email': customer[2],
+                    'license_count': customer[3]
+                })
+            
+            cur.close()
+            conn.close()
+            
+            return jsonify({
+                'results': results,
+                'pagination': {'more': False}
+            })
+        
+        # SQL Query mit optionaler Suche
+        elif search:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                WHERE LOWER(c.name) LIKE LOWER(%s)
+                   OR LOWER(c.email) LIKE LOWER(%s)
+                GROUP BY c.id, c.name, c.email
+                ORDER BY c.name
+                LIMIT %s OFFSET %s
+            """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page))
+        else:
+            cur.execute("""
+                SELECT c.id, c.name, c.email,
+                       COUNT(l.id) as license_count
+                FROM customers c
+                LEFT JOIN licenses l ON c.id = l.customer_id
+                GROUP BY c.id, c.name, c.email
+                ORDER BY c.name
+                LIMIT %s OFFSET %s
+            """, (per_page, (page - 1) * per_page))
+        
+        customers = cur.fetchall()
+        
+        # Format für Select2
+        results = []
+        for customer in customers:
+            results.append({
+                'id': customer[0],
+                'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)",
+                'name': customer[1],
+                'email': customer[2],
+                'license_count': customer[3]
+            })
+        
+        # Gesamtanzahl für Pagination
+        if search:
+            cur.execute("""
+                SELECT COUNT(*) FROM customers
+                WHERE LOWER(name) LIKE LOWER(%s)
+                   OR LOWER(email) LIKE LOWER(%s)
+            """, (f'%{search}%', f'%{search}%'))
+        else:
+            cur.execute("SELECT COUNT(*) FROM customers")
+        
+        total_count = cur.fetchone()[0]
+        
+        cur.close()
+        conn.close()
+        
+        # Select2 Response Format
+        return jsonify({
+            'results': results,
+            'pagination': {
+                'more': (page * per_page) < total_count
+            }
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Kundensuche: {str(e)}")
+        return jsonify({
+            'results': [],
+            'error': 'Fehler bei der Kundensuche'
+        }), 500
+
+@app.route("/")
+@login_required
+def dashboard():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Statistiken abrufen
+    # Gesamtanzahl Kunden (ohne Testdaten)
+    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE")
+    total_customers = cur.fetchone()[0]
+    
+    # Gesamtanzahl Lizenzen (ohne Testdaten)
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE")
+    total_licenses = cur.fetchone()[0]
+    
+    # Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE
+    """)
+    active_licenses = cur.fetchone()[0]
+    
+    # Aktive Sessions
+    cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
+    active_sessions_count = cur.fetchone()[0]
+    
+    # Abgelaufene Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until < CURRENT_DATE AND is_test = FALSE
+    """)
+    expired_licenses = cur.fetchone()[0]
+    
+    # Deaktivierte Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE is_active = FALSE AND is_test = FALSE
+    """)
+    inactive_licenses = cur.fetchone()[0]
+    
+    # Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten)
+    cur.execute("""
+        SELECT COUNT(*) FROM licenses 
+        WHERE valid_until >= CURRENT_DATE 
+        AND valid_until < CURRENT_DATE + INTERVAL '30 days'
+        AND is_active = TRUE
+        AND is_test = FALSE
+    """)
+    expiring_soon = cur.fetchone()[0]
+    
+    # Testlizenzen vs Vollversionen (ohne Testdaten)
+    cur.execute("""
+        SELECT license_type, COUNT(*) 
+        FROM licenses 
+        WHERE is_test = FALSE
+        GROUP BY license_type
+    """)
+    license_types = dict(cur.fetchall())
+    
+    # Anzahl Testdaten
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE")
+    test_data_count = cur.fetchone()[0]
+    
+    # Anzahl Test-Kunden
+    cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE")
+    test_customers_count = cur.fetchone()[0]
+    
+    # Anzahl Test-Ressourcen
+    cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE")
+    test_resources_count = cur.fetchone()[0]
+    
+    # Letzte 5 erstellten Lizenzen (ohne Testdaten)
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, l.valid_until,
+               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 as status
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.is_test = FALSE
+        ORDER BY l.id DESC
+        LIMIT 5
+    """)
+    recent_licenses = cur.fetchall()
+    
+    # Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten)
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, l.valid_until,
+               l.valid_until - CURRENT_DATE as days_left
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.valid_until >= CURRENT_DATE 
+        AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
+        AND l.is_active = TRUE
+        AND l.is_test = FALSE
+        ORDER BY l.valid_until
+        LIMIT 10
+    """)
+    expiring_licenses = cur.fetchall()
+    
+    # Letztes Backup
+    cur.execute("""
+        SELECT created_at, filesize, duration_seconds, backup_type, status
+        FROM backup_history 
+        ORDER BY created_at DESC 
+        LIMIT 1
+    """)
+    last_backup_info = cur.fetchone()
+    
+    # Sicherheitsstatistiken
+    # Gesperrte IPs
+    cur.execute("""
+        SELECT COUNT(*) FROM login_attempts 
+        WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP
+    """)
+    blocked_ips_count = cur.fetchone()[0]
+    
+    # Fehlversuche heute
+    cur.execute("""
+        SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts
+        WHERE last_attempt::date = CURRENT_DATE
+    """)
+    failed_attempts_today = cur.fetchone()[0]
+    
+    # Letzte 5 Sicherheitsereignisse
+    cur.execute("""
+        SELECT 
+            la.ip_address, 
+            la.attempt_count, 
+            la.last_attempt, 
+            la.blocked_until,
+            la.last_username_tried,
+            la.last_error_message
+        FROM login_attempts la
+        ORDER BY la.last_attempt DESC
+        LIMIT 5
+    """)
+    recent_security_events = []
+    for event in cur.fetchall():
+        recent_security_events.append({
+            'ip_address': event[0],
+            'attempt_count': event[1],
+            'last_attempt': event[2].strftime('%d.%m %H:%M'),
+            'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) else None,
+            'username_tried': event[4],
+            'error_message': event[5]
+        })
+    
+    # Sicherheitslevel berechnen
+    if blocked_ips_count > 5 or failed_attempts_today > 50:
+        security_level = 'danger'
+        security_level_text = 'KRITISCH'
+    elif blocked_ips_count > 2 or failed_attempts_today > 20:
+        security_level = 'warning'
+        security_level_text = 'ERHÖHT'
+    else:
+        security_level = 'success'
+        security_level_text = 'NORMAL'
+    
+    # Resource Pool Statistiken (nur Live-Daten, keine Testdaten)
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'available') as available,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+            COUNT(*) as total
+        FROM resource_pools
+        WHERE is_test = FALSE
+        GROUP BY resource_type
+    """)
+    
+    resource_stats = {}
+    resource_warning = None
+    
+    for row in cur.fetchall():
+        available_percent = round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
+        resource_stats[row[0]] = {
+            'available': row[1],
+            'allocated': row[2],
+            'quarantine': row[3],
+            'total': row[4],
+            'available_percent': available_percent,
+            'warning_level': 'danger' if row[1] < 50 else 'warning' if row[1] < 100 else 'success'
+        }
+        
+        # Warnung bei niedrigem Bestand
+        if row[1] < 50:
+            if not resource_warning:
+                resource_warning = f"Niedriger Bestand bei {row[0].upper()}: nur noch {row[1]} verfügbar!"
+            else:
+                resource_warning += f" | {row[0].upper()}: {row[1]}"
+    
+    cur.close()
+    conn.close()
+    
+    stats = {
+        'total_customers': total_customers,
+        'total_licenses': total_licenses,
+        'active_licenses': active_licenses,
+        'expired_licenses': expired_licenses,
+        'inactive_licenses': inactive_licenses,
+        'expiring_soon': expiring_soon,
+        'full_licenses': license_types.get('full', 0),
+        'test_licenses': license_types.get('test', 0),
+        'test_data_count': test_data_count,
+        'test_customers_count': test_customers_count,
+        'test_resources_count': test_resources_count,
+        'recent_licenses': recent_licenses,
+        'expiring_licenses': expiring_licenses,
+        'active_sessions': active_sessions_count,
+        'last_backup': last_backup_info,
+        # Sicherheitsstatistiken
+        'blocked_ips_count': blocked_ips_count,
+        'failed_attempts_today': failed_attempts_today,
+        'recent_security_events': recent_security_events,
+        'security_level': security_level,
+        'security_level_text': security_level_text,
+        'resource_stats': resource_stats
+    }
+    
+    return render_template("dashboard.html", 
+                         stats=stats, 
+                         resource_stats=resource_stats,
+                         resource_warning=resource_warning,
+                         username=session.get('username'))
+
+@app.route("/create", methods=["GET", "POST"])
+@login_required
+def create_license():
+    if request.method == "POST":
+        customer_id = request.form.get("customer_id")
+        license_key = request.form["license_key"].upper()  # Immer Großbuchstaben
+        license_type = request.form["license_type"]
+        valid_from = request.form["valid_from"]
+        is_test = request.form.get("is_test") == "on"  # Checkbox value
+        
+        # Berechne valid_until basierend auf Laufzeit
+        duration = int(request.form.get("duration", 1))
+        duration_type = request.form.get("duration_type", "years")
+        
+        from datetime import datetime, timedelta
+        from dateutil.relativedelta import relativedelta
+        
+        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
+        
+        if duration_type == "days":
+            end_date = start_date + timedelta(days=duration)
+        elif duration_type == "months":
+            end_date = start_date + relativedelta(months=duration)
+        else:  # years
+            end_date = start_date + relativedelta(years=duration)
+        
+        # Ein Tag abziehen, da der Starttag mitgezählt wird
+        end_date = end_date - timedelta(days=1)
+        valid_until = end_date.strftime("%Y-%m-%d")
+
+        # Validiere License Key Format
+        if not validate_license_key(license_key):
+            flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error')
+            return redirect(url_for('create_license'))
+        
+        # Resource counts
+        domain_count = int(request.form.get("domain_count", 1))
+        ipv4_count = int(request.form.get("ipv4_count", 1))
+        phone_count = int(request.form.get("phone_count", 1))
+        device_limit = int(request.form.get("device_limit", 3))
+
+        conn = get_connection()
+        cur = conn.cursor()
+
+        try:
+            # Prüfe ob neuer Kunde oder bestehender
+            if customer_id == "new":
+                # Neuer Kunde
+                name = request.form.get("customer_name")
+                email = request.form.get("email")
+                
+                if not name:
+                    flash('Kundenname ist erforderlich!', 'error')
+                    return redirect(url_for('create_license'))
+                
+                # Prüfe ob E-Mail bereits existiert
+                if email:
+                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
+                    existing = cur.fetchone()
+                    if existing:
+                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
+                        return redirect(url_for('create_license'))
+                
+                # Kunde einfügen (erbt Test-Status von Lizenz)
+                cur.execute("""
+                    INSERT INTO customers (name, email, is_test, created_at)
+                    VALUES (%s, %s, %s, NOW())
+                    RETURNING id
+                """, (name, email, is_test))
+                customer_id = cur.fetchone()[0]
+                customer_info = {'name': name, 'email': email, 'is_test': is_test}
+                
+                # Audit-Log für neuen Kunden
+                log_audit('CREATE', 'customer', customer_id, 
+                         new_values={'name': name, 'email': email, 'is_test': is_test})
+            else:
+                # Bestehender Kunde - hole Infos für Audit-Log
+                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+                customer_data = cur.fetchone()
+                if not customer_data:
+                    flash('Kunde nicht gefunden!', 'error')
+                    return redirect(url_for('create_license'))
+                customer_info = {'name': customer_data[0], 'email': customer_data[1]}
+                
+                # Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
+                if customer_data[2]:  # is_test des Kunden
+                    is_test = True
+
+            # Lizenz hinzufügen
+            cur.execute("""
+                INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active, 
+                                    domain_count, ipv4_count, phone_count, device_limit, is_test)
+                VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
+                RETURNING id
+            """, (license_key, customer_id, license_type, valid_from, valid_until, 
+                  domain_count, ipv4_count, phone_count, device_limit, is_test))
+            license_id = cur.fetchone()[0]
+            
+            # Ressourcen zuweisen
+            try:
+                # Prüfe Verfügbarkeit
+                cur.execute("""
+                    SELECT 
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
+                        (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
+                """, (is_test, is_test, is_test))
+                available = cur.fetchone()
+                
+                if available[0] < domain_count:
+                    raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})")
+                if available[1] < ipv4_count:
+                    raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})")
+                if available[2] < phone_count:
+                    raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})")
+                
+                # Domains zuweisen
+                if domain_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, domain_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # IPv4s zuweisen
+                if ipv4_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, ipv4_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # Telefonnummern zuweisen
+                if phone_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, phone_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+            except ValueError as e:
+                conn.rollback()
+                flash(str(e), 'error')
+                return redirect(url_for('create_license'))
+
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE', 'license', license_id, 
+                     new_values={
+                         'license_key': license_key,
+                         'customer_name': customer_info['name'],
+                         'customer_email': customer_info['email'],
+                         'license_type': license_type,
+                         'valid_from': valid_from,
+                         'valid_until': valid_until,
+                         'device_limit': device_limit,
+                         'is_test': is_test
+                     })
+            
+            flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
+            
+        except Exception as e:
+            conn.rollback()
+            logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}")
+            flash('Fehler beim Erstellen der Lizenz!', 'error')
+        finally:
+            cur.close()
+            conn.close()
+
+        # Preserve show_test parameter if present
+        redirect_url = "/create"
+        if request.args.get('show_test') == 'true':
+            redirect_url += "?show_test=true"
+        return redirect(redirect_url)
+
+    # Unterstützung für vorausgewählten Kunden
+    preselected_customer_id = request.args.get('customer_id', type=int)
+    return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)
+
+@app.route("/batch", methods=["GET", "POST"])
+@login_required
+def batch_licenses():
+    """Batch-Generierung mehrerer Lizenzen für einen Kunden"""
+    if request.method == "POST":
+        # Formulardaten
+        customer_id = request.form.get("customer_id")
+        license_type = request.form["license_type"]
+        quantity = int(request.form["quantity"])
+        valid_from = request.form["valid_from"]
+        is_test = request.form.get("is_test") == "on"  # Checkbox value
+        
+        # Berechne valid_until basierend auf Laufzeit
+        duration = int(request.form.get("duration", 1))
+        duration_type = request.form.get("duration_type", "years")
+        
+        from datetime import datetime, timedelta
+        from dateutil.relativedelta import relativedelta
+        
+        start_date = datetime.strptime(valid_from, "%Y-%m-%d")
+        
+        if duration_type == "days":
+            end_date = start_date + timedelta(days=duration)
+        elif duration_type == "months":
+            end_date = start_date + relativedelta(months=duration)
+        else:  # years
+            end_date = start_date + relativedelta(years=duration)
+        
+        # Ein Tag abziehen, da der Starttag mitgezählt wird
+        end_date = end_date - timedelta(days=1)
+        valid_until = end_date.strftime("%Y-%m-%d")
+        
+        # Resource counts
+        domain_count = int(request.form.get("domain_count", 1))
+        ipv4_count = int(request.form.get("ipv4_count", 1))
+        phone_count = int(request.form.get("phone_count", 1))
+        device_limit = int(request.form.get("device_limit", 3))
+        
+        # Sicherheitslimit
+        if quantity < 1 or quantity > 100:
+            flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
+            return redirect(url_for('batch_licenses'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        try:
+            # Prüfe ob neuer Kunde oder bestehender
+            if customer_id == "new":
+                # Neuer Kunde
+                name = request.form.get("customer_name")
+                email = request.form.get("email")
+                
+                if not name:
+                    flash('Kundenname ist erforderlich!', 'error')
+                    return redirect(url_for('batch_licenses'))
+                
+                # Prüfe ob E-Mail bereits existiert
+                if email:
+                    cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,))
+                    existing = cur.fetchone()
+                    if existing:
+                        flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
+                        return redirect(url_for('batch_licenses'))
+                
+                # Kunde einfügen (erbt Test-Status von Lizenz)
+                cur.execute("""
+                    INSERT INTO customers (name, email, is_test, created_at)
+                    VALUES (%s, %s, %s, NOW())
+                    RETURNING id
+                """, (name, email, is_test))
+                customer_id = cur.fetchone()[0]
+                
+                # Audit-Log für neuen Kunden
+                log_audit('CREATE', 'customer', customer_id, 
+                         new_values={'name': name, 'email': email, 'is_test': is_test})
+            else:
+                # Bestehender Kunde - hole Infos
+                cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+                customer_data = cur.fetchone()
+                if not customer_data:
+                    flash('Kunde nicht gefunden!', 'error')
+                    return redirect(url_for('batch_licenses'))
+                name = customer_data[0]
+                email = customer_data[1]
+                
+                # Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren
+                if customer_data[2]:  # is_test des Kunden
+                    is_test = True
+            
+            # Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
+            total_domains_needed = domain_count * quantity
+            total_ipv4s_needed = ipv4_count * quantity
+            total_phones_needed = phone_count * quantity
+            
+            cur.execute("""
+                SELECT 
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
+                    (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
+            """, (is_test, is_test, is_test))
+            available = cur.fetchone()
+            
+            if available[0] < total_domains_needed:
+                flash(f"Nicht genügend Domains verfügbar (benötigt: {total_domains_needed}, verfügbar: {available[0]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            if available[1] < total_ipv4s_needed:
+                flash(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {total_ipv4s_needed}, verfügbar: {available[1]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            if available[2] < total_phones_needed:
+                flash(f"Nicht genügend Telefonnummern verfügbar (benötigt: {total_phones_needed}, verfügbar: {available[2]})", 'error')
+                return redirect(url_for('batch_licenses'))
+            
+            # Lizenzen generieren und speichern
+            generated_licenses = []
+            for i in range(quantity):
+                # Eindeutigen Key generieren
+                attempts = 0
+                while attempts < 10:
+                    license_key = generate_license_key(license_type)
+                    cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,))
+                    if not cur.fetchone():
+                        break
+                    attempts += 1
+                
+                # Lizenz einfügen
+                cur.execute("""
+                    INSERT INTO licenses (license_key, customer_id, license_type, is_test, 
+                                        valid_from, valid_until, is_active,
+                                        domain_count, ipv4_count, phone_count, device_limit)
+                    VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
+                    RETURNING id
+                """, (license_key, customer_id, license_type, is_test, valid_from, valid_until,
+                      domain_count, ipv4_count, phone_count, device_limit))
+                license_id = cur.fetchone()[0]
+                
+                # Ressourcen für diese Lizenz zuweisen
+                # Domains
+                if domain_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, domain_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # IPv4s
+                if ipv4_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, ipv4_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                # Telefonnummern
+                if phone_count > 0:
+                    cur.execute("""
+                        SELECT id FROM resource_pools
+                        WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
+                        LIMIT %s FOR UPDATE
+                    """, (is_test, phone_count))
+                    for (resource_id,) in cur.fetchall():
+                        cur.execute("""
+                            UPDATE resource_pools
+                            SET status = 'allocated', allocated_to_license = %s,
+                                status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s
+                            WHERE id = %s
+                        """, (license_id, session['username'], resource_id))
+                        
+                        cur.execute("""
+                            INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                            VALUES (%s, %s, %s)
+                        """, (license_id, resource_id, session['username']))
+                        
+                        cur.execute("""
+                            INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                            VALUES (%s, %s, 'allocated', %s, %s)
+                        """, (resource_id, license_id, session['username'], get_client_ip()))
+                
+                generated_licenses.append({
+                    'id': license_id,
+                    'key': license_key,
+                    'type': license_type
+                })
+            
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE_BATCH', 'license', 
+                     new_values={'customer': name, 'quantity': quantity, 'type': license_type},
+                     additional_info=f"Batch-Generierung von {quantity} Lizenzen")
+            
+            # Session für Export speichern
+            session['batch_export'] = {
+                'customer': name,
+                'email': email,
+                'licenses': generated_licenses,
+                'valid_from': valid_from,
+                'valid_until': valid_until,
+                'timestamp': datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
+            }
+            
+            flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success')
+            return render_template("batch_result.html", 
+                                 customer=name,
+                                 email=email,
+                                 licenses=generated_licenses,
+                                 valid_from=valid_from,
+                                 valid_until=valid_until)
+            
+        except Exception as e:
+            conn.rollback()
+            logging.error(f"Fehler bei Batch-Generierung: {str(e)}")
+            flash('Fehler bei der Batch-Generierung!', 'error')
+            return redirect(url_for('batch_licenses'))
+        finally:
+            cur.close()
+            conn.close()
+    
+    # GET Request
+    return render_template("batch_form.html")
+
+@app.route("/batch/export")
+@login_required
+def export_batch():
+    """Exportiert die zuletzt generierten Batch-Lizenzen"""
+    batch_data = session.get('batch_export')
+    if not batch_data:
+        flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error')
+        return redirect(url_for('batch_licenses'))
+    
+    # CSV generieren
+    output = io.StringIO()
+    output.write('\ufeff')  # UTF-8 BOM für Excel
+    
+    # Header
+    output.write(f"Kunde: {batch_data['customer']}\n")
+    output.write(f"E-Mail: {batch_data['email']}\n")
+    output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n")
+    output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n")
+    output.write("\n")
+    output.write("Nr;Lizenzschlüssel;Typ\n")
+    
+    # Lizenzen
+    for i, license in enumerate(batch_data['licenses'], 1):
+        typ_text = "Vollversion" if license['type'] == 'full' else "Testversion"
+        output.write(f"{i};{license['key']};{typ_text}\n")
+    
+    output.seek(0)
+    
+    # Audit-Log
+    log_audit('EXPORT', 'batch_licenses', 
+             additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen")
+    
+    return send_file(
+        io.BytesIO(output.getvalue().encode('utf-8-sig')),
+        mimetype='text/csv',
+        as_attachment=True,
+        download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv"
+    )
+
+@app.route("/licenses")
+@login_required
+def licenses():
+    # Redirect zur kombinierten Ansicht
+    return redirect("/customers-licenses")
+
+@app.route("/license/edit/<int:license_id>", methods=["GET", "POST"])
+@login_required
+def edit_license(license_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if request.method == "POST":
+        # Alte Werte für Audit-Log abrufen
+        cur.execute("""
+            SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit 
+            FROM licenses WHERE id = %s
+        """, (license_id,))
+        old_license = cur.fetchone()
+        
+        # Update license
+        license_key = request.form["license_key"]
+        license_type = request.form["license_type"]
+        valid_from = request.form["valid_from"]
+        valid_until = request.form["valid_until"]
+        is_active = request.form.get("is_active") == "on"
+        is_test = request.form.get("is_test") == "on"
+        device_limit = int(request.form.get("device_limit", 3))
+        
+        cur.execute("""
+            UPDATE licenses 
+            SET license_key = %s, license_type = %s, valid_from = %s, 
+                valid_until = %s, is_active = %s, is_test = %s, device_limit = %s
+            WHERE id = %s
+        """, (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('UPDATE', 'license', license_id,
+                 old_values={
+                     'license_key': old_license[0],
+                     'license_type': old_license[1],
+                     'valid_from': str(old_license[2]),
+                     'valid_until': str(old_license[3]),
+                     'is_active': old_license[4],
+                     'is_test': old_license[5],
+                     'device_limit': old_license[6]
+                 },
+                 new_values={
+                     'license_key': license_key,
+                     'license_type': license_type,
+                     'valid_from': valid_from,
+                     'valid_until': valid_until,
+                     'is_active': is_active,
+                     'is_test': is_test,
+                     'device_limit': device_limit
+                 })
+        
+        cur.close()
+        conn.close()
+        
+        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
+        redirect_url = "/customers-licenses"
+        
+        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
+        show_test = request.form.get('show_test') or request.args.get('show_test')
+        if show_test == 'true':
+            redirect_url += "?show_test=true"
+            
+        # Behalte customer_id bei wenn vorhanden
+        if request.referrer and 'customer_id=' in request.referrer:
+            import re
+            match = re.search(r'customer_id=(\d+)', request.referrer)
+            if match:
+                connector = "&" if "?" in redirect_url else "?"
+                redirect_url += f"{connector}customer_id={match.group(1)}"
+                
+        return redirect(redirect_url)
+    
+    # Get license data
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name, c.email, l.license_type, 
+               l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE l.id = %s
+    """, (license_id,))
+    
+    license = cur.fetchone()
+    cur.close()
+    conn.close()
+    
+    if not license:
+        return redirect("/licenses")
+    
+    return render_template("edit_license.html", license=license, username=session.get('username'))
+
+@app.route("/license/delete/<int:license_id>", methods=["POST"])
+@login_required
+def delete_license(license_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Lizenzdetails für Audit-Log abrufen
+    cur.execute("""
+        SELECT l.license_key, c.name, l.license_type 
+        FROM licenses l 
+        JOIN customers c ON l.customer_id = c.id 
+        WHERE l.id = %s
+    """, (license_id,))
+    license_info = cur.fetchone()
+    
+    cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,))
+    
+    conn.commit()
+    
+    # Audit-Log
+    if license_info:
+        log_audit('DELETE', 'license', license_id,
+                 old_values={
+                     'license_key': license_info[0],
+                     'customer_name': license_info[1],
+                     'license_type': license_info[2]
+                 })
+    
+    cur.close()
+    conn.close()
+    
+    return redirect("/licenses")
+
+@app.route("/customers")
+@login_required
+def customers():
+    # Redirect zur kombinierten Ansicht
+    return redirect("/customers-licenses")
+
+@app.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
+@login_required
+def edit_customer(customer_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if request.method == "POST":
+        # Alte Werte für Audit-Log abrufen
+        cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
+        old_customer = cur.fetchone()
+        
+        # Update customer
+        name = request.form["name"]
+        email = request.form["email"]
+        is_test = request.form.get("is_test") == "on"
+        
+        cur.execute("""
+            UPDATE customers 
+            SET name = %s, email = %s, is_test = %s
+            WHERE id = %s
+        """, (name, email, is_test, customer_id))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('UPDATE', 'customer', customer_id,
+                 old_values={
+                     'name': old_customer[0],
+                     'email': old_customer[1],
+                     'is_test': old_customer[2]
+                 },
+                 new_values={
+                     'name': name,
+                     'email': email,
+                     'is_test': is_test
+                 })
+        
+        cur.close()
+        conn.close()
+        
+        # Redirect zurück zu customers-licenses mit beibehaltenen Parametern
+        redirect_url = "/customers-licenses"
+        
+        # Behalte show_test Parameter bei (aus Form oder GET-Parameter)
+        show_test = request.form.get('show_test') or request.args.get('show_test')
+        if show_test == 'true':
+            redirect_url += "?show_test=true"
+            
+        # Behalte customer_id bei (immer der aktuelle Kunde)
+        connector = "&" if "?" in redirect_url else "?"
+        redirect_url += f"{connector}customer_id={customer_id}"
+                
+        return redirect(redirect_url)
+    
+    # Get customer data with licenses
+    cur.execute("""
+        SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s
+    """, (customer_id,))
+    
+    customer = cur.fetchone()
+    if not customer:
+        cur.close()
+        conn.close()
+        return "Kunde nicht gefunden", 404
+    
+    
+    # Get customer's licenses
+    cur.execute("""
+        SELECT id, license_key, license_type, valid_from, valid_until, is_active
+        FROM licenses
+        WHERE customer_id = %s
+        ORDER BY valid_until DESC
+    """, (customer_id,))
+    
+    licenses = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    if not customer:
+        return redirect("/customers-licenses")
+    
+    return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username'))
+
+@app.route("/customer/create", methods=["GET", "POST"])
+@login_required
+def create_customer():
+    """Erstellt einen neuen Kunden ohne Lizenz"""
+    if request.method == "POST":
+        name = request.form.get('name')
+        email = request.form.get('email')
+        is_test = request.form.get('is_test') == 'on'
+        
+        if not name or not email:
+            flash("Name und E-Mail sind Pflichtfelder!", "error")
+            return render_template("create_customer.html", username=session.get('username'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        try:
+            # Prüfen ob E-Mail bereits existiert
+            cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
+            existing = cur.fetchone()
+            if existing:
+                flash(f"Ein Kunde mit der E-Mail '{email}' existiert bereits: {existing[1]}", "error")
+                return render_template("create_customer.html", username=session.get('username'))
+            
+            # Kunde erstellen
+            cur.execute("""
+                INSERT INTO customers (name, email, created_at, is_test) 
+                VALUES (%s, %s, %s, %s) RETURNING id
+            """, (name, email, datetime.now(), is_test))
+            
+            customer_id = cur.fetchone()[0]
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('CREATE', 'customer', customer_id,
+                     new_values={
+                         'name': name,
+                         'email': email,
+                         'is_test': is_test
+                     })
+            
+            flash(f"Kunde '{name}' wurde erfolgreich angelegt!", "success")
+            return redirect(f"/customer/edit/{customer_id}")
+            
+        except Exception as e:
+            conn.rollback()
+            flash(f"Fehler beim Anlegen des Kunden: {str(e)}", "error")
+            return render_template("create_customer.html", username=session.get('username'))
+        finally:
+            cur.close()
+            conn.close()
+    
+    # GET Request - Formular anzeigen
+    return render_template("create_customer.html", username=session.get('username'))
+
+@app.route("/customer/delete/<int:customer_id>", methods=["POST"])
+@login_required
+def delete_customer(customer_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Prüfen ob Kunde Lizenzen hat
+    cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
+    license_count = cur.fetchone()[0]
+    
+    if license_count > 0:
+        # Kunde hat Lizenzen - nicht löschen
+        cur.close()
+        conn.close()
+        return redirect("/customers")
+    
+    # Kundendetails für Audit-Log abrufen
+    cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
+    customer_info = cur.fetchone()
+    
+    # Kunde löschen wenn keine Lizenzen vorhanden
+    cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
+    
+    conn.commit()
+    
+    # Audit-Log
+    if customer_info:
+        log_audit('DELETE', 'customer', customer_id,
+                 old_values={
+                     'name': customer_info[0],
+                     'email': customer_info[1]
+                 })
+    
+    cur.close()
+    conn.close()
+    
+    return redirect("/customers")
+
+@app.route("/customers-licenses")
+@login_required
+def customers_licenses():
+    """Kombinierte Ansicht für Kunden und deren Lizenzen"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    query = """
+        SELECT 
+            c.id,
+            c.name,
+            c.email,
+            c.created_at,
+            COUNT(l.id) as total_licenses,
+            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
+        FROM customers c
+        LEFT JOIN licenses l ON c.id = l.customer_id
+    """
+    
+    if not show_test:
+        query += " WHERE c.is_test = FALSE"
+    
+    query += """
+        GROUP BY c.id, c.name, c.email, c.created_at
+        ORDER BY c.name
+    """
+    
+    cur.execute(query)
+    customers = cur.fetchall()
+    
+    # Hole ausgewählten Kunden nur wenn explizit in URL angegeben
+    selected_customer_id = request.args.get('customer_id', type=int)
+    licenses = []
+    selected_customer = None
+    
+    if customers and selected_customer_id:
+        # Hole Daten des ausgewählten Kunden
+        for customer in customers:
+            if customer[0] == selected_customer_id:
+                selected_customer = customer
+                break
+        
+        # Hole Lizenzen des ausgewählten Kunden
+        if selected_customer:
+            cur.execute("""
+                SELECT 
+                    l.id,
+                    l.license_key,
+                    l.license_type,
+                    l.valid_from,
+                    l.valid_until,
+                    l.is_active,
+                    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 as status,
+                    l.domain_count,
+                    l.ipv4_count,
+                    l.phone_count,
+                    l.device_limit,
+                    (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
+                    -- Actual resource counts
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
+                    (SELECT COUNT(*) FROM license_resources lr 
+                     JOIN resource_pools rp ON lr.resource_id = rp.id 
+                     WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
+                FROM licenses l
+                WHERE l.customer_id = %s
+                ORDER BY l.created_at DESC, l.id DESC
+            """, (selected_customer_id,))
+            licenses = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("customers_licenses.html",
+                         customers=customers,
+                         selected_customer=selected_customer,
+                         selected_customer_id=selected_customer_id,
+                         licenses=licenses,
+                         show_test=show_test)
+
+@app.route("/api/customer/<int:customer_id>/licenses")
+@login_required
+def api_customer_licenses(customer_id):
+    """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole Lizenzen des Kunden
+    cur.execute("""
+        SELECT 
+            l.id,
+            l.license_key,
+            l.license_type,
+            l.valid_from,
+            l.valid_until,
+            l.is_active,
+            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 as status,
+            l.domain_count,
+            l.ipv4_count,
+            l.phone_count,
+            l.device_limit,
+            (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices,
+            -- Actual resource counts
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count,
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count,
+            (SELECT COUNT(*) FROM license_resources lr 
+             JOIN resource_pools rp ON lr.resource_id = rp.id 
+             WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count
+        FROM licenses l
+        WHERE l.customer_id = %s
+        ORDER BY l.created_at DESC, l.id DESC
+    """, (customer_id,))
+    
+    licenses = []
+    for row in cur.fetchall():
+        license_id = row[0]
+        
+        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
+        cur.execute("""
+            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
+            FROM resource_pools rp
+            JOIN license_resources lr ON rp.id = lr.resource_id
+            WHERE lr.license_id = %s AND lr.is_active = true
+            ORDER BY rp.resource_type, rp.resource_value
+        """, (license_id,))
+        
+        resources = {
+            'domains': [],
+            'ipv4s': [],
+            'phones': []
+        }
+        
+        for res_row in cur.fetchall():
+            resource_info = {
+                'id': res_row[0],
+                'value': res_row[2],
+                'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else ''
+            }
+            
+            if res_row[1] == 'domain':
+                resources['domains'].append(resource_info)
+            elif res_row[1] == 'ipv4':
+                resources['ipv4s'].append(resource_info)
+            elif res_row[1] == 'phone':
+                resources['phones'].append(resource_info)
+        
+        licenses.append({
+            'id': row[0],
+            'license_key': row[1],
+            'license_type': row[2],
+            'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '',
+            'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '',
+            'is_active': row[5],
+            'status': row[6],
+            'domain_count': row[7],  # limit
+            'ipv4_count': row[8],     # limit
+            'phone_count': row[9],    # limit
+            'device_limit': row[10],
+            'active_devices': row[11],
+            'actual_domain_count': row[12],  # actual count
+            'actual_ipv4_count': row[13],    # actual count
+            'actual_phone_count': row[14],   # actual count
+            'resources': resources
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'success': True,
+        'licenses': licenses,
+        'count': len(licenses)
+    })
+
+@app.route("/api/customer/<int:customer_id>/quick-stats")
+@login_required
+def api_customer_quick_stats(customer_id):
+    """API-Endpoint für Schnellstatistiken eines Kunden"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Hole Kundenstatistiken
+    cur.execute("""
+        SELECT 
+            COUNT(l.id) as total_licenses,
+            COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses,
+            COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon
+        FROM licenses l
+        WHERE l.customer_id = %s
+    """, (customer_id,))
+    
+    stats = cur.fetchone()
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'success': True,
+        'stats': {
+            'total': stats[0],
+            'active': stats[1],
+            'expired': stats[2],
+            'expiring_soon': stats[3]
+        }
+    })
+
+@app.route("/api/license/<int:license_id>/quick-edit", methods=['POST'])
+@login_required
+def api_license_quick_edit(license_id):
+    """API-Endpoint für schnelle Lizenz-Bearbeitung"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        data = request.get_json()
+        
+        # Hole alte Werte für Audit-Log
+        cur.execute("""
+            SELECT is_active, valid_until, license_type 
+            FROM licenses WHERE id = %s
+        """, (license_id,))
+        old_values = cur.fetchone()
+        
+        if not old_values:
+            return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404
+        
+        # Update-Felder vorbereiten
+        updates = []
+        params = []
+        new_values = {}
+        
+        if 'is_active' in data:
+            updates.append("is_active = %s")
+            params.append(data['is_active'])
+            new_values['is_active'] = data['is_active']
+        
+        if 'valid_until' in data:
+            updates.append("valid_until = %s")
+            params.append(data['valid_until'])
+            new_values['valid_until'] = data['valid_until']
+        
+        if 'license_type' in data:
+            updates.append("license_type = %s")
+            params.append(data['license_type'])
+            new_values['license_type'] = data['license_type']
+        
+        if updates:
+            params.append(license_id)
+            cur.execute(f"""
+                UPDATE licenses 
+                SET {', '.join(updates)}
+                WHERE id = %s
+            """, params)
+            
+            conn.commit()
+            
+            # Audit-Log
+            log_audit('UPDATE', 'license', license_id,
+                     old_values={
+                         'is_active': old_values[0],
+                         'valid_until': old_values[1].isoformat() if old_values[1] else None,
+                         'license_type': old_values[2]
+                     },
+                     new_values=new_values)
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True})
+    
+    except Exception as e:
+        conn.rollback()
+        cur.close()
+        conn.close()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route("/api/license/<int:license_id>/resources")
+@login_required
+def api_license_resources(license_id):
+    """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
+        cur.execute("""
+            SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
+            FROM resource_pools rp
+            JOIN license_resources lr ON rp.id = lr.resource_id
+            WHERE lr.license_id = %s AND lr.is_active = true
+            ORDER BY rp.resource_type, rp.resource_value
+        """, (license_id,))
+        
+        resources = {
+            'domains': [],
+            'ipv4s': [],
+            'phones': []
+        }
+        
+        for row in cur.fetchall():
+            resource_info = {
+                'id': row[0],
+                'value': row[2],
+                'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else ''
+            }
+            
+            if row[1] == 'domain':
+                resources['domains'].append(resource_info)
+            elif row[1] == 'ipv4':
+                resources['ipv4s'].append(resource_info)
+            elif row[1] == 'phone':
+                resources['phones'].append(resource_info)
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'resources': resources
+        })
+        
+    except Exception as e:
+        cur.close()
+        conn.close()
+        return jsonify({'success': False, 'error': str(e)}), 500
+
+@app.route("/sessions")
+@login_required
+def sessions():
+    conn = get_connection()
+    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
+    cur.execute(f"""
+        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
+               s.user_agent, s.started_at, s.last_heartbeat,
+               EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
+        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 {active_sort_fields[active_sort]} {active_order.upper()}
+    """)
+    active_sessions = cur.fetchall()
+    
+    # Inaktive Sessions der letzten 24 Stunden
+    cur.execute(f"""
+        SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address, 
+               s.started_at, s.ended_at, 
+               EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
+        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 > NOW() - INTERVAL '24 hours'
+        ORDER BY {ended_sort_fields[ended_sort]} {ended_order.upper()}
+        LIMIT 50
+    """)
+    recent_sessions = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("sessions.html", 
+                         active_sessions=active_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'))
+
+@app.route("/session/end/<int:session_id>", methods=["POST"])
+@login_required
+def end_session(session_id):
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Session beenden
+    cur.execute("""
+        UPDATE sessions 
+        SET is_active = FALSE, ended_at = NOW()
+        WHERE id = %s AND is_active = TRUE
+    """, (session_id,))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    return redirect("/sessions")
+
+@app.route("/export/licenses")
+@login_required
+def export_licenses():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
+    include_test = request.args.get('include_test', 'false').lower() == 'true'
+    customer_id = request.args.get('customer_id', type=int)
+    
+    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.is_test,
+               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 as status
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+    """
+    
+    # Build WHERE clause
+    where_conditions = []
+    params = []
+    
+    if not include_test:
+        where_conditions.append("l.is_test = FALSE")
+    
+    if customer_id:
+        where_conditions.append("l.customer_id = %s")
+        params.append(customer_id)
+    
+    if where_conditions:
+        query += " WHERE " + " AND ".join(where_conditions)
+    
+    query += " ORDER BY l.id"
+    
+    cur.execute(query, params)
+    
+    # Spaltennamen
+    columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 
+               'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status']
+    
+    # Daten in DataFrame
+    data = cur.fetchall()
+    df = pd.DataFrame(data, columns=columns)
+    
+    # Datumsformatierung
+    df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y')
+    df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y')
+    
+    # Typ und Aktiv Status anpassen
+    df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
+    df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
+    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
+    
+    cur.close()
+    conn.close()
+    
+    # Export Format
+    export_format = request.args.get('format', 'excel')
+    
+    # Audit-Log
+    log_audit('EXPORT', 'license', 
+             additional_info=f"Export aller Lizenzen als {export_format.upper()}")
+    filename = f'lizenzen_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
+    
+    if export_format == 'csv':
+        # CSV Export
+        output = io.StringIO()
+        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
+        output.seek(0)
+        
+        return send_file(
+            io.BytesIO(output.getvalue().encode('utf-8-sig')),
+            mimetype='text/csv',
+            as_attachment=True,
+            download_name=f'{filename}.csv'
+        )
+    else:
+        # Excel Export
+        output = io.BytesIO()
+        with pd.ExcelWriter(output, engine='openpyxl') as writer:
+            df.to_excel(writer, sheet_name='Lizenzen', index=False)
+            
+            # Formatierung
+            worksheet = writer.sheets['Lizenzen']
+            for column in worksheet.columns:
+                max_length = 0
+                column_letter = column[0].column_letter
+                for cell in column:
+                    try:
+                        if len(str(cell.value)) > max_length:
+                            max_length = len(str(cell.value))
+                    except:
+                        pass
+                adjusted_width = min(max_length + 2, 50)
+                worksheet.column_dimensions[column_letter].width = adjusted_width
+        
+        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/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():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Check if test data should be included
+    include_test = request.args.get('include_test', 'false').lower() == 'true'
+    
+    # Build query based on test data filter
+    if include_test:
+        # Include all customers
+        query = """
+            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
+                   COUNT(l.id) as total_licenses,
+                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
+                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
+            FROM customers c
+            LEFT JOIN licenses l ON c.id = l.customer_id
+            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
+            ORDER BY c.id
+        """
+    else:
+        # Exclude test customers and test licenses
+        query = """
+            SELECT c.id, c.name, c.email, c.created_at, c.is_test,
+                   COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
+                   COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
+                   COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
+            FROM customers c
+            LEFT JOIN licenses l ON c.id = l.customer_id
+            WHERE c.is_test = FALSE
+            GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
+            ORDER BY c.id
+        """
+    
+    cur.execute(query)
+    
+    # Spaltennamen
+    columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten',
+               'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
+    
+    # Daten in DataFrame
+    data = cur.fetchall()
+    df = pd.DataFrame(data, columns=columns)
+    
+    # Datumsformatierung
+    df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
+    
+    # Testdaten formatting
+    df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
+    
+    cur.close()
+    conn.close()
+    
+    # Export Format
+    export_format = request.args.get('format', 'excel')
+    
+    # Audit-Log
+    log_audit('EXPORT', 'customer', 
+             additional_info=f"Export aller Kunden als {export_format.upper()}")
+    filename = f'kunden_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}'
+    
+    if export_format == 'csv':
+        # CSV Export
+        output = io.StringIO()
+        df.to_csv(output, index=False, encoding='utf-8-sig', sep=';')
+        output.seek(0)
+        
+        return send_file(
+            io.BytesIO(output.getvalue().encode('utf-8-sig')),
+            mimetype='text/csv',
+            as_attachment=True,
+            download_name=f'{filename}.csv'
+        )
+    else:
+        # Excel Export
+        output = io.BytesIO()
+        with pd.ExcelWriter(output, engine='openpyxl') as writer:
+            df.to_excel(writer, sheet_name='Kunden', index=False)
+            
+            # Formatierung
+            worksheet = writer.sheets['Kunden']
+            for column in worksheet.columns:
+                max_length = 0
+                column_letter = column[0].column_letter
+                for cell in column:
+                    try:
+                        if len(str(cell.value)) > max_length:
+                            max_length = len(str(cell.value))
+                    except:
+                        pass
+                adjusted_width = min(max_length + 2, 50)
+                worksheet.column_dimensions[column_letter].width = adjusted_width
+        
+        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/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.resource_type, r.resource_value, r.status, r.allocated_to_license, 
+               r.created_at, r.status_changed_at,
+               l.license_key, c.name as customer_name, c.email as customer_email,
+               l.license_type
+        FROM resource_pools r
+        LEFT JOIN licenses l ON r.allocated_to_license = 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 (r.is_test = false OR r.is_test IS NULL)"
+    
+    # Filter für Ressourcentyp
+    if filter_type:
+        query += " AND r.resource_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.resource_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():
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Parameter
+    filter_user = request.args.get('user', '').strip()
+    filter_action = request.args.get('action', '').strip()
+    filter_entity = request.args.get('entity', '').strip()
+    page = request.args.get('page', 1, type=int)
+    sort = request.args.get('sort', 'timestamp')
+    order = request.args.get('order', 'desc')
+    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
+    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 = []
+    
+    # Filter
+    if filter_user:
+        query += " AND LOWER(username) LIKE LOWER(%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)
+    
+    # Gesamtanzahl für Pagination
+    count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table"
+    cur.execute(count_query, params)
+    total = cur.fetchone()[0]
+    
+    # Pagination
+    offset = (page - 1) * per_page
+    query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s"
+    params.extend([per_page, offset])
+    
+    cur.execute(query, params)
+    logs = cur.fetchall()
+    
+    # JSON-Werte parsen
+    parsed_logs = []
+    for log in logs:
+        parsed_log = list(log)
+        # old_values und new_values sind bereits Dictionaries (JSONB)
+        # Keine Konvertierung nötig
+        parsed_logs.append(parsed_log)
+    
+    # Pagination Info
+    total_pages = (total + per_page - 1) // per_page
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("audit_log.html", 
+                         logs=parsed_logs,
+                         filter_user=filter_user,
+                         filter_action=filter_action,
+                         filter_entity=filter_entity,
+                         page=page,
+                         total_pages=total_pages,
+                         total=total,
+                         sort=sort,
+                         order=order,
+                         username=session.get('username'))
+
+@app.route("/backups")
+@login_required
+def backups():
+    """Zeigt die Backup-Historie an"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Letztes erfolgreiches Backup für Dashboard
+    cur.execute("""
+        SELECT created_at, filesize, duration_seconds 
+        FROM backup_history 
+        WHERE status = 'success' 
+        ORDER BY created_at DESC 
+        LIMIT 1
+    """)
+    last_backup = cur.fetchone()
+    
+    # Alle Backups abrufen
+    cur.execute("""
+        SELECT id, filename, filesize, backup_type, status, error_message,
+               created_at, created_by, tables_count, records_count, 
+               duration_seconds, is_encrypted
+        FROM backup_history
+        ORDER BY created_at DESC
+    """)
+    backups = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("backups.html", 
+                         backups=backups,
+                         last_backup=last_backup,
+                         username=session.get('username'))
+
+@app.route("/backup/create", methods=["POST"])
+@login_required
+def create_backup_route():
+    """Erstellt ein manuelles Backup"""
+    username = session.get('username')
+    success, result = create_backup(backup_type="manual", created_by=username)
+    
+    if success:
+        return jsonify({
+            'success': True,
+            'message': f'Backup erfolgreich erstellt: {result}'
+        })
+    else:
+        return jsonify({
+            'success': False,
+            'message': f'Backup fehlgeschlagen: {result}'
+        }), 500
+
+@app.route("/backup/restore/<int:backup_id>", methods=["POST"])
+@login_required
+def restore_backup_route(backup_id):
+    """Stellt ein Backup wieder her"""
+    encryption_key = request.form.get('encryption_key')
+    
+    success, message = restore_backup(backup_id, encryption_key)
+    
+    if success:
+        return jsonify({
+            'success': True,
+            'message': message
+        })
+    else:
+        return jsonify({
+            'success': False,
+            'message': message
+        }), 500
+
+@app.route("/backup/download/<int:backup_id>")
+@login_required
+def download_backup(backup_id):
+    """Lädt eine Backup-Datei herunter"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT filename, filepath 
+        FROM backup_history 
+        WHERE id = %s
+    """, (backup_id,))
+    backup_info = cur.fetchone()
+    
+    cur.close()
+    conn.close()
+    
+    if not backup_info:
+        return "Backup nicht gefunden", 404
+    
+    filename, filepath = backup_info
+    filepath = Path(filepath)
+    
+    if not filepath.exists():
+        return "Backup-Datei nicht gefunden", 404
+    
+    # Audit-Log
+    log_audit('DOWNLOAD', 'backup', backup_id,
+             additional_info=f"Backup heruntergeladen: {filename}")
+    
+    return send_file(filepath, as_attachment=True, download_name=filename)
+
+@app.route("/backup/delete/<int:backup_id>", methods=["DELETE"])
+@login_required
+def delete_backup(backup_id):
+    """Löscht ein Backup"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        # Backup-Informationen abrufen
+        cur.execute("""
+            SELECT filename, filepath 
+            FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        backup_info = cur.fetchone()
+        
+        if not backup_info:
+            return jsonify({
+                'success': False,
+                'message': 'Backup nicht gefunden'
+            }), 404
+        
+        filename, filepath = backup_info
+        filepath = Path(filepath)
+        
+        # Datei löschen, wenn sie existiert
+        if filepath.exists():
+            filepath.unlink()
+        
+        # Aus Datenbank löschen
+        cur.execute("""
+            DELETE FROM backup_history 
+            WHERE id = %s
+        """, (backup_id,))
+        
+        conn.commit()
+        
+        # Audit-Log
+        log_audit('DELETE', 'backup', backup_id,
+                 additional_info=f"Backup gelöscht: {filename}")
+        
+        return jsonify({
+            'success': True,
+            'message': f'Backup "{filename}" wurde erfolgreich gelöscht'
+        })
+        
+    except Exception as e:
+        conn.rollback()
+        return jsonify({
+            'success': False,
+            'message': f'Fehler beim Löschen des Backups: {str(e)}'
+        }), 500
+    finally:
+        cur.close()
+        conn.close()
+
+@app.route("/security/blocked-ips")
+@login_required
+def blocked_ips():
+    """Zeigt alle gesperrten IPs an"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    cur.execute("""
+        SELECT 
+            ip_address,
+            attempt_count,
+            first_attempt,
+            last_attempt,
+            blocked_until,
+            last_username_tried,
+            last_error_message
+        FROM login_attempts
+        WHERE blocked_until IS NOT NULL
+        ORDER BY blocked_until DESC
+    """)
+    
+    blocked_ips_list = []
+    for ip in cur.fetchall():
+        blocked_ips_list.append({
+            'ip_address': ip[0],
+            'attempt_count': ip[1],
+            'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'),
+            'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'),
+            'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'),
+            'is_active': ip[4] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None),
+            'last_username': ip[5],
+            'last_error': ip[6]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return render_template("blocked_ips.html", 
+                         blocked_ips=blocked_ips_list,
+                         username=session.get('username'))
+
+@app.route("/security/unblock-ip", methods=["POST"])
+@login_required
+def unblock_ip():
+    """Entsperrt eine IP-Adresse"""
+    ip_address = request.form.get('ip_address')
+    
+    if ip_address:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        cur.execute("""
+            UPDATE login_attempts
+            SET blocked_until = NULL
+            WHERE ip_address = %s
+        """, (ip_address,))
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        # Audit-Log
+        log_audit('UNBLOCK_IP', 'security', 
+                 additional_info=f"IP {ip_address} manuell entsperrt")
+    
+    return redirect(url_for('blocked_ips'))
+
+@app.route("/security/clear-attempts", methods=["POST"])
+@login_required
+def clear_attempts():
+    """Löscht alle Login-Versuche für eine IP"""
+    ip_address = request.form.get('ip_address')
+    
+    if ip_address:
+        reset_login_attempts(ip_address)
+        
+        # Audit-Log
+        log_audit('CLEAR_ATTEMPTS', 'security', 
+                 additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt")
+    
+    return redirect(url_for('blocked_ips'))
+
+# API Endpoints for License Management
+@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
+@login_required
+def toggle_license_api(license_id):
+    """Toggle license active status via API"""
+    try:
+        data = request.get_json()
+        is_active = data.get('is_active', False)
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update license status
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = %s
+            WHERE id = %s
+        """, (is_active, license_id))
+        
+        conn.commit()
+        
+        # Log the action
+        log_audit('UPDATE', 'license', license_id,
+                 new_values={'is_active': is_active},
+                 additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Status erfolgreich geändert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/licenses/bulk-activate", methods=["POST"])
+@login_required
+def bulk_activate_licenses():
+    """Activate multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update all selected licenses (nur Live-Daten)
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = TRUE
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_UPDATE', 'licenses', None,
+                 new_values={'is_active': True, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen aktiviert")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
+@login_required
+def bulk_deactivate_licenses():
+    """Deactivate multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Update all selected licenses (nur Live-Daten)
+        cur.execute("""
+            UPDATE licenses 
+            SET is_active = FALSE
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_UPDATE', 'licenses', None,
+                 new_values={'is_active': False, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen deaktiviert")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+@app.route("/api/license/<int:license_id>/devices")
+@login_required
+def get_license_devices(license_id):
+    """Hole alle registrierten Geräte einer Lizenz"""
+    try:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob Lizenz existiert und hole device_limit
+        cur.execute("""
+            SELECT device_limit FROM licenses WHERE id = %s
+        """, (license_id,))
+        license_data = cur.fetchone()
+        
+        if not license_data:
+            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
+        
+        device_limit = license_data[0]
+        
+        # Hole alle Geräte für diese Lizenz
+        cur.execute("""
+            SELECT id, hardware_id, device_name, operating_system,
+                   first_seen, last_seen, is_active, ip_address
+            FROM device_registrations
+            WHERE license_id = %s
+            ORDER BY is_active DESC, last_seen DESC
+        """, (license_id,))
+        
+        devices = []
+        for row in cur.fetchall():
+            devices.append({
+                'id': row[0],
+                'hardware_id': row[1],
+                'device_name': row[2] or 'Unbekanntes Gerät',
+                'operating_system': row[3] or 'Unbekannt',
+                'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '',
+                'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '',
+                'is_active': row[6],
+                'ip_address': row[7] or '-'
+            })
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'devices': devices,
+            'device_limit': device_limit,
+            'active_count': sum(1 for d in devices if d['is_active'])
+        })
+        
+    except Exception as e:
+        logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
+
+@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
+def register_device(license_id):
+    """Registriere ein neues Gerät für eine Lizenz"""
+    try:
+        data = request.get_json()
+        hardware_id = data.get('hardware_id')
+        device_name = data.get('device_name', '')
+        operating_system = data.get('operating_system', '')
+        
+        if not hardware_id:
+            return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob Lizenz existiert und aktiv ist
+        cur.execute("""
+            SELECT device_limit, is_active, valid_until
+            FROM licenses 
+            WHERE id = %s
+        """, (license_id,))
+        license_data = cur.fetchone()
+        
+        if not license_data:
+            return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
+        
+        device_limit, is_active, valid_until = license_data
+        
+        # Prüfe ob Lizenz aktiv und gültig ist
+        if not is_active:
+            return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
+        
+        if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
+            return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
+        
+        # Prüfe ob Gerät bereits registriert ist
+        cur.execute("""
+            SELECT id, is_active FROM device_registrations
+            WHERE license_id = %s AND hardware_id = %s
+        """, (license_id, hardware_id))
+        existing_device = cur.fetchone()
+        
+        if existing_device:
+            device_id, is_device_active = existing_device
+            if is_device_active:
+                # Gerät ist bereits aktiv, update last_seen
+                cur.execute("""
+                    UPDATE device_registrations
+                    SET last_seen = CURRENT_TIMESTAMP,
+                        ip_address = %s,
+                        user_agent = %s
+                    WHERE id = %s
+                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                conn.commit()
+                return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
+            else:
+                # Gerät war deaktiviert, prüfe ob wir es reaktivieren können
+                cur.execute("""
+                    SELECT COUNT(*) FROM device_registrations
+                    WHERE license_id = %s AND is_active = TRUE
+                """, (license_id,))
+                active_count = cur.fetchone()[0]
+                
+                if active_count >= device_limit:
+                    return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+                
+                # Reaktiviere das Gerät
+                cur.execute("""
+                    UPDATE device_registrations
+                    SET is_active = TRUE,
+                        last_seen = CURRENT_TIMESTAMP,
+                        deactivated_at = NULL,
+                        deactivated_by = NULL,
+                        ip_address = %s,
+                        user_agent = %s
+                    WHERE id = %s
+                """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
+                conn.commit()
+                return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
+        
+        # Neues Gerät - prüfe Gerätelimit
+        cur.execute("""
+            SELECT COUNT(*) FROM device_registrations
+            WHERE license_id = %s AND is_active = TRUE
+        """, (license_id,))
+        active_count = cur.fetchone()[0]
+        
+        if active_count >= device_limit:
+            return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
+        
+        # Registriere neues Gerät
+        cur.execute("""
+            INSERT INTO device_registrations 
+            (license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
+            VALUES (%s, %s, %s, %s, %s, %s)
+            RETURNING id
+        """, (license_id, hardware_id, device_name, operating_system, 
+              get_client_ip(), request.headers.get('User-Agent', '')))
+        device_id = cur.fetchone()[0]
+        
+        conn.commit()
+        
+        # Audit Log
+        log_audit('DEVICE_REGISTER', 'device', device_id,
+                 new_values={'license_id': license_id, 'hardware_id': hardware_id})
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
+        
+    except Exception as e:
+        logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
+
+@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
+@login_required
+def deactivate_device(license_id, device_id):
+    """Deaktiviere ein registriertes Gerät"""
+    try:
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Prüfe ob das Gerät zu dieser Lizenz gehört
+        cur.execute("""
+            SELECT id FROM device_registrations
+            WHERE id = %s AND license_id = %s AND is_active = TRUE
+        """, (device_id, license_id))
+        
+        if not cur.fetchone():
+            return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404
+        
+        # Deaktiviere das Gerät
+        cur.execute("""
+            UPDATE device_registrations
+            SET is_active = FALSE,
+                deactivated_at = CURRENT_TIMESTAMP,
+                deactivated_by = %s
+            WHERE id = %s
+        """, (session['username'], device_id))
+        
+        conn.commit()
+        
+        # Audit Log
+        log_audit('DEVICE_DEACTIVATE', 'device', device_id,
+                 old_values={'is_active': True},
+                 new_values={'is_active': False})
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'})
+        
+    except Exception as e:
+        logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
+        return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
+
+@app.route("/api/licenses/bulk-delete", methods=["POST"])
+@login_required
+def bulk_delete_licenses():
+    """Delete multiple licenses at once"""
+    try:
+        data = request.get_json()
+        license_ids = data.get('ids', [])
+        
+        if not license_ids:
+            return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        # Get license info for audit log (nur Live-Daten)
+        cur.execute("""
+            SELECT license_key 
+            FROM licenses 
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        license_keys = [row[0] for row in cur.fetchall()]
+        
+        # Delete all selected licenses (nur Live-Daten)
+        cur.execute("""
+            DELETE FROM licenses 
+            WHERE id = ANY(%s) AND is_test = FALSE
+        """, (license_ids,))
+        
+        affected_rows = cur.rowcount
+        conn.commit()
+        
+        # Log the bulk action
+        log_audit('BULK_DELETE', 'licenses', None,
+                 old_values={'license_keys': license_keys, 'count': affected_rows},
+                 additional_info=f"{affected_rows} Lizenzen gelöscht")
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'})
+    except Exception as e:
+        return jsonify({'success': False, 'message': str(e)}), 500
+
+# ===================== RESOURCE POOL MANAGEMENT =====================
+
+@app.route('/resources')
+@login_required
+def resources():
+    """Resource Pool Hauptübersicht"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Prüfe ob Testdaten angezeigt werden sollen (gleiche Logik wie bei Kunden)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    # Statistiken abrufen
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'available') as available,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+            COUNT(*) as total
+        FROM resource_pools
+        WHERE is_test = %s
+        GROUP BY resource_type
+    """, (show_test,))
+    
+    stats = {}
+    for row in cur.fetchall():
+        stats[row[0]] = {
+            'available': row[1],
+            'allocated': row[2],
+            'quarantine': row[3],
+            'total': row[4],
+            'available_percent': round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1)
+        }
+    
+    # Letzte Aktivitäten (gefiltert nach Test/Live)
+    cur.execute("""
+        SELECT 
+            rh.action,
+            rh.action_by,
+            rh.action_at,
+            rp.resource_type,
+            rp.resource_value,
+            rh.details
+        FROM resource_history rh
+        JOIN resource_pools rp ON rh.resource_id = rp.id
+        WHERE rp.is_test = %s
+        ORDER BY rh.action_at DESC
+        LIMIT 10
+    """, (show_test,))
+    recent_activities = cur.fetchall()
+    
+    # Ressourcen-Liste mit Pagination
+    page = request.args.get('page', 1, type=int)
+    per_page = 50
+    offset = (page - 1) * per_page
+    
+    resource_type = request.args.get('type', '')
+    status_filter = request.args.get('status', '')
+    search = request.args.get('search', '')
+    
+    # Sortierung
+    sort_by = request.args.get('sort', 'id')
+    sort_order = request.args.get('order', 'desc')
+    
+    # Base Query
+    query = """
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            rp.status,
+            rp.allocated_to_license,
+            l.license_key,
+            c.name as customer_name,
+            rp.status_changed_at,
+            rp.quarantine_reason,
+            rp.quarantine_until,
+            c.id as customer_id
+        FROM resource_pools rp
+        LEFT JOIN licenses l ON rp.allocated_to_license = l.id
+        LEFT JOIN customers c ON l.customer_id = c.id
+        WHERE rp.is_test = %s
+    """
+    params = [show_test]
+    
+    if resource_type:
+        query += " AND rp.resource_type = %s"
+        params.append(resource_type)
+    
+    if status_filter:
+        query += " AND rp.status = %s"
+        params.append(status_filter)
+    
+    if search:
+        query += " AND rp.resource_value ILIKE %s"
+        params.append(f'%{search}%')
+    
+    # Count total
+    count_query = f"SELECT COUNT(*) FROM ({query}) as cnt"
+    cur.execute(count_query, params)
+    total = cur.fetchone()[0]
+    total_pages = (total + per_page - 1) // per_page
+    
+    # Get paginated results with dynamic sorting
+    sort_column_map = {
+        'id': 'rp.id',
+        'type': 'rp.resource_type',
+        'resource': 'rp.resource_value',
+        'status': 'rp.status',
+        'assigned': 'c.name',
+        'changed': 'rp.status_changed_at'
+    }
+    
+    sort_column = sort_column_map.get(sort_by, 'rp.id')
+    sort_direction = 'ASC' if sort_order == 'asc' else 'DESC'
+    
+    query += f" ORDER BY {sort_column} {sort_direction} LIMIT %s OFFSET %s"
+    params.extend([per_page, offset])
+    
+    cur.execute(query, params)
+    resources = cur.fetchall()
+    
+    cur.close()
+    conn.close()
+    
+    return render_template('resources.html',
+                         stats=stats,
+                         resources=resources,
+                         recent_activities=recent_activities,
+                         page=page,
+                         total_pages=total_pages,
+                         total=total,
+                         resource_type=resource_type,
+                         status_filter=status_filter,
+                         search=search,
+                         show_test=show_test,
+                         sort_by=sort_by,
+                         sort_order=sort_order,
+                         datetime=datetime,
+                         timedelta=timedelta)
+
+@app.route('/resources/add', methods=['GET', 'POST'])
+@login_required
+def add_resources():
+    """Ressourcen zum Pool hinzufügen"""
+    # Hole show_test Parameter für die Anzeige
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    if request.method == 'POST':
+        resource_type = request.form.get('resource_type')
+        resources_text = request.form.get('resources_text', '')
+        is_test = request.form.get('is_test') == 'on'  # Checkbox für Testdaten
+        
+        # Parse resources (one per line)
+        resources = [r.strip() for r in resources_text.split('\n') if r.strip()]
+        
+        if not resources:
+            flash('Keine Ressourcen angegeben', 'error')
+            return redirect(url_for('add_resources', show_test=show_test))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        added = 0
+        duplicates = 0
+        
+        for resource_value in resources:
+            try:
+                cur.execute("""
+                    INSERT INTO resource_pools (resource_type, resource_value, status_changed_by, is_test)
+                    VALUES (%s, %s, %s, %s)
+                    ON CONFLICT (resource_type, resource_value) DO NOTHING
+                """, (resource_type, resource_value, session['username'], is_test))
+                
+                if cur.rowcount > 0:
+                    added += 1
+                    # Get the inserted ID
+                    cur.execute("SELECT id FROM resource_pools WHERE resource_type = %s AND resource_value = %s", 
+                              (resource_type, resource_value))
+                    resource_id = cur.fetchone()[0]
+                    
+                    # Log in history
+                    cur.execute("""
+                        INSERT INTO resource_history (resource_id, action, action_by, ip_address)
+                        VALUES (%s, 'created', %s, %s)
+                    """, (resource_id, session['username'], get_client_ip()))
+                else:
+                    duplicates += 1
+                    
+            except Exception as e:
+                app.logger.error(f"Error adding resource {resource_value}: {e}")
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        log_audit('CREATE', 'resource_pool', None,
+                 new_values={'type': resource_type, 'added': added, 'duplicates': duplicates, 'is_test': is_test},
+                 additional_info=f"{added} {'Test-' if is_test else ''}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen")
+        
+        flash(f'{added} {"Test-" if is_test else ""}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen', 'success')
+        return redirect(url_for('resources', show_test=show_test))
+    
+    return render_template('add_resources.html', show_test=show_test)
+
+@app.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
+@login_required
+def quarantine_resource(resource_id):
+    """Ressource in Quarantäne setzen"""
+    reason = request.form.get('reason', 'review')
+    until_date = request.form.get('until_date')
+    notes = request.form.get('notes', '')
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Get current resource info
+    cur.execute("SELECT resource_type, resource_value, status FROM resource_pools WHERE id = %s", (resource_id,))
+    resource = cur.fetchone()
+    
+    if not resource:
+        flash('Ressource nicht gefunden', 'error')
+        return redirect(url_for('resources'))
+    
+    old_status = resource[2]
+    
+    # Update resource
+    cur.execute("""
+        UPDATE resource_pools
+        SET status = 'quarantine',
+            quarantine_reason = %s,
+            quarantine_until = %s,
+            notes = %s,
+            status_changed_at = CURRENT_TIMESTAMP,
+            status_changed_by = %s
+        WHERE id = %s
+    """, (reason, until_date if until_date else None, notes, session['username'], resource_id))
+    
+    # Log in history
+    cur.execute("""
+        INSERT INTO resource_history (resource_id, action, action_by, ip_address, details)
+        VALUES (%s, 'quarantined', %s, %s, %s)
+    """, (resource_id, session['username'], get_client_ip(), 
+          Json({'reason': reason, 'until': until_date, 'notes': notes, 'old_status': old_status})))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('UPDATE', 'resource', resource_id,
+             old_values={'status': old_status},
+             new_values={'status': 'quarantine', 'reason': reason},
+             additional_info=f"Ressource {resource[0]}: {resource[1]} in Quarantäne")
+    
+    flash('Ressource in Quarantäne gesetzt', 'success')
+    
+    # Redirect mit allen aktuellen Filtern
+    return redirect(url_for('resources', 
+                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
+                          type=request.args.get('type', request.form.get('type', '')),
+                          status=request.args.get('status', request.form.get('status', '')),
+                          search=request.args.get('search', request.form.get('search', ''))))
+
+@app.route('/resources/release', methods=['POST'])
+@login_required
+def release_resources():
+    """Ressourcen aus Quarantäne freigeben"""
+    resource_ids = request.form.getlist('resource_ids')
+    
+    if not resource_ids:
+        flash('Keine Ressourcen ausgewählt', 'error')
+        return redirect(url_for('resources'))
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    released = 0
+    for resource_id in resource_ids:
+        cur.execute("""
+            UPDATE resource_pools
+            SET status = 'available',
+                quarantine_reason = NULL,
+                quarantine_until = NULL,
+                allocated_to_license = NULL,
+                status_changed_at = CURRENT_TIMESTAMP,
+                status_changed_by = %s
+            WHERE id = %s AND status = 'quarantine'
+        """, (session['username'], resource_id))
+        
+        if cur.rowcount > 0:
+            released += 1
+            # Log in history
+            cur.execute("""
+                INSERT INTO resource_history (resource_id, action, action_by, ip_address)
+                VALUES (%s, 'released', %s, %s)
+            """, (resource_id, session['username'], get_client_ip()))
+    
+    conn.commit()
+    cur.close()
+    conn.close()
+    
+    log_audit('UPDATE', 'resource_pool', None,
+             new_values={'released': released},
+             additional_info=f"{released} Ressourcen aus Quarantäne freigegeben")
+    
+    flash(f'{released} Ressourcen freigegeben', 'success')
+    
+    # Redirect mit allen aktuellen Filtern
+    return redirect(url_for('resources', 
+                          show_test=request.args.get('show_test', request.form.get('show_test', 'false')),
+                          type=request.args.get('type', request.form.get('type', '')),
+                          status=request.args.get('status', request.form.get('status', '')),
+                          search=request.args.get('search', request.form.get('search', ''))))
+
+@app.route('/api/resources/allocate', methods=['POST'])
+@login_required
+def allocate_resources_api():
+    """API für Ressourcen-Zuweisung bei Lizenzerstellung"""
+    data = request.json
+    license_id = data.get('license_id')
+    domain_count = data.get('domain_count', 1)
+    ipv4_count = data.get('ipv4_count', 1)
+    phone_count = data.get('phone_count', 1)
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    try:
+        allocated = {'domains': [], 'ipv4s': [], 'phones': []}
+        
+        # Allocate domains
+        if domain_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'domain' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (domain_count,))
+            domains = cur.fetchall()
+            
+            if len(domains) < domain_count:
+                raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {len(domains)})")
+            
+            for domain_id, domain_value in domains:
+                # Update resource status
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], domain_id))
+                
+                # Create assignment
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, domain_id, session['username']))
+                
+                # Log history
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (domain_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['domains'].append(domain_value)
+        
+        # Allocate IPv4s (similar logic)
+        if ipv4_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'ipv4' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (ipv4_count,))
+            ipv4s = cur.fetchall()
+            
+            if len(ipv4s) < ipv4_count:
+                raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar")
+            
+            for ipv4_id, ipv4_value in ipv4s:
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], ipv4_id))
+                
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, ipv4_id, session['username']))
+                
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (ipv4_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['ipv4s'].append(ipv4_value)
+        
+        # Allocate phones (similar logic)
+        if phone_count > 0:
+            cur.execute("""
+                SELECT id, resource_value FROM resource_pools
+                WHERE resource_type = 'phone' AND status = 'available'
+                LIMIT %s FOR UPDATE
+            """, (phone_count,))
+            phones = cur.fetchall()
+            
+            if len(phones) < phone_count:
+                raise ValueError(f"Nicht genügend Telefonnummern verfügbar")
+            
+            for phone_id, phone_value in phones:
+                cur.execute("""
+                    UPDATE resource_pools
+                    SET status = 'allocated',
+                        allocated_to_license = %s,
+                        status_changed_at = CURRENT_TIMESTAMP,
+                        status_changed_by = %s
+                    WHERE id = %s
+                """, (license_id, session['username'], phone_id))
+                
+                cur.execute("""
+                    INSERT INTO license_resources (license_id, resource_id, assigned_by)
+                    VALUES (%s, %s, %s)
+                """, (license_id, phone_id, session['username']))
+                
+                cur.execute("""
+                    INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
+                    VALUES (%s, %s, 'allocated', %s, %s)
+                """, (phone_id, license_id, session['username'], get_client_ip()))
+                
+                allocated['phones'].append(phone_value)
+        
+        # Update license resource counts
+        cur.execute("""
+            UPDATE licenses
+            SET domain_count = %s,
+                ipv4_count = %s,
+                phone_count = %s
+            WHERE id = %s
+        """, (domain_count, ipv4_count, phone_count, license_id))
+        
+        conn.commit()
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'success': True,
+            'allocated': allocated
+        })
+        
+    except Exception as e:
+        conn.rollback()
+        cur.close()
+        conn.close()
+        return jsonify({
+            'success': False,
+            'error': str(e)
+        }), 400
+
+@app.route('/api/resources/check-availability', methods=['GET'])
+@login_required
+def check_resource_availability():
+    """Prüft verfügbare Ressourcen"""
+    resource_type = request.args.get('type', '')
+    count = request.args.get('count', 10, type=int)
+    show_test = request.args.get('show_test', 'false').lower() == 'true'
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    if resource_type:
+        # Spezifische Ressourcen für einen Typ
+        cur.execute("""
+            SELECT id, resource_value
+            FROM resource_pools
+            WHERE status = 'available' 
+            AND resource_type = %s
+            AND is_test = %s
+            ORDER BY resource_value
+            LIMIT %s
+        """, (resource_type, show_test, count))
+        
+        resources = []
+        for row in cur.fetchall():
+            resources.append({
+                'id': row[0],
+                'value': row[1]
+            })
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify({
+            'available': resources,
+            'type': resource_type,
+            'count': len(resources)
+        })
+    else:
+        # Zusammenfassung aller Typen
+        cur.execute("""
+            SELECT 
+                resource_type,
+                COUNT(*) as available
+            FROM resource_pools
+            WHERE status = 'available'
+            AND is_test = %s
+            GROUP BY resource_type
+        """, (show_test,))
+        
+        availability = {}
+        for row in cur.fetchall():
+            availability[row[0]] = row[1]
+        
+        cur.close()
+        conn.close()
+        
+        return jsonify(availability)
+
+@app.route('/api/global-search', methods=['GET'])
+@login_required
+def global_search():
+    """Global search API endpoint for searching customers and licenses"""
+    query = request.args.get('q', '').strip()
+    
+    if not query or len(query) < 2:
+        return jsonify({'customers': [], 'licenses': []})
+    
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Search pattern with wildcards
+    search_pattern = f'%{query}%'
+    
+    # Search customers
+    cur.execute("""
+        SELECT id, name, email, company_name
+        FROM customers
+        WHERE (LOWER(name) LIKE LOWER(%s) 
+               OR LOWER(email) LIKE LOWER(%s)
+               OR LOWER(company_name) LIKE LOWER(%s))
+        AND is_test = FALSE
+        ORDER BY name
+        LIMIT 5
+    """, (search_pattern, search_pattern, search_pattern))
+    
+    customers = []
+    for row in cur.fetchall():
+        customers.append({
+            'id': row[0],
+            'name': row[1],
+            'email': row[2],
+            'company_name': row[3]
+        })
+    
+    # Search licenses
+    cur.execute("""
+        SELECT l.id, l.license_key, c.name as customer_name
+        FROM licenses l
+        JOIN customers c ON l.customer_id = c.id
+        WHERE LOWER(l.license_key) LIKE LOWER(%s)
+        AND l.is_test = FALSE
+        ORDER BY l.created_at DESC
+        LIMIT 5
+    """, (search_pattern,))
+    
+    licenses = []
+    for row in cur.fetchall():
+        licenses.append({
+            'id': row[0],
+            'license_key': row[1],
+            'customer_name': row[2]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    return jsonify({
+        'customers': customers,
+        'licenses': licenses
+    })
+
+@app.route('/resources/history/<int:resource_id>')
+@login_required
+def resource_history(resource_id):
+    """Zeigt die komplette Historie einer Ressource"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Get complete resource info using named columns
+    cur.execute("""
+        SELECT id, resource_type, resource_value, status, allocated_to_license,
+               status_changed_at, status_changed_by, quarantine_reason, 
+               quarantine_until, created_at, notes
+        FROM resource_pools
+        WHERE id = %s
+    """, (resource_id,))
+    row = cur.fetchone()
+    
+    if not row:
+        flash('Ressource nicht gefunden', 'error')
+        return redirect(url_for('resources'))
+    
+    # Create resource object with named attributes
+    resource = {
+        'id': row[0],
+        'resource_type': row[1],
+        'resource_value': row[2],
+        'status': row[3],
+        'allocated_to_license': row[4],
+        'status_changed_at': row[5],
+        'status_changed_by': row[6],
+        'quarantine_reason': row[7],
+        'quarantine_until': row[8],
+        'created_at': row[9],
+        'notes': row[10]
+    }
+    
+    # Get license info if allocated
+    license_info = None
+    if resource['allocated_to_license']:
+        cur.execute("SELECT license_key FROM licenses WHERE id = %s", 
+                   (resource['allocated_to_license'],))
+        lic = cur.fetchone()
+        if lic:
+            license_info = {'license_key': lic[0]}
+    
+    # Get history with named columns
+    cur.execute("""
+        SELECT 
+            rh.action,
+            rh.action_by,
+            rh.action_at,
+            rh.details,
+            rh.license_id,
+            rh.ip_address
+        FROM resource_history rh
+        WHERE rh.resource_id = %s
+        ORDER BY rh.action_at DESC
+    """, (resource_id,))
+    
+    history = []
+    for row in cur.fetchall():
+        history.append({
+            'action': row[0],
+            'action_by': row[1],
+            'action_at': row[2],
+            'details': row[3],
+            'license_id': row[4],
+            'ip_address': row[5]
+        })
+    
+    cur.close()
+    conn.close()
+    
+    # Convert to object-like for template
+    class ResourceObj:
+        def __init__(self, data):
+            for key, value in data.items():
+                setattr(self, key, value)
+    
+    resource_obj = ResourceObj(resource)
+    history_objs = [ResourceObj(h) for h in history]
+    
+    return render_template('resource_history.html',
+                         resource=resource_obj,
+                         license_info=license_info,
+                         history=history_objs)
+
+@app.route('/resources/metrics')
+@login_required
+def resources_metrics():
+    """Dashboard für Resource Metrics und Reports"""
+    conn = get_connection()
+    cur = conn.cursor()
+    
+    # Overall stats with fallback values
+    cur.execute("""
+        SELECT 
+            COUNT(DISTINCT resource_id) as total_resources,
+            COALESCE(AVG(performance_score), 0) as avg_performance,
+            COALESCE(SUM(cost), 0) as total_cost,
+            COALESCE(SUM(revenue), 0) as total_revenue,
+            COALESCE(SUM(issues_count), 0) as total_issues
+        FROM resource_metrics
+        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
+    """)
+    row = cur.fetchone()
+    
+    # Calculate ROI
+    roi = 0
+    if row[2] > 0:  # if total_cost > 0
+        roi = row[3] / row[2]  # revenue / cost
+    
+    stats = {
+        'total_resources': row[0] or 0,
+        'avg_performance': row[1] or 0,
+        'total_cost': row[2] or 0,
+        'total_revenue': row[3] or 0,
+        'total_issues': row[4] or 0,
+        'roi': roi
+    }
+    
+    # Performance by type
+    cur.execute("""
+        SELECT 
+            rp.resource_type,
+            COALESCE(AVG(rm.performance_score), 0) as avg_score,
+            COUNT(DISTINCT rp.id) as resource_count
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        GROUP BY rp.resource_type
+        ORDER BY rp.resource_type
+    """)
+    performance_by_type = cur.fetchall()
+    
+    # Utilization data
+    cur.execute("""
+        SELECT 
+            resource_type,
+            COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+            COUNT(*) as total,
+            ROUND(COUNT(*) FILTER (WHERE status = 'allocated') * 100.0 / COUNT(*), 1) as allocated_percent
+        FROM resource_pools
+        GROUP BY resource_type
+    """)
+    utilization_rows = cur.fetchall()
+    utilization_data = [
+        {
+            'type': row[0].upper(),
+            'allocated': row[1],
+            'total': row[2],
+            'allocated_percent': row[3]
+        }
+        for row in utilization_rows
+    ]
+    
+    # Top performing resources
+    cur.execute("""
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            COALESCE(AVG(rm.performance_score), 0) as avg_score,
+            COALESCE(SUM(rm.revenue), 0) as total_revenue,
+            COALESCE(SUM(rm.cost), 1) as total_cost,
+            CASE 
+                WHEN COALESCE(SUM(rm.cost), 0) = 0 THEN 0
+                ELSE COALESCE(SUM(rm.revenue), 0) / COALESCE(SUM(rm.cost), 1)
+            END as roi
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        WHERE rp.status != 'quarantine'
+        GROUP BY rp.id, rp.resource_type, rp.resource_value
+        HAVING AVG(rm.performance_score) IS NOT NULL
+        ORDER BY avg_score DESC
+        LIMIT 10
+    """)
+    top_rows = cur.fetchall()
+    top_performers = [
+        {
+            'id': row[0],
+            'resource_type': row[1],
+            'resource_value': row[2],
+            'avg_score': row[3],
+            'roi': row[6]
+        }
+        for row in top_rows
+    ]
+    
+    # Resources with issues
+    cur.execute("""
+        SELECT 
+            rp.id,
+            rp.resource_type,
+            rp.resource_value,
+            rp.status,
+            COALESCE(SUM(rm.issues_count), 0) as total_issues
+        FROM resource_pools rp
+        LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id
+            AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        WHERE rm.issues_count > 0 OR rp.status = 'quarantine'
+        GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
+        HAVING SUM(rm.issues_count) > 0
+        ORDER BY total_issues DESC
+        LIMIT 10
+    """)
+    problem_rows = cur.fetchall()
+    problem_resources = [
+        {
+            'id': row[0],
+            'resource_type': row[1],
+            'resource_value': row[2],
+            'status': row[3],
+            'total_issues': row[4]
+        }
+        for row in problem_rows
+    ]
+    
+    # Daily metrics for trend chart (last 30 days)
+    cur.execute("""
+        SELECT 
+            metric_date,
+            COALESCE(AVG(performance_score), 0) as avg_performance,
+            COALESCE(SUM(issues_count), 0) as total_issues
+        FROM resource_metrics
+        WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days'
+        GROUP BY metric_date
+        ORDER BY metric_date
+    """)
+    daily_rows = cur.fetchall()
+    daily_metrics = [
+        {
+            'date': row[0].strftime('%d.%m'),
+            'performance': float(row[1]),
+            'issues': int(row[2])
+        }
+        for row in daily_rows
+    ]
+    
+    cur.close()
+    conn.close()
+    
+    return render_template('resource_metrics.html',
+                         stats=stats,
+                         performance_by_type=performance_by_type,
+                         utilization_data=utilization_data,
+                         top_performers=top_performers,
+                         problem_resources=problem_resources,
+                         daily_metrics=daily_metrics)
+
+@app.route('/resources/report', methods=['GET'])
+@login_required
+def resources_report():
+    """Generiert Ressourcen-Reports oder zeigt Report-Formular"""
+    # Prüfe ob Download angefordert wurde
+    if request.args.get('download') == 'true':
+        report_type = request.args.get('type', 'usage')
+        format_type = request.args.get('format', 'excel')
+        date_from = request.args.get('from', (datetime.now(ZoneInfo("Europe/Berlin")) - timedelta(days=30)).strftime('%Y-%m-%d'))
+        date_to = request.args.get('to', datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d'))
+        
+        conn = get_connection()
+        cur = conn.cursor()
+        
+        if report_type == 'usage':
+            # Auslastungsreport
+            query = """
+                SELECT 
+                    rp.resource_type,
+                    rp.resource_value,
+                    rp.status,
+                    COUNT(DISTINCT rh.license_id) as unique_licenses,
+                    COUNT(rh.id) as total_allocations,
+                    MIN(rh.action_at) as first_used,
+                    MAX(rh.action_at) as last_used
+                FROM resource_pools rp
+                LEFT JOIN resource_history rh ON rp.id = rh.resource_id 
+                    AND rh.action = 'allocated'
+                    AND rh.action_at BETWEEN %s AND %s
+                GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status
+                ORDER BY rp.resource_type, total_allocations DESC
+            """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Typ', 'Ressource', 'Status', 'Unique Lizenzen', 'Gesamt Zuweisungen', 'Erste Nutzung', 'Letzte Nutzung']
+            
+        elif report_type == 'performance':
+            # Performance-Report
+            query = """
+                SELECT 
+                    rp.resource_type,
+                rp.resource_value,
+                AVG(rm.performance_score) as avg_performance,
+                SUM(rm.usage_count) as total_usage,
+                SUM(rm.revenue) as total_revenue,
+                SUM(rm.cost) as total_cost,
+                SUM(rm.revenue - rm.cost) as profit,
+                SUM(rm.issues_count) as total_issues
+            FROM resource_pools rp
+            JOIN resource_metrics rm ON rp.id = rm.resource_id
+            WHERE rm.metric_date BETWEEN %s AND %s
+            GROUP BY rp.id, rp.resource_type, rp.resource_value
+            ORDER BY profit DESC
+        """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Typ', 'Ressource', 'Durchschn. Performance', 'Gesamt Nutzung', 'Umsatz', 'Kosten', 'Gewinn', 'Issues']
+            
+        elif report_type == 'compliance':
+            # Compliance-Report
+            query = """
+            SELECT 
+                rh.action_at,
+                rh.action,
+                rh.action_by,
+                rp.resource_type,
+                rp.resource_value,
+                l.license_key,
+                c.name as customer_name,
+                rh.ip_address
+            FROM resource_history rh
+            JOIN resource_pools rp ON rh.resource_id = rp.id
+            LEFT JOIN licenses l ON rh.license_id = l.id
+            LEFT JOIN customers c ON l.customer_id = c.id
+            WHERE rh.action_at BETWEEN %s AND %s
+                ORDER BY rh.action_at DESC
+            """
+            cur.execute(query, (date_from, date_to))
+            columns = ['Zeit', 'Aktion', 'Von', 'Typ', 'Ressource', 'Lizenz', 'Kunde', 'IP-Adresse']
+        
+        else:  # inventory report
+            # Inventar-Report
+            query = """
+                SELECT 
+                    resource_type,
+                    COUNT(*) FILTER (WHERE status = 'available') as available,
+                    COUNT(*) FILTER (WHERE status = 'allocated') as allocated,
+                    COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine,
+                    COUNT(*) as total
+                FROM resource_pools
+                GROUP BY resource_type
+                ORDER BY resource_type
+            """
+            cur.execute(query)
+            columns = ['Typ', 'Verfügbar', 'Zugeteilt', 'Quarantäne', 'Gesamt']
+        
+        # Convert to DataFrame
+        data = cur.fetchall()
+        df = pd.DataFrame(data, columns=columns)
+        
+        cur.close()
+        conn.close()
+        
+        # Generate file
+        timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S')
+        filename = f"resource_report_{report_type}_{timestamp}"
+        
+        if format_type == 'excel':
+            output = io.BytesIO()
+            with pd.ExcelWriter(output, engine='openpyxl') as writer:
+                df.to_excel(writer, sheet_name='Report', index=False)
+                
+                # Auto-adjust columns width
+                worksheet = writer.sheets['Report']
+                for column in worksheet.columns:
+                    max_length = 0
+                    column = [cell for cell in column]
+                    for cell in column:
+                        try:
+                            if len(str(cell.value)) > max_length:
+                                max_length = len(str(cell.value))
+                        except:
+                            pass
+                    adjusted_width = (max_length + 2)
+                    worksheet.column_dimensions[column[0].column_letter].width = adjusted_width
+            
+            output.seek(0)
+            
+            log_audit('EXPORT', 'resource_report', None,
+                     new_values={'type': report_type, 'format': 'excel', 'rows': len(df)},
+                     additional_info=f"Resource Report {report_type} exportiert")
+            
+            return send_file(output, 
+                            mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+                            as_attachment=True,
+                            download_name=f'{filename}.xlsx')
+        
+        else:  # CSV
+            output = io.StringIO()
+            df.to_csv(output, index=False, sep=';', encoding='utf-8-sig')
+            output.seek(0)
+            
+            log_audit('EXPORT', 'resource_report', None,
+                     new_values={'type': report_type, 'format': 'csv', 'rows': len(df)},
+                     additional_info=f"Resource Report {report_type} exportiert")
+            
+            return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')),
+                            mimetype='text/csv',
+                            as_attachment=True,
+                            download_name=f'{filename}.csv')
+    
+    # Wenn kein Download, zeige Report-Formular
+    return render_template('resource_report.html',
+                         datetime=datetime,
+                         timedelta=timedelta,
+                         username=session.get('username'))
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=5000)
diff --git a/v2_adminpanel/comment_routes.py b/v2_adminpanel/comment_routes.py
deleted file mode 100644
index 1c5c44d..0000000
--- a/v2_adminpanel/comment_routes.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-"""
-Script to comment out routes that have been moved to blueprints
-"""
-
-# Routes that have been moved to auth_routes.py
-auth_routes = [
-    ("@app.route(\"/login\"", "def login():", 138, 251),  # login route
-    ("@app.route(\"/logout\")", "def logout():", 252, 263),  # logout route
-    ("@app.route(\"/verify-2fa\"", "def verify_2fa():", 264, 342),  # verify-2fa route
-    ("@app.route(\"/profile\")", "def profile():", 343, 352),  # profile route
-    ("@app.route(\"/profile/change-password\"", "def change_password():", 353, 390),  # change-password route
-    ("@app.route(\"/profile/setup-2fa\")", "def setup_2fa():", 391, 410),  # setup-2fa route
-    ("@app.route(\"/profile/enable-2fa\"", "def enable_2fa():", 411, 448),  # enable-2fa route
-    ("@app.route(\"/profile/disable-2fa\"", "def disable_2fa():", 449, 475),  # disable-2fa route
-    ("@app.route(\"/heartbeat\"", "def heartbeat():", 476, 489),  # heartbeat route
-]
-
-# Routes that have been moved to admin_routes.py
-admin_routes = [
-    ("@app.route(\"/\")", "def dashboard():", 647, 870),  # dashboard route
-    ("@app.route(\"/audit\")", "def audit_log():", 2772, 2866),  # audit route
-    ("@app.route(\"/backups\")", "def backups():", 2866, 2901),  # backups route
-    ("@app.route(\"/backup/create\"", "def create_backup_route():", 2901, 2919),  # backup/create route
-    ("@app.route(\"/backup/restore/<int:backup_id>\"", "def restore_backup_route(backup_id):", 2919, 2938),  # backup/restore route
-    ("@app.route(\"/backup/download/<int:backup_id>\")", "def download_backup(backup_id):", 2938, 2970),  # backup/download route
-    ("@app.route(\"/backup/delete/<int:backup_id>\"", "def delete_backup(backup_id):", 2970, 3026),  # backup/delete route
-    ("@app.route(\"/security/blocked-ips\")", "def blocked_ips():", 3026, 3067),  # security/blocked-ips route
-    ("@app.route(\"/security/unblock-ip\"", "def unblock_ip():", 3067, 3093),  # security/unblock-ip route
-    ("@app.route(\"/security/clear-attempts\"", "def clear_attempts():", 3093, 3119),  # security/clear-attempts route
-]
-
-print("This script would comment out the following routes:")
-print("\nAuth routes:")
-for route in auth_routes:
-    print(f"  - {route[0]} (lines {route[2]}-{route[3]})")
-
-print("\nAdmin routes:")
-for route in admin_routes:
-    print(f"  - {route[0]} (lines {route[2]}-{route[3]})")
-
-print("\nNote: Manual verification and adjustment of line numbers is recommended before running the actual commenting.")
\ No newline at end of file
diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql
index fc91a66..5754e24 100644
--- a/v2_adminpanel/init.sql
+++ b/v2_adminpanel/init.sql
@@ -1,282 +1,282 @@
--- UTF-8 Encoding für deutsche Sonderzeichen sicherstellen
-SET client_encoding = 'UTF8';
-
--- Zeitzone auf Europe/Berlin setzen
-SET timezone = 'Europe/Berlin';
-
-CREATE TABLE IF NOT EXISTS customers (
-    id SERIAL PRIMARY KEY,
-    name TEXT NOT NULL,
-    email TEXT,
-    is_test BOOLEAN DEFAULT FALSE,
-    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    CONSTRAINT unique_email UNIQUE (email)
-);
-
-CREATE TABLE IF NOT EXISTS licenses (
-    id SERIAL PRIMARY KEY,
-    license_key TEXT UNIQUE NOT NULL,
-    customer_id INTEGER REFERENCES customers(id),
-    license_type TEXT NOT NULL,
-    valid_from DATE NOT NULL,
-    valid_until DATE NOT NULL,
-    is_active BOOLEAN DEFAULT TRUE,
-    is_test BOOLEAN DEFAULT FALSE,
-    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE TABLE IF NOT EXISTS sessions (
-    id SERIAL PRIMARY KEY,
-    license_id INTEGER REFERENCES licenses(id),
-    session_id TEXT UNIQUE NOT NULL,
-    ip_address TEXT,
-    user_agent TEXT,
-    started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    ended_at TIMESTAMP WITH TIME ZONE,
-    is_active BOOLEAN DEFAULT TRUE
-);
-
--- Audit-Log-Tabelle für Änderungsprotokolle
-CREATE TABLE IF NOT EXISTS audit_log (
-    id SERIAL PRIMARY KEY,
-    timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    username TEXT NOT NULL,
-    action TEXT NOT NULL,
-    entity_type TEXT NOT NULL,
-    entity_id INTEGER,
-    old_values JSONB,
-    new_values JSONB,
-    ip_address TEXT,
-    user_agent TEXT,
-    additional_info TEXT
-);
-
--- Index für bessere Performance bei Abfragen
-CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC);
-CREATE INDEX idx_audit_log_username ON audit_log(username);
-CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
-
--- Backup-Historie-Tabelle
-CREATE TABLE IF NOT EXISTS backup_history (
-    id SERIAL PRIMARY KEY,
-    filename TEXT NOT NULL,
-    filepath TEXT NOT NULL,
-    filesize BIGINT,
-    backup_type TEXT NOT NULL, -- 'manual' oder 'scheduled'
-    status TEXT NOT NULL, -- 'success', 'failed', 'in_progress'
-    error_message TEXT,
-    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    created_by TEXT NOT NULL,
-    tables_count INTEGER,
-    records_count INTEGER,
-    duration_seconds NUMERIC,
-    is_encrypted BOOLEAN DEFAULT TRUE
-);
-
--- Index für bessere Performance
-CREATE INDEX idx_backup_history_created_at ON backup_history(created_at DESC);
-CREATE INDEX idx_backup_history_status ON backup_history(status);
-
--- Login-Attempts-Tabelle für Rate-Limiting
-CREATE TABLE IF NOT EXISTS login_attempts (
-    ip_address VARCHAR(45) PRIMARY KEY,
-    attempt_count INTEGER DEFAULT 0,
-    first_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    last_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    blocked_until TIMESTAMP WITH TIME ZONE NULL,
-    last_username_tried TEXT,
-    last_error_message TEXT
-);
-
--- Index für schnelle Abfragen
-CREATE INDEX idx_login_attempts_blocked_until ON login_attempts(blocked_until);
-CREATE INDEX idx_login_attempts_last_attempt ON login_attempts(last_attempt DESC);
-
--- Migration: Füge created_at zu licenses hinzu, falls noch nicht vorhanden
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'licenses' AND column_name = 'created_at') THEN
-        ALTER TABLE licenses ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
-        
-        -- Setze created_at für bestehende Einträge auf das valid_from Datum
-        UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL;
-    END IF;
-END $$;
-
--- ===================== RESOURCE POOL SYSTEM =====================
-
--- Haupttabelle für den Resource Pool
-CREATE TABLE IF NOT EXISTS resource_pools (
-    id SERIAL PRIMARY KEY,
-    resource_type VARCHAR(20) NOT NULL CHECK (resource_type IN ('domain', 'ipv4', 'phone')),
-    resource_value VARCHAR(255) NOT NULL,
-    status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'allocated', 'quarantine')),
-    allocated_to_license INTEGER REFERENCES licenses(id) ON DELETE SET NULL,
-    status_changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    status_changed_by VARCHAR(50),
-    quarantine_reason VARCHAR(100) CHECK (quarantine_reason IN ('abuse', 'defect', 'maintenance', 'blacklisted', 'expired', 'review', NULL)),
-    quarantine_until TIMESTAMP WITH TIME ZONE,
-    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    notes TEXT,
-    is_test BOOLEAN DEFAULT FALSE,
-    UNIQUE(resource_type, resource_value)
-);
-
--- Resource History für vollständige Nachverfolgbarkeit
-CREATE TABLE IF NOT EXISTS resource_history (
-    id SERIAL PRIMARY KEY,
-    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
-    license_id INTEGER REFERENCES licenses(id) ON DELETE SET NULL,
-    action VARCHAR(50) NOT NULL CHECK (action IN ('allocated', 'deallocated', 'quarantined', 'released', 'created', 'deleted')),
-    action_by VARCHAR(50) NOT NULL,
-    action_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    details JSONB,
-    ip_address TEXT
-);
-
--- Resource Metrics für Performance-Tracking und ROI
-CREATE TABLE IF NOT EXISTS resource_metrics (
-    id SERIAL PRIMARY KEY,
-    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
-    metric_date DATE NOT NULL,
-    usage_count INTEGER DEFAULT 0,
-    performance_score DECIMAL(5,2) DEFAULT 0.00,
-    cost DECIMAL(10,2) DEFAULT 0.00,
-    revenue DECIMAL(10,2) DEFAULT 0.00,
-    issues_count INTEGER DEFAULT 0,
-    availability_percent DECIMAL(5,2) DEFAULT 100.00,
-    UNIQUE(resource_id, metric_date)
-);
-
--- Zuordnungstabelle zwischen Lizenzen und Ressourcen
-CREATE TABLE IF NOT EXISTS license_resources (
-    id SERIAL PRIMARY KEY,
-    license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
-    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
-    assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    assigned_by VARCHAR(50),
-    is_active BOOLEAN DEFAULT TRUE,
-    UNIQUE(license_id, resource_id)
-);
-
--- Erweiterung der licenses Tabelle um Resource-Counts
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'licenses' AND column_name = 'domain_count') THEN
-        ALTER TABLE licenses 
-        ADD COLUMN domain_count INTEGER DEFAULT 1 CHECK (domain_count >= 0 AND domain_count <= 10),
-        ADD COLUMN ipv4_count INTEGER DEFAULT 1 CHECK (ipv4_count >= 0 AND ipv4_count <= 10),
-        ADD COLUMN phone_count INTEGER DEFAULT 1 CHECK (phone_count >= 0 AND phone_count <= 10);
-    END IF;
-END $$;
-
--- Erweiterung der licenses Tabelle um device_limit
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'licenses' AND column_name = 'device_limit') THEN
-        ALTER TABLE licenses 
-        ADD COLUMN device_limit INTEGER DEFAULT 3 CHECK (device_limit >= 1 AND device_limit <= 10);
-    END IF;
-END $$;
-
--- Tabelle für Geräte-Registrierungen
-CREATE TABLE IF NOT EXISTS device_registrations (
-    id SERIAL PRIMARY KEY,
-    license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
-    hardware_id TEXT NOT NULL,
-    device_name TEXT,
-    operating_system TEXT,
-    first_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    last_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    is_active BOOLEAN DEFAULT TRUE,
-    deactivated_at TIMESTAMP WITH TIME ZONE,
-    deactivated_by TEXT,
-    ip_address TEXT,
-    user_agent TEXT,
-    UNIQUE(license_id, hardware_id)
-);
-
--- Indizes für device_registrations
-CREATE INDEX IF NOT EXISTS idx_device_license ON device_registrations(license_id);
-CREATE INDEX IF NOT EXISTS idx_device_hardware ON device_registrations(hardware_id);
-CREATE INDEX IF NOT EXISTS idx_device_active ON device_registrations(license_id, is_active) WHERE is_active = TRUE;
-
--- Indizes für Performance
-CREATE INDEX IF NOT EXISTS idx_resource_status ON resource_pools(status);
-CREATE INDEX IF NOT EXISTS idx_resource_type_status ON resource_pools(resource_type, status);
-CREATE INDEX IF NOT EXISTS idx_resource_allocated ON resource_pools(allocated_to_license) WHERE allocated_to_license IS NOT NULL;
-CREATE INDEX IF NOT EXISTS idx_resource_quarantine ON resource_pools(quarantine_until) WHERE status = 'quarantine';
-CREATE INDEX IF NOT EXISTS idx_resource_history_date ON resource_history(action_at DESC);
-CREATE INDEX IF NOT EXISTS idx_resource_history_resource ON resource_history(resource_id);
-CREATE INDEX IF NOT EXISTS idx_resource_metrics_date ON resource_metrics(metric_date DESC);
-CREATE INDEX IF NOT EXISTS idx_license_resources_active ON license_resources(license_id) WHERE is_active = TRUE;
-
--- Users table for authentication with password and 2FA support
-CREATE TABLE IF NOT EXISTS users (
-    id SERIAL PRIMARY KEY,
-    username VARCHAR(50) UNIQUE NOT NULL,
-    password_hash VARCHAR(255) NOT NULL,
-    email VARCHAR(100),
-    totp_secret VARCHAR(32),
-    totp_enabled BOOLEAN DEFAULT FALSE,
-    backup_codes TEXT, -- JSON array of hashed backup codes
-    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
-    password_reset_token VARCHAR(64),
-    password_reset_expires TIMESTAMP WITH TIME ZONE,
-    failed_2fa_attempts INTEGER DEFAULT 0,
-    last_failed_2fa TIMESTAMP WITH TIME ZONE
-);
-
--- Index for faster login lookups
-CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;
-
--- Migration: Add is_test column to licenses if it doesn't exist
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'licenses' AND column_name = 'is_test') THEN
-        ALTER TABLE licenses ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
-        
-        -- Mark all existing licenses as test data
-        UPDATE licenses SET is_test = TRUE;
-        
-        -- Add index for better performance when filtering test data
-        CREATE INDEX idx_licenses_is_test ON licenses(is_test);
-    END IF;
-END $$;
-
--- Migration: Add is_test column to customers if it doesn't exist
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'customers' AND column_name = 'is_test') THEN
-        ALTER TABLE customers ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
-        
-        -- Mark all existing customers as test data
-        UPDATE customers SET is_test = TRUE;
-        
-        -- Add index for better performance
-        CREATE INDEX idx_customers_is_test ON customers(is_test);
-    END IF;
-END $$;
-
--- Migration: Add is_test column to resource_pools if it doesn't exist
-DO $$ 
-BEGIN
-    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
-                   WHERE table_name = 'resource_pools' AND column_name = 'is_test') THEN
-        ALTER TABLE resource_pools ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
-        
-        -- Mark all existing resources as test data
-        UPDATE resource_pools SET is_test = TRUE;
-        
-        -- Add index for better performance
-        CREATE INDEX idx_resource_pools_is_test ON resource_pools(is_test);
-    END IF;
-END $$;
+-- UTF-8 Encoding für deutsche Sonderzeichen sicherstellen
+SET client_encoding = 'UTF8';
+
+-- Zeitzone auf Europe/Berlin setzen
+SET timezone = 'Europe/Berlin';
+
+CREATE TABLE IF NOT EXISTS customers (
+    id SERIAL PRIMARY KEY,
+    name TEXT NOT NULL,
+    email TEXT,
+    is_test BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    CONSTRAINT unique_email UNIQUE (email)
+);
+
+CREATE TABLE IF NOT EXISTS licenses (
+    id SERIAL PRIMARY KEY,
+    license_key TEXT UNIQUE NOT NULL,
+    customer_id INTEGER REFERENCES customers(id),
+    license_type TEXT NOT NULL,
+    valid_from DATE NOT NULL,
+    valid_until DATE NOT NULL,
+    is_active BOOLEAN DEFAULT TRUE,
+    is_test BOOLEAN DEFAULT FALSE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS sessions (
+    id SERIAL PRIMARY KEY,
+    license_id INTEGER REFERENCES licenses(id),
+    session_id TEXT UNIQUE NOT NULL,
+    ip_address TEXT,
+    user_agent TEXT,
+    started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    last_heartbeat TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    ended_at TIMESTAMP WITH TIME ZONE,
+    is_active BOOLEAN DEFAULT TRUE
+);
+
+-- Audit-Log-Tabelle für Änderungsprotokolle
+CREATE TABLE IF NOT EXISTS audit_log (
+    id SERIAL PRIMARY KEY,
+    timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    username TEXT NOT NULL,
+    action TEXT NOT NULL,
+    entity_type TEXT NOT NULL,
+    entity_id INTEGER,
+    old_values JSONB,
+    new_values JSONB,
+    ip_address TEXT,
+    user_agent TEXT,
+    additional_info TEXT
+);
+
+-- Index für bessere Performance bei Abfragen
+CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC);
+CREATE INDEX idx_audit_log_username ON audit_log(username);
+CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
+
+-- Backup-Historie-Tabelle
+CREATE TABLE IF NOT EXISTS backup_history (
+    id SERIAL PRIMARY KEY,
+    filename TEXT NOT NULL,
+    filepath TEXT NOT NULL,
+    filesize BIGINT,
+    backup_type TEXT NOT NULL, -- 'manual' oder 'scheduled'
+    status TEXT NOT NULL, -- 'success', 'failed', 'in_progress'
+    error_message TEXT,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    created_by TEXT NOT NULL,
+    tables_count INTEGER,
+    records_count INTEGER,
+    duration_seconds NUMERIC,
+    is_encrypted BOOLEAN DEFAULT TRUE
+);
+
+-- Index für bessere Performance
+CREATE INDEX idx_backup_history_created_at ON backup_history(created_at DESC);
+CREATE INDEX idx_backup_history_status ON backup_history(status);
+
+-- Login-Attempts-Tabelle für Rate-Limiting
+CREATE TABLE IF NOT EXISTS login_attempts (
+    ip_address VARCHAR(45) PRIMARY KEY,
+    attempt_count INTEGER DEFAULT 0,
+    first_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    last_attempt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    blocked_until TIMESTAMP WITH TIME ZONE NULL,
+    last_username_tried TEXT,
+    last_error_message TEXT
+);
+
+-- Index für schnelle Abfragen
+CREATE INDEX idx_login_attempts_blocked_until ON login_attempts(blocked_until);
+CREATE INDEX idx_login_attempts_last_attempt ON login_attempts(last_attempt DESC);
+
+-- Migration: Füge created_at zu licenses hinzu, falls noch nicht vorhanden
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'licenses' AND column_name = 'created_at') THEN
+        ALTER TABLE licenses ADD COLUMN created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
+        
+        -- Setze created_at für bestehende Einträge auf das valid_from Datum
+        UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL;
+    END IF;
+END $$;
+
+-- ===================== RESOURCE POOL SYSTEM =====================
+
+-- Haupttabelle für den Resource Pool
+CREATE TABLE IF NOT EXISTS resource_pools (
+    id SERIAL PRIMARY KEY,
+    resource_type VARCHAR(20) NOT NULL CHECK (resource_type IN ('domain', 'ipv4', 'phone')),
+    resource_value VARCHAR(255) NOT NULL,
+    status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'allocated', 'quarantine')),
+    allocated_to_license INTEGER REFERENCES licenses(id) ON DELETE SET NULL,
+    status_changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    status_changed_by VARCHAR(50),
+    quarantine_reason VARCHAR(100) CHECK (quarantine_reason IN ('abuse', 'defect', 'maintenance', 'blacklisted', 'expired', 'review', NULL)),
+    quarantine_until TIMESTAMP WITH TIME ZONE,
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    notes TEXT,
+    is_test BOOLEAN DEFAULT FALSE,
+    UNIQUE(resource_type, resource_value)
+);
+
+-- Resource History für vollständige Nachverfolgbarkeit
+CREATE TABLE IF NOT EXISTS resource_history (
+    id SERIAL PRIMARY KEY,
+    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
+    license_id INTEGER REFERENCES licenses(id) ON DELETE SET NULL,
+    action VARCHAR(50) NOT NULL CHECK (action IN ('allocated', 'deallocated', 'quarantined', 'released', 'created', 'deleted')),
+    action_by VARCHAR(50) NOT NULL,
+    action_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    details JSONB,
+    ip_address TEXT
+);
+
+-- Resource Metrics für Performance-Tracking und ROI
+CREATE TABLE IF NOT EXISTS resource_metrics (
+    id SERIAL PRIMARY KEY,
+    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
+    metric_date DATE NOT NULL,
+    usage_count INTEGER DEFAULT 0,
+    performance_score DECIMAL(5,2) DEFAULT 0.00,
+    cost DECIMAL(10,2) DEFAULT 0.00,
+    revenue DECIMAL(10,2) DEFAULT 0.00,
+    issues_count INTEGER DEFAULT 0,
+    availability_percent DECIMAL(5,2) DEFAULT 100.00,
+    UNIQUE(resource_id, metric_date)
+);
+
+-- Zuordnungstabelle zwischen Lizenzen und Ressourcen
+CREATE TABLE IF NOT EXISTS license_resources (
+    id SERIAL PRIMARY KEY,
+    license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
+    resource_id INTEGER REFERENCES resource_pools(id) ON DELETE CASCADE,
+    assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    assigned_by VARCHAR(50),
+    is_active BOOLEAN DEFAULT TRUE,
+    UNIQUE(license_id, resource_id)
+);
+
+-- Erweiterung der licenses Tabelle um Resource-Counts
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'licenses' AND column_name = 'domain_count') THEN
+        ALTER TABLE licenses 
+        ADD COLUMN domain_count INTEGER DEFAULT 1 CHECK (domain_count >= 0 AND domain_count <= 10),
+        ADD COLUMN ipv4_count INTEGER DEFAULT 1 CHECK (ipv4_count >= 0 AND ipv4_count <= 10),
+        ADD COLUMN phone_count INTEGER DEFAULT 1 CHECK (phone_count >= 0 AND phone_count <= 10);
+    END IF;
+END $$;
+
+-- Erweiterung der licenses Tabelle um device_limit
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'licenses' AND column_name = 'device_limit') THEN
+        ALTER TABLE licenses 
+        ADD COLUMN device_limit INTEGER DEFAULT 3 CHECK (device_limit >= 1 AND device_limit <= 10);
+    END IF;
+END $$;
+
+-- Tabelle für Geräte-Registrierungen
+CREATE TABLE IF NOT EXISTS device_registrations (
+    id SERIAL PRIMARY KEY,
+    license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
+    hardware_id TEXT NOT NULL,
+    device_name TEXT,
+    operating_system TEXT,
+    first_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    last_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    is_active BOOLEAN DEFAULT TRUE,
+    deactivated_at TIMESTAMP WITH TIME ZONE,
+    deactivated_by TEXT,
+    ip_address TEXT,
+    user_agent TEXT,
+    UNIQUE(license_id, hardware_id)
+);
+
+-- Indizes für device_registrations
+CREATE INDEX IF NOT EXISTS idx_device_license ON device_registrations(license_id);
+CREATE INDEX IF NOT EXISTS idx_device_hardware ON device_registrations(hardware_id);
+CREATE INDEX IF NOT EXISTS idx_device_active ON device_registrations(license_id, is_active) WHERE is_active = TRUE;
+
+-- Indizes für Performance
+CREATE INDEX IF NOT EXISTS idx_resource_status ON resource_pools(status);
+CREATE INDEX IF NOT EXISTS idx_resource_type_status ON resource_pools(resource_type, status);
+CREATE INDEX IF NOT EXISTS idx_resource_allocated ON resource_pools(allocated_to_license) WHERE allocated_to_license IS NOT NULL;
+CREATE INDEX IF NOT EXISTS idx_resource_quarantine ON resource_pools(quarantine_until) WHERE status = 'quarantine';
+CREATE INDEX IF NOT EXISTS idx_resource_history_date ON resource_history(action_at DESC);
+CREATE INDEX IF NOT EXISTS idx_resource_history_resource ON resource_history(resource_id);
+CREATE INDEX IF NOT EXISTS idx_resource_metrics_date ON resource_metrics(metric_date DESC);
+CREATE INDEX IF NOT EXISTS idx_license_resources_active ON license_resources(license_id) WHERE is_active = TRUE;
+
+-- Users table for authentication with password and 2FA support
+CREATE TABLE IF NOT EXISTS users (
+    id SERIAL PRIMARY KEY,
+    username VARCHAR(50) UNIQUE NOT NULL,
+    password_hash VARCHAR(255) NOT NULL,
+    email VARCHAR(100),
+    totp_secret VARCHAR(32),
+    totp_enabled BOOLEAN DEFAULT FALSE,
+    backup_codes TEXT, -- JSON array of hashed backup codes
+    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    last_password_change TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+    password_reset_token VARCHAR(64),
+    password_reset_expires TIMESTAMP WITH TIME ZONE,
+    failed_2fa_attempts INTEGER DEFAULT 0,
+    last_failed_2fa TIMESTAMP WITH TIME ZONE
+);
+
+-- Index for faster login lookups
+CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
+CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;
+
+-- Migration: Add is_test column to licenses if it doesn't exist
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'licenses' AND column_name = 'is_test') THEN
+        ALTER TABLE licenses ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
+        
+        -- Mark all existing licenses as test data
+        UPDATE licenses SET is_test = TRUE;
+        
+        -- Add index for better performance when filtering test data
+        CREATE INDEX idx_licenses_is_test ON licenses(is_test);
+    END IF;
+END $$;
+
+-- Migration: Add is_test column to customers if it doesn't exist
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'customers' AND column_name = 'is_test') THEN
+        ALTER TABLE customers ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
+        
+        -- Mark all existing customers as test data
+        UPDATE customers SET is_test = TRUE;
+        
+        -- Add index for better performance
+        CREATE INDEX idx_customers_is_test ON customers(is_test);
+    END IF;
+END $$;
+
+-- Migration: Add is_test column to resource_pools if it doesn't exist
+DO $$ 
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.columns 
+                   WHERE table_name = 'resource_pools' AND column_name = 'is_test') THEN
+        ALTER TABLE resource_pools ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
+        
+        -- Mark all existing resources as test data
+        UPDATE resource_pools SET is_test = TRUE;
+        
+        -- Add index for better performance
+        CREATE INDEX idx_resource_pools_is_test ON resource_pools(is_test);
+    END IF;
+END $$;
diff --git a/v2_adminpanel/requirements.txt b/v2_adminpanel/requirements.txt
index f1fcc95..8588045 100644
--- a/v2_adminpanel/requirements.txt
+++ b/v2_adminpanel/requirements.txt
@@ -1,14 +1,14 @@
-flask
-flask-session
-psycopg2-binary
-python-dotenv
-pyopenssl
-pandas
-openpyxl
-cryptography
-apscheduler
-requests
-python-dateutil
-bcrypt
-pyotp
-qrcode[pil]
+flask
+flask-session
+psycopg2-binary
+python-dotenv
+pyopenssl
+pandas
+openpyxl
+cryptography
+apscheduler
+requests
+python-dateutil
+bcrypt
+pyotp
+qrcode[pil]
diff --git a/v2_adminpanel/templates/create_customer.html b/v2_adminpanel/templates/create_customer.html
index 55518fc..689ad85 100644
--- a/v2_adminpanel/templates/create_customer.html
+++ b/v2_adminpanel/templates/create_customer.html
@@ -1,71 +1,71 @@
-{% extends "base.html" %}
-
-{% block title %}Neuer Kunde{% endblock %}
-
-{% block content %}
-<div class="container py-5">
-    <div class="d-flex justify-content-between align-items-center mb-4">
-        <h2>👤 Neuer Kunde anlegen</h2>
-        <a href="/customers-licenses" class="btn btn-secondary">← Zurück zur Übersicht</a>
-    </div>
-
-    <div class="card">
-        <div class="card-body">
-            <form method="post" action="/customer/create" accept-charset="UTF-8">
-                <div class="row g-3">
-                    <div class="col-md-6">
-                        <label for="name" class="form-label">Kundenname <span class="text-danger">*</span></label>
-                        <input type="text" class="form-control" id="name" name="name" 
-                               placeholder="Firmenname oder Vor- und Nachname" 
-                               accept-charset="UTF-8" required autofocus>
-                        <div class="form-text">Der Name des Kunden oder der Firma</div>
-                    </div>
-                    <div class="col-md-6">
-                        <label for="email" class="form-label">E-Mail <span class="text-danger">*</span></label>
-                        <input type="email" class="form-control" id="email" name="email" 
-                               placeholder="kunde@beispiel.de"
-                               accept-charset="UTF-8" required>
-                        <div class="form-text">Kontakt-E-Mail-Adresse des Kunden</div>
-                    </div>
-                </div>
-
-                <div class="form-check mt-3">
-                    <input class="form-check-input" type="checkbox" id="isTest" name="is_test">
-                    <label class="form-check-label" for="isTest">
-                        <i class="fas fa-flask"></i> Als Testdaten markieren
-                        <small class="text-muted">(Kunde wird von der Software ignoriert)</small>
-                    </label>
-                </div>
-
-                <div class="alert alert-info mt-4" role="alert">
-                    <i class="fas fa-info-circle"></i> 
-                    <strong>Hinweis:</strong> Nach dem Anlegen des Kunden können Sie direkt Lizenzen für diesen Kunden erstellen.
-                </div>
-
-                <div class="mt-4">
-                    <button type="submit" class="btn btn-primary">💾 Kunde anlegen</button>
-                    <a href="/customers-licenses" class="btn btn-secondary">Abbrechen</a>
-                </div>
-            </form>
-        </div>
-    </div>
-</div>
-
-<!-- Flash Messages -->
-{% with messages = get_flashed_messages(with_categories=true) %}
-    {% if messages %}
-        <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
-            {% for category, message in messages %}
-                <div class="toast show align-items-center text-white bg-{{ 'danger' if category == 'error' else 'success' }} border-0" role="alert">
-                    <div class="d-flex">
-                        <div class="toast-body">
-                            {{ message }}
-                        </div>
-                        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
-                    </div>
-                </div>
-            {% endfor %}
-        </div>
-    {% endif %}
-{% endwith %}
+{% extends "base.html" %}
+
+{% block title %}Neuer Kunde{% endblock %}
+
+{% block content %}
+<div class="container py-5">
+    <div class="d-flex justify-content-between align-items-center mb-4">
+        <h2>👤 Neuer Kunde anlegen</h2>
+        <a href="/customers-licenses" class="btn btn-secondary">← Zurück zur Übersicht</a>
+    </div>
+
+    <div class="card">
+        <div class="card-body">
+            <form method="post" action="/customer/create" accept-charset="UTF-8">
+                <div class="row g-3">
+                    <div class="col-md-6">
+                        <label for="name" class="form-label">Kundenname <span class="text-danger">*</span></label>
+                        <input type="text" class="form-control" id="name" name="name" 
+                               placeholder="Firmenname oder Vor- und Nachname" 
+                               accept-charset="UTF-8" required autofocus>
+                        <div class="form-text">Der Name des Kunden oder der Firma</div>
+                    </div>
+                    <div class="col-md-6">
+                        <label for="email" class="form-label">E-Mail <span class="text-danger">*</span></label>
+                        <input type="email" class="form-control" id="email" name="email" 
+                               placeholder="kunde@beispiel.de"
+                               accept-charset="UTF-8" required>
+                        <div class="form-text">Kontakt-E-Mail-Adresse des Kunden</div>
+                    </div>
+                </div>
+
+                <div class="form-check mt-3">
+                    <input class="form-check-input" type="checkbox" id="isTest" name="is_test">
+                    <label class="form-check-label" for="isTest">
+                        <i class="fas fa-flask"></i> Als Testdaten markieren
+                        <small class="text-muted">(Kunde wird von der Software ignoriert)</small>
+                    </label>
+                </div>
+
+                <div class="alert alert-info mt-4" role="alert">
+                    <i class="fas fa-info-circle"></i> 
+                    <strong>Hinweis:</strong> Nach dem Anlegen des Kunden können Sie direkt Lizenzen für diesen Kunden erstellen.
+                </div>
+
+                <div class="mt-4">
+                    <button type="submit" class="btn btn-primary">💾 Kunde anlegen</button>
+                    <a href="/customers-licenses" class="btn btn-secondary">Abbrechen</a>
+                </div>
+            </form>
+        </div>
+    </div>
+</div>
+
+<!-- Flash Messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% if messages %}
+        <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
+            {% for category, message in messages %}
+                <div class="toast show align-items-center text-white bg-{{ 'danger' if category == 'error' else 'success' }} border-0" role="alert">
+                    <div class="d-flex">
+                        <div class="toast-body">
+                            {{ message }}
+                        </div>
+                        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
+                    </div>
+                </div>
+            {% endfor %}
+        </div>
+    {% endif %}
+{% endwith %}
 {% endblock %}
\ No newline at end of file
diff --git a/v2_adminpanel/templates/index.html b/v2_adminpanel/templates/index.html
index a14329a..2e0810f 100644
--- a/v2_adminpanel/templates/index.html
+++ b/v2_adminpanel/templates/index.html
@@ -1,533 +1,533 @@
-{% extends "base.html" %}
-
-{% block title %}Admin Panel{% endblock %}
-
-{% block content %}
-<div class="container py-5">
-    <div class="d-flex justify-content-between align-items-center mb-4">
-        <h2>Neue Lizenz erstellen</h2>
-        <a href="/customers-licenses" class="btn btn-secondary">← Zurück zur Übersicht</a>
-    </div>
-
-    <form method="post" action="/create" accept-charset="UTF-8">
-        <div class="row g-3">
-            <div class="col-md-12">
-                <label for="customerSelect" class="form-label">Kunde auswählen</label>
-                <select class="form-select" id="customerSelect" name="customer_id" required>
-                    <option value="">🔍 Kunde suchen oder neuen Kunden anlegen...</option>
-                    <option value="new">➕ Neuer Kunde</option>
-                </select>
-            </div>
-            <div class="col-md-6" id="customerNameDiv" style="display: none;">
-                <label for="customerName" class="form-label">Kundenname</label>
-                <input type="text" class="form-control" id="customerName" name="customer_name" accept-charset="UTF-8">
-            </div>
-            <div class="col-md-6" id="emailDiv" style="display: none;">
-                <label for="email" class="form-label">E-Mail</label>
-                <input type="email" class="form-control" id="email" name="email" accept-charset="UTF-8">
-            </div>
-            <div class="col-md-4">
-                <label for="licenseKey" class="form-label">Lizenzschlüssel</label>
-                <div class="input-group">
-                    <input type="text" class="form-control" id="licenseKey" name="license_key" 
-                           placeholder="AF-F-YYYYMM-XXXX-YYYY-ZZZZ" required
-                           pattern="AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}"
-                           title="Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ">
-                    <button type="button" class="btn btn-outline-primary" onclick="generateLicenseKey()">
-                        🔑 Generieren
-                    </button>
-                </div>
-                <div class="form-text">Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ (F=Full, T=Test)</div>
-            </div>
-            <div class="col-md-4">
-                <label for="licenseType" class="form-label">Lizenztyp</label>
-                <select class="form-select" id="licenseType" name="license_type" required>
-                    <option value="full">Vollversion</option>
-                    <option value="test">Testversion</option>
-                </select>
-            </div>
-            <div class="col-md-2">
-                <label for="validFrom" class="form-label">Kaufdatum</label>
-                <input type="date" class="form-control" id="validFrom" name="valid_from" required>
-            </div>
-            <div class="col-md-1">
-                <label for="duration" class="form-label">Laufzeit</label>
-                <input type="number" class="form-control" id="duration" name="duration" value="1" min="1" required>
-            </div>
-            <div class="col-md-1">
-                <label for="durationType" class="form-label">Einheit</label>
-                <select class="form-select" id="durationType" name="duration_type" required>
-                    <option value="days">Tage</option>
-                    <option value="months">Monate</option>
-                    <option value="years" selected>Jahre</option>
-                </select>
-            </div>
-            <div class="col-md-2">
-                <label for="validUntil" class="form-label">Ablaufdatum</label>
-                <input type="date" class="form-control" id="validUntil" name="valid_until" readonly style="background-color: #e9ecef;">
-            </div>
-        </div>
-
-        <!-- Resource Pool Allocation -->
-        <div class="card mt-4">
-            <div class="card-header">
-                <h5 class="mb-0">
-                    <i class="fas fa-server"></i> Ressourcen-Zuweisung
-                    <small class="text-muted float-end" id="resourceStatus"></small>
-                </h5>
-            </div>
-            <div class="card-body">
-                <div class="row g-3">
-                    <div class="col-md-4">
-                        <label for="domainCount" class="form-label">
-                            <i class="fas fa-globe"></i> Domains
-                        </label>
-                        <select class="form-select" id="domainCount" name="domain_count" required>
-                            {% for i in range(11) %}
-                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
-                            {% endfor %}
-                        </select>
-                        <small class="form-text text-muted">
-                            Verfügbar: <span id="domainsAvailable" class="fw-bold">-</span>
-                        </small>
-                    </div>
-                    <div class="col-md-4">
-                        <label for="ipv4Count" class="form-label">
-                            <i class="fas fa-network-wired"></i> IPv4-Adressen
-                        </label>
-                        <select class="form-select" id="ipv4Count" name="ipv4_count" required>
-                            {% for i in range(11) %}
-                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
-                            {% endfor %}
-                        </select>
-                        <small class="form-text text-muted">
-                            Verfügbar: <span id="ipv4Available" class="fw-bold">-</span>
-                        </small>
-                    </div>
-                    <div class="col-md-4">
-                        <label for="phoneCount" class="form-label">
-                            <i class="fas fa-phone"></i> Telefonnummern
-                        </label>
-                        <select class="form-select" id="phoneCount" name="phone_count" required>
-                            {% for i in range(11) %}
-                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
-                            {% endfor %}
-                        </select>
-                        <small class="form-text text-muted">
-                            Verfügbar: <span id="phoneAvailable" class="fw-bold">-</span>
-                        </small>
-                    </div>
-                </div>
-                <div class="alert alert-info mt-3 mb-0" role="alert">
-                    <i class="fas fa-info-circle"></i> 
-                    Die Ressourcen werden bei der Lizenzerstellung automatisch aus dem Pool zugewiesen.
-                    Wählen Sie 0, wenn für diesen Typ keine Ressourcen benötigt werden.
-                </div>
-            </div>
-        </div>
-
-        <!-- Device Limit -->
-        <div class="card mt-4">
-            <div class="card-header">
-                <h5 class="mb-0">
-                    <i class="fas fa-laptop"></i> Gerätelimit
-                </h5>
-            </div>
-            <div class="card-body">
-                <div class="row">
-                    <div class="col-md-6">
-                        <label for="deviceLimit" class="form-label">
-                            Maximale Anzahl Geräte
-                        </label>
-                        <select class="form-select" id="deviceLimit" name="device_limit" required>
-                            {% for i in range(1, 11) %}
-                            <option value="{{ i }}" {% if i == 3 %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
-                            {% endfor %}
-                        </select>
-                        <small class="form-text text-muted">
-                            Anzahl der Geräte, auf denen die Lizenz gleichzeitig aktiviert sein kann.
-                        </small>
-                    </div>
-                </div>
-            </div>
-        </div>
-
-        <!-- Test Data Checkbox -->
-        <div class="form-check mt-3">
-            <input class="form-check-input" type="checkbox" id="isTest" name="is_test">
-            <label class="form-check-label" for="isTest">
-                <i class="fas fa-flask"></i> Als Testdaten markieren
-                <small class="text-muted">(wird von der Software ignoriert)</small>
-            </label>
-        </div>
-
-        <div class="mt-4">
-            <button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
-        </div>
-    </form>
-</div>
-
-<!-- Flash Messages -->
-{% with messages = get_flashed_messages(with_categories=true) %}
-    {% if messages %}
-        <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
-            {% for category, message in messages %}
-                <div class="toast show align-items-center text-white bg-{{ 'danger' if category == 'error' else 'success' }} border-0" role="alert">
-                    <div class="d-flex">
-                        <div class="toast-body">
-                            {{ message }}
-                        </div>
-                        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
-                    </div>
-                </div>
-            {% endfor %}
-        </div>
-    {% endif %}
-{% endwith %}
-
-<script>
-// License Key Generator
-function generateLicenseKey() {
-    const licenseType = document.getElementById('licenseType').value;
-    
-    // Zeige Ladeindikator
-    const button = event.target;
-    const originalText = button.innerHTML;
-    button.disabled = true;
-    button.innerHTML = '⏳ Generiere...';
-    
-    // API-Call
-    fetch('/api/generate-license-key', {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({type: licenseType})
-    })
-    .then(response => response.json())
-    .then(data => {
-        if (data.success) {
-            document.getElementById('licenseKey').value = data.key;
-            // Visuelles Feedback
-            document.getElementById('licenseKey').classList.add('border-success');
-            setTimeout(() => {
-                document.getElementById('licenseKey').classList.remove('border-success');
-            }, 2000);
-        } else {
-            alert('Fehler bei der Key-Generierung: ' + (data.error || 'Unbekannter Fehler'));
-        }
-    })
-    .catch(error => {
-        console.error('Fehler:', error);
-        alert('Netzwerkfehler bei der Key-Generierung');
-    })
-    .finally(() => {
-        // Button zurücksetzen
-        button.disabled = false;
-        button.innerHTML = originalText;
-    });
-}
-
-// Event Listener für Lizenztyp-Änderung
-document.getElementById('licenseType').addEventListener('change', function() {
-    const keyField = document.getElementById('licenseKey');
-    if (keyField.value && keyField.value.startsWith('AF-')) {
-        // Prüfe ob der Key zum neuen Typ passt
-        const currentType = this.value;
-        const keyType = keyField.value.charAt(3); // Position des F/T im Key (AF-F-...)
-        
-        if ((currentType === 'full' && keyType === 'T') || 
-            (currentType === 'test' && keyType === 'F')) {
-            if (confirm('Der aktuelle Key passt nicht zum gewählten Lizenztyp. Neuen Key generieren?')) {
-                generateLicenseKey();
-            }
-        }
-    }
-});
-
-// Auto-Uppercase für License Key Input
-document.getElementById('licenseKey').addEventListener('input', function(e) {
-    e.target.value = e.target.value.toUpperCase();
-});
-
-// Funktion zur Berechnung des Ablaufdatums
-function calculateValidUntil() {
-    const validFrom = document.getElementById('validFrom').value;
-    const duration = parseInt(document.getElementById('duration').value) || 1;
-    const durationType = document.getElementById('durationType').value;
-    
-    if (!validFrom) return;
-    
-    const startDate = new Date(validFrom);
-    let endDate = new Date(startDate);
-    
-    switch(durationType) {
-        case 'days':
-            endDate.setDate(endDate.getDate() + duration);
-            break;
-        case 'months':
-            endDate.setMonth(endDate.getMonth() + duration);
-            break;
-        case 'years':
-            endDate.setFullYear(endDate.getFullYear() + duration);
-            break;
-    }
-    
-    // Ein Tag abziehen, da der Starttag mitgezählt wird
-    endDate.setDate(endDate.getDate() - 1);
-    
-    document.getElementById('validUntil').value = endDate.toISOString().split('T')[0];
-}
-
-// Event Listener für Änderungen
-document.getElementById('validFrom').addEventListener('change', calculateValidUntil);
-document.getElementById('duration').addEventListener('input', calculateValidUntil);
-document.getElementById('durationType').addEventListener('change', calculateValidUntil);
-
-// Setze heutiges Datum als Standard für valid_from
-document.addEventListener('DOMContentLoaded', function() {
-    const today = new Date().toISOString().split('T')[0];
-    document.getElementById('validFrom').value = today;
-    
-    // Berechne initiales Ablaufdatum
-    calculateValidUntil();
-    
-    // Initialisiere Select2 für Kundenauswahl
-    $('#customerSelect').select2({
-        theme: 'bootstrap-5',
-        placeholder: '🔍 Kunde suchen oder neuen Kunden anlegen...',
-        allowClear: true,
-        ajax: {
-            url: '/api/customers',
-            dataType: 'json',
-            delay: 250,
-            data: function (params) {
-                return {
-                    q: params.term,
-                    page: params.page || 1
-                };
-            },
-            processResults: function (data, params) {
-                params.page = params.page || 1;
-                
-                // "Neuer Kunde" Option immer oben anzeigen
-                const results = data.results || [];
-                if (params.page === 1) {
-                    results.unshift({
-                        id: 'new',
-                        text: '➕ Neuer Kunde',
-                        isNew: true
-                    });
-                }
-                
-                return {
-                    results: results,
-                    pagination: data.pagination
-                };
-            },
-            cache: true
-        },
-        minimumInputLength: 0,
-        language: {
-            inputTooShort: function() { return ''; },
-            noResults: function() { return 'Keine Kunden gefunden'; },
-            searching: function() { return 'Suche...'; },
-            loadingMore: function() { return 'Lade weitere Ergebnisse...'; }
-        }
-    });
-    
-    // Vorausgewählten Kunden setzen (falls von kombinierter Ansicht kommend)
-    {% if preselected_customer_id %}
-    // Lade Kundendetails und setze Auswahl
-    fetch('/api/customers?id={{ preselected_customer_id }}')
-        .then(response => response.json())
-        .then(data => {
-            if (data.results && data.results.length > 0) {
-                const customer = data.results[0];
-                // Erstelle Option und setze sie als ausgewählt
-                const option = new Option(customer.text, customer.id, true, true);
-                $('#customerSelect').append(option).trigger('change');
-                // Verstecke die Eingabefelder
-                document.getElementById('customerNameDiv').style.display = 'none';
-                document.getElementById('emailDiv').style.display = 'none';
-            }
-        });
-    {% endif %}
-    
-    // Event Handler für Kundenauswahl
-    $('#customerSelect').on('select2:select', function (e) {
-        const selectedValue = e.params.data.id;
-        const nameDiv = document.getElementById('customerNameDiv');
-        const emailDiv = document.getElementById('emailDiv');
-        const nameInput = document.getElementById('customerName');
-        const emailInput = document.getElementById('email');
-        
-        if (selectedValue === 'new') {
-            // Zeige Eingabefelder für neuen Kunden
-            nameDiv.style.display = 'block';
-            emailDiv.style.display = 'block';
-            nameInput.required = true;
-            emailInput.required = true;
-        } else {
-            // Verstecke Eingabefelder bei bestehendem Kunden
-            nameDiv.style.display = 'none';
-            emailDiv.style.display = 'none';
-            nameInput.required = false;
-            emailInput.required = false;
-            nameInput.value = '';
-            emailInput.value = '';
-        }
-    });
-    
-    // Clear handler
-    $('#customerSelect').on('select2:clear', function (e) {
-        document.getElementById('customerNameDiv').style.display = 'none';
-        document.getElementById('emailDiv').style.display = 'none';
-        document.getElementById('customerName').required = false;
-        document.getElementById('email').required = false;
-    });
-    
-    // Resource Availability Check
-    checkResourceAvailability();
-    
-    // Event Listener für Resource Count Änderungen
-    document.getElementById('domainCount').addEventListener('change', checkResourceAvailability);
-    document.getElementById('ipv4Count').addEventListener('change', checkResourceAvailability);
-    document.getElementById('phoneCount').addEventListener('change', checkResourceAvailability);
-});
-
-// Funktion zur Prüfung der Ressourcen-Verfügbarkeit
-function checkResourceAvailability() {
-    const domainCount = parseInt(document.getElementById('domainCount').value) || 0;
-    const ipv4Count = parseInt(document.getElementById('ipv4Count').value) || 0;
-    const phoneCount = parseInt(document.getElementById('phoneCount').value) || 0;
-    
-    // API-Call zur Verfügbarkeitsprüfung
-    fetch(`/api/resources/check-availability?domain=${domainCount}&ipv4=${ipv4Count}&phone=${phoneCount}`)
-        .then(response => response.json())
-        .then(data => {
-            // Update der Verfügbarkeitsanzeigen
-            updateAvailabilityDisplay('domainsAvailable', data.domain_available, domainCount);
-            updateAvailabilityDisplay('ipv4Available', data.ipv4_available, ipv4Count);
-            updateAvailabilityDisplay('phoneAvailable', data.phone_available, phoneCount);
-            
-            // Gesamtstatus aktualisieren
-            updateResourceStatus(data, domainCount, ipv4Count, phoneCount);
-        })
-        .catch(error => {
-            console.error('Fehler bei Verfügbarkeitsprüfung:', error);
-        });
-}
-
-// Hilfsfunktion zur Anzeige der Verfügbarkeit
-function updateAvailabilityDisplay(elementId, available, requested) {
-    const element = document.getElementById(elementId);
-    const container = element.parentElement;
-    
-    // Verfügbarkeit mit Prozentanzeige
-    const percent = Math.round((available / (available + requested + 50)) * 100);
-    let statusHtml = `<strong>${available}</strong>`;
-    
-    if (requested > 0 && available < requested) {
-        element.classList.remove('text-success', 'text-warning');
-        element.classList.add('text-danger');
-        statusHtml += ` <i class="bi bi-exclamation-triangle"></i>`;
-        
-        // Füge Warnung hinzu
-        if (!container.querySelector('.availability-warning')) {
-            const warning = document.createElement('div');
-            warning.className = 'availability-warning text-danger small mt-1';
-            warning.innerHTML = `<i class="bi bi-x-circle"></i> Nicht genügend Ressourcen verfügbar!`;
-            container.appendChild(warning);
-        }
-    } else {
-        // Entferne Warnung wenn vorhanden
-        const warning = container.querySelector('.availability-warning');
-        if (warning) warning.remove();
-        
-        if (available < 20) {
-            element.classList.remove('text-success');
-            element.classList.add('text-danger');
-            statusHtml += ` <span class="badge bg-danger">Kritisch</span>`;
-        } else if (available < 50) {
-            element.classList.remove('text-success', 'text-danger');
-            element.classList.add('text-warning');
-            statusHtml += ` <span class="badge bg-warning text-dark">Niedrig</span>`;
-        } else {
-            element.classList.remove('text-danger', 'text-warning');
-            element.classList.add('text-success');
-            statusHtml += ` <span class="badge bg-success">OK</span>`;
-        }
-    }
-    
-    element.innerHTML = statusHtml;
-    
-    // Zeige Fortschrittsbalken
-    updateResourceProgressBar(elementId.replace('Available', ''), available, requested);
-}
-
-// Fortschrittsbalken für Ressourcen
-function updateResourceProgressBar(resourceType, available, requested) {
-    const progressId = `${resourceType}Progress`;
-    let progressBar = document.getElementById(progressId);
-    
-    // Erstelle Fortschrittsbalken wenn nicht vorhanden
-    if (!progressBar) {
-        const container = document.querySelector(`#${resourceType}Available`).parentElement.parentElement;
-        const progressDiv = document.createElement('div');
-        progressDiv.className = 'mt-2';
-        progressDiv.innerHTML = `
-            <div class="progress" style="height: 20px;" id="${progressId}">
-                <div class="progress-bar bg-success" role="progressbar" style="width: 0%">
-                    <span class="progress-text"></span>
-                </div>
-            </div>
-        `;
-        container.appendChild(progressDiv);
-        progressBar = document.getElementById(progressId);
-    }
-    
-    const total = available + requested;
-    const availablePercent = total > 0 ? (available / total) * 100 : 100;
-    const bar = progressBar.querySelector('.progress-bar');
-    const text = progressBar.querySelector('.progress-text');
-    
-    // Setze Farbe basierend auf Verfügbarkeit
-    bar.classList.remove('bg-success', 'bg-warning', 'bg-danger');
-    if (requested > 0 && available < requested) {
-        bar.classList.add('bg-danger');
-    } else if (availablePercent < 30) {
-        bar.classList.add('bg-warning');
-    } else {
-        bar.classList.add('bg-success');
-    }
-    
-    // Animiere Fortschrittsbalken
-    bar.style.width = `${availablePercent}%`;
-    text.textContent = requested > 0 ? `${available} von ${total}` : `${available} verfügbar`;
-}
-
-// Gesamtstatus der Ressourcen-Verfügbarkeit
-function updateResourceStatus(data, domainCount, ipv4Count, phoneCount) {
-    const statusElement = document.getElementById('resourceStatus');
-    let hasIssue = false;
-    let message = '';
-    
-    if (domainCount > 0 && data.domain_available < domainCount) {
-        hasIssue = true;
-        message = '⚠️ Nicht genügend Domains';
-    } else if (ipv4Count > 0 && data.ipv4_available < ipv4Count) {
-        hasIssue = true;
-        message = '⚠️ Nicht genügend IPv4-Adressen';
-    } else if (phoneCount > 0 && data.phone_available < phoneCount) {
-        hasIssue = true;
-        message = '⚠️ Nicht genügend Telefonnummern';
-    } else {
-        message = '✅ Alle Ressourcen verfügbar';
-    }
-    
-    statusElement.textContent = message;
-    statusElement.className = hasIssue ? 'text-danger' : 'text-success';
-}
-</script>
-{% endblock %}
+{% extends "base.html" %}
+
+{% block title %}Admin Panel{% endblock %}
+
+{% block content %}
+<div class="container py-5">
+    <div class="d-flex justify-content-between align-items-center mb-4">
+        <h2>Neue Lizenz erstellen</h2>
+        <a href="/customers-licenses" class="btn btn-secondary">← Zurück zur Übersicht</a>
+    </div>
+
+    <form method="post" action="/create" accept-charset="UTF-8">
+        <div class="row g-3">
+            <div class="col-md-12">
+                <label for="customerSelect" class="form-label">Kunde auswählen</label>
+                <select class="form-select" id="customerSelect" name="customer_id" required>
+                    <option value="">🔍 Kunde suchen oder neuen Kunden anlegen...</option>
+                    <option value="new">➕ Neuer Kunde</option>
+                </select>
+            </div>
+            <div class="col-md-6" id="customerNameDiv" style="display: none;">
+                <label for="customerName" class="form-label">Kundenname</label>
+                <input type="text" class="form-control" id="customerName" name="customer_name" accept-charset="UTF-8">
+            </div>
+            <div class="col-md-6" id="emailDiv" style="display: none;">
+                <label for="email" class="form-label">E-Mail</label>
+                <input type="email" class="form-control" id="email" name="email" accept-charset="UTF-8">
+            </div>
+            <div class="col-md-4">
+                <label for="licenseKey" class="form-label">Lizenzschlüssel</label>
+                <div class="input-group">
+                    <input type="text" class="form-control" id="licenseKey" name="license_key" 
+                           placeholder="AF-F-YYYYMM-XXXX-YYYY-ZZZZ" required
+                           pattern="AF-[FT]-\d{6}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}"
+                           title="Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ">
+                    <button type="button" class="btn btn-outline-primary" onclick="generateLicenseKey()">
+                        🔑 Generieren
+                    </button>
+                </div>
+                <div class="form-text">Format: AF-F-YYYYMM-XXXX-YYYY-ZZZZ (F=Full, T=Test)</div>
+            </div>
+            <div class="col-md-4">
+                <label for="licenseType" class="form-label">Lizenztyp</label>
+                <select class="form-select" id="licenseType" name="license_type" required>
+                    <option value="full">Vollversion</option>
+                    <option value="test">Testversion</option>
+                </select>
+            </div>
+            <div class="col-md-2">
+                <label for="validFrom" class="form-label">Kaufdatum</label>
+                <input type="date" class="form-control" id="validFrom" name="valid_from" required>
+            </div>
+            <div class="col-md-1">
+                <label for="duration" class="form-label">Laufzeit</label>
+                <input type="number" class="form-control" id="duration" name="duration" value="1" min="1" required>
+            </div>
+            <div class="col-md-1">
+                <label for="durationType" class="form-label">Einheit</label>
+                <select class="form-select" id="durationType" name="duration_type" required>
+                    <option value="days">Tage</option>
+                    <option value="months">Monate</option>
+                    <option value="years" selected>Jahre</option>
+                </select>
+            </div>
+            <div class="col-md-2">
+                <label for="validUntil" class="form-label">Ablaufdatum</label>
+                <input type="date" class="form-control" id="validUntil" name="valid_until" readonly style="background-color: #e9ecef;">
+            </div>
+        </div>
+
+        <!-- Resource Pool Allocation -->
+        <div class="card mt-4">
+            <div class="card-header">
+                <h5 class="mb-0">
+                    <i class="fas fa-server"></i> Ressourcen-Zuweisung
+                    <small class="text-muted float-end" id="resourceStatus"></small>
+                </h5>
+            </div>
+            <div class="card-body">
+                <div class="row g-3">
+                    <div class="col-md-4">
+                        <label for="domainCount" class="form-label">
+                            <i class="fas fa-globe"></i> Domains
+                        </label>
+                        <select class="form-select" id="domainCount" name="domain_count" required>
+                            {% for i in range(11) %}
+                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
+                            {% endfor %}
+                        </select>
+                        <small class="form-text text-muted">
+                            Verfügbar: <span id="domainsAvailable" class="fw-bold">-</span>
+                        </small>
+                    </div>
+                    <div class="col-md-4">
+                        <label for="ipv4Count" class="form-label">
+                            <i class="fas fa-network-wired"></i> IPv4-Adressen
+                        </label>
+                        <select class="form-select" id="ipv4Count" name="ipv4_count" required>
+                            {% for i in range(11) %}
+                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
+                            {% endfor %}
+                        </select>
+                        <small class="form-text text-muted">
+                            Verfügbar: <span id="ipv4Available" class="fw-bold">-</span>
+                        </small>
+                    </div>
+                    <div class="col-md-4">
+                        <label for="phoneCount" class="form-label">
+                            <i class="fas fa-phone"></i> Telefonnummern
+                        </label>
+                        <select class="form-select" id="phoneCount" name="phone_count" required>
+                            {% for i in range(11) %}
+                            <option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
+                            {% endfor %}
+                        </select>
+                        <small class="form-text text-muted">
+                            Verfügbar: <span id="phoneAvailable" class="fw-bold">-</span>
+                        </small>
+                    </div>
+                </div>
+                <div class="alert alert-info mt-3 mb-0" role="alert">
+                    <i class="fas fa-info-circle"></i> 
+                    Die Ressourcen werden bei der Lizenzerstellung automatisch aus dem Pool zugewiesen.
+                    Wählen Sie 0, wenn für diesen Typ keine Ressourcen benötigt werden.
+                </div>
+            </div>
+        </div>
+
+        <!-- Device Limit -->
+        <div class="card mt-4">
+            <div class="card-header">
+                <h5 class="mb-0">
+                    <i class="fas fa-laptop"></i> Gerätelimit
+                </h5>
+            </div>
+            <div class="card-body">
+                <div class="row">
+                    <div class="col-md-6">
+                        <label for="deviceLimit" class="form-label">
+                            Maximale Anzahl Geräte
+                        </label>
+                        <select class="form-select" id="deviceLimit" name="device_limit" required>
+                            {% for i in range(1, 11) %}
+                            <option value="{{ i }}" {% if i == 3 %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
+                            {% endfor %}
+                        </select>
+                        <small class="form-text text-muted">
+                            Anzahl der Geräte, auf denen die Lizenz gleichzeitig aktiviert sein kann.
+                        </small>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Test Data Checkbox -->
+        <div class="form-check mt-3">
+            <input class="form-check-input" type="checkbox" id="isTest" name="is_test">
+            <label class="form-check-label" for="isTest">
+                <i class="fas fa-flask"></i> Als Testdaten markieren
+                <small class="text-muted">(wird von der Software ignoriert)</small>
+            </label>
+        </div>
+
+        <div class="mt-4">
+            <button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
+        </div>
+    </form>
+</div>
+
+<!-- Flash Messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% if messages %}
+        <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
+            {% for category, message in messages %}
+                <div class="toast show align-items-center text-white bg-{{ 'danger' if category == 'error' else 'success' }} border-0" role="alert">
+                    <div class="d-flex">
+                        <div class="toast-body">
+                            {{ message }}
+                        </div>
+                        <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
+                    </div>
+                </div>
+            {% endfor %}
+        </div>
+    {% endif %}
+{% endwith %}
+
+<script>
+// License Key Generator
+function generateLicenseKey() {
+    const licenseType = document.getElementById('licenseType').value;
+    
+    // Zeige Ladeindikator
+    const button = event.target;
+    const originalText = button.innerHTML;
+    button.disabled = true;
+    button.innerHTML = '⏳ Generiere...';
+    
+    // API-Call
+    fetch('/api/generate-license-key', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({type: licenseType})
+    })
+    .then(response => response.json())
+    .then(data => {
+        if (data.success) {
+            document.getElementById('licenseKey').value = data.key;
+            // Visuelles Feedback
+            document.getElementById('licenseKey').classList.add('border-success');
+            setTimeout(() => {
+                document.getElementById('licenseKey').classList.remove('border-success');
+            }, 2000);
+        } else {
+            alert('Fehler bei der Key-Generierung: ' + (data.error || 'Unbekannter Fehler'));
+        }
+    })
+    .catch(error => {
+        console.error('Fehler:', error);
+        alert('Netzwerkfehler bei der Key-Generierung');
+    })
+    .finally(() => {
+        // Button zurücksetzen
+        button.disabled = false;
+        button.innerHTML = originalText;
+    });
+}
+
+// Event Listener für Lizenztyp-Änderung
+document.getElementById('licenseType').addEventListener('change', function() {
+    const keyField = document.getElementById('licenseKey');
+    if (keyField.value && keyField.value.startsWith('AF-')) {
+        // Prüfe ob der Key zum neuen Typ passt
+        const currentType = this.value;
+        const keyType = keyField.value.charAt(3); // Position des F/T im Key (AF-F-...)
+        
+        if ((currentType === 'full' && keyType === 'T') || 
+            (currentType === 'test' && keyType === 'F')) {
+            if (confirm('Der aktuelle Key passt nicht zum gewählten Lizenztyp. Neuen Key generieren?')) {
+                generateLicenseKey();
+            }
+        }
+    }
+});
+
+// Auto-Uppercase für License Key Input
+document.getElementById('licenseKey').addEventListener('input', function(e) {
+    e.target.value = e.target.value.toUpperCase();
+});
+
+// Funktion zur Berechnung des Ablaufdatums
+function calculateValidUntil() {
+    const validFrom = document.getElementById('validFrom').value;
+    const duration = parseInt(document.getElementById('duration').value) || 1;
+    const durationType = document.getElementById('durationType').value;
+    
+    if (!validFrom) return;
+    
+    const startDate = new Date(validFrom);
+    let endDate = new Date(startDate);
+    
+    switch(durationType) {
+        case 'days':
+            endDate.setDate(endDate.getDate() + duration);
+            break;
+        case 'months':
+            endDate.setMonth(endDate.getMonth() + duration);
+            break;
+        case 'years':
+            endDate.setFullYear(endDate.getFullYear() + duration);
+            break;
+    }
+    
+    // Ein Tag abziehen, da der Starttag mitgezählt wird
+    endDate.setDate(endDate.getDate() - 1);
+    
+    document.getElementById('validUntil').value = endDate.toISOString().split('T')[0];
+}
+
+// Event Listener für Änderungen
+document.getElementById('validFrom').addEventListener('change', calculateValidUntil);
+document.getElementById('duration').addEventListener('input', calculateValidUntil);
+document.getElementById('durationType').addEventListener('change', calculateValidUntil);
+
+// Setze heutiges Datum als Standard für valid_from
+document.addEventListener('DOMContentLoaded', function() {
+    const today = new Date().toISOString().split('T')[0];
+    document.getElementById('validFrom').value = today;
+    
+    // Berechne initiales Ablaufdatum
+    calculateValidUntil();
+    
+    // Initialisiere Select2 für Kundenauswahl
+    $('#customerSelect').select2({
+        theme: 'bootstrap-5',
+        placeholder: '🔍 Kunde suchen oder neuen Kunden anlegen...',
+        allowClear: true,
+        ajax: {
+            url: '/api/customers',
+            dataType: 'json',
+            delay: 250,
+            data: function (params) {
+                return {
+                    q: params.term,
+                    page: params.page || 1
+                };
+            },
+            processResults: function (data, params) {
+                params.page = params.page || 1;
+                
+                // "Neuer Kunde" Option immer oben anzeigen
+                const results = data.results || [];
+                if (params.page === 1) {
+                    results.unshift({
+                        id: 'new',
+                        text: '➕ Neuer Kunde',
+                        isNew: true
+                    });
+                }
+                
+                return {
+                    results: results,
+                    pagination: data.pagination
+                };
+            },
+            cache: true
+        },
+        minimumInputLength: 0,
+        language: {
+            inputTooShort: function() { return ''; },
+            noResults: function() { return 'Keine Kunden gefunden'; },
+            searching: function() { return 'Suche...'; },
+            loadingMore: function() { return 'Lade weitere Ergebnisse...'; }
+        }
+    });
+    
+    // Vorausgewählten Kunden setzen (falls von kombinierter Ansicht kommend)
+    {% if preselected_customer_id %}
+    // Lade Kundendetails und setze Auswahl
+    fetch('/api/customers?id={{ preselected_customer_id }}')
+        .then(response => response.json())
+        .then(data => {
+            if (data.results && data.results.length > 0) {
+                const customer = data.results[0];
+                // Erstelle Option und setze sie als ausgewählt
+                const option = new Option(customer.text, customer.id, true, true);
+                $('#customerSelect').append(option).trigger('change');
+                // Verstecke die Eingabefelder
+                document.getElementById('customerNameDiv').style.display = 'none';
+                document.getElementById('emailDiv').style.display = 'none';
+            }
+        });
+    {% endif %}
+    
+    // Event Handler für Kundenauswahl
+    $('#customerSelect').on('select2:select', function (e) {
+        const selectedValue = e.params.data.id;
+        const nameDiv = document.getElementById('customerNameDiv');
+        const emailDiv = document.getElementById('emailDiv');
+        const nameInput = document.getElementById('customerName');
+        const emailInput = document.getElementById('email');
+        
+        if (selectedValue === 'new') {
+            // Zeige Eingabefelder für neuen Kunden
+            nameDiv.style.display = 'block';
+            emailDiv.style.display = 'block';
+            nameInput.required = true;
+            emailInput.required = true;
+        } else {
+            // Verstecke Eingabefelder bei bestehendem Kunden
+            nameDiv.style.display = 'none';
+            emailDiv.style.display = 'none';
+            nameInput.required = false;
+            emailInput.required = false;
+            nameInput.value = '';
+            emailInput.value = '';
+        }
+    });
+    
+    // Clear handler
+    $('#customerSelect').on('select2:clear', function (e) {
+        document.getElementById('customerNameDiv').style.display = 'none';
+        document.getElementById('emailDiv').style.display = 'none';
+        document.getElementById('customerName').required = false;
+        document.getElementById('email').required = false;
+    });
+    
+    // Resource Availability Check
+    checkResourceAvailability();
+    
+    // Event Listener für Resource Count Änderungen
+    document.getElementById('domainCount').addEventListener('change', checkResourceAvailability);
+    document.getElementById('ipv4Count').addEventListener('change', checkResourceAvailability);
+    document.getElementById('phoneCount').addEventListener('change', checkResourceAvailability);
+});
+
+// Funktion zur Prüfung der Ressourcen-Verfügbarkeit
+function checkResourceAvailability() {
+    const domainCount = parseInt(document.getElementById('domainCount').value) || 0;
+    const ipv4Count = parseInt(document.getElementById('ipv4Count').value) || 0;
+    const phoneCount = parseInt(document.getElementById('phoneCount').value) || 0;
+    
+    // API-Call zur Verfügbarkeitsprüfung
+    fetch(`/api/resources/check-availability?domain=${domainCount}&ipv4=${ipv4Count}&phone=${phoneCount}`)
+        .then(response => response.json())
+        .then(data => {
+            // Update der Verfügbarkeitsanzeigen
+            updateAvailabilityDisplay('domainsAvailable', data.domain_available, domainCount);
+            updateAvailabilityDisplay('ipv4Available', data.ipv4_available, ipv4Count);
+            updateAvailabilityDisplay('phoneAvailable', data.phone_available, phoneCount);
+            
+            // Gesamtstatus aktualisieren
+            updateResourceStatus(data, domainCount, ipv4Count, phoneCount);
+        })
+        .catch(error => {
+            console.error('Fehler bei Verfügbarkeitsprüfung:', error);
+        });
+}
+
+// Hilfsfunktion zur Anzeige der Verfügbarkeit
+function updateAvailabilityDisplay(elementId, available, requested) {
+    const element = document.getElementById(elementId);
+    const container = element.parentElement;
+    
+    // Verfügbarkeit mit Prozentanzeige
+    const percent = Math.round((available / (available + requested + 50)) * 100);
+    let statusHtml = `<strong>${available}</strong>`;
+    
+    if (requested > 0 && available < requested) {
+        element.classList.remove('text-success', 'text-warning');
+        element.classList.add('text-danger');
+        statusHtml += ` <i class="bi bi-exclamation-triangle"></i>`;
+        
+        // Füge Warnung hinzu
+        if (!container.querySelector('.availability-warning')) {
+            const warning = document.createElement('div');
+            warning.className = 'availability-warning text-danger small mt-1';
+            warning.innerHTML = `<i class="bi bi-x-circle"></i> Nicht genügend Ressourcen verfügbar!`;
+            container.appendChild(warning);
+        }
+    } else {
+        // Entferne Warnung wenn vorhanden
+        const warning = container.querySelector('.availability-warning');
+        if (warning) warning.remove();
+        
+        if (available < 20) {
+            element.classList.remove('text-success');
+            element.classList.add('text-danger');
+            statusHtml += ` <span class="badge bg-danger">Kritisch</span>`;
+        } else if (available < 50) {
+            element.classList.remove('text-success', 'text-danger');
+            element.classList.add('text-warning');
+            statusHtml += ` <span class="badge bg-warning text-dark">Niedrig</span>`;
+        } else {
+            element.classList.remove('text-danger', 'text-warning');
+            element.classList.add('text-success');
+            statusHtml += ` <span class="badge bg-success">OK</span>`;
+        }
+    }
+    
+    element.innerHTML = statusHtml;
+    
+    // Zeige Fortschrittsbalken
+    updateResourceProgressBar(elementId.replace('Available', ''), available, requested);
+}
+
+// Fortschrittsbalken für Ressourcen
+function updateResourceProgressBar(resourceType, available, requested) {
+    const progressId = `${resourceType}Progress`;
+    let progressBar = document.getElementById(progressId);
+    
+    // Erstelle Fortschrittsbalken wenn nicht vorhanden
+    if (!progressBar) {
+        const container = document.querySelector(`#${resourceType}Available`).parentElement.parentElement;
+        const progressDiv = document.createElement('div');
+        progressDiv.className = 'mt-2';
+        progressDiv.innerHTML = `
+            <div class="progress" style="height: 20px;" id="${progressId}">
+                <div class="progress-bar bg-success" role="progressbar" style="width: 0%">
+                    <span class="progress-text"></span>
+                </div>
+            </div>
+        `;
+        container.appendChild(progressDiv);
+        progressBar = document.getElementById(progressId);
+    }
+    
+    const total = available + requested;
+    const availablePercent = total > 0 ? (available / total) * 100 : 100;
+    const bar = progressBar.querySelector('.progress-bar');
+    const text = progressBar.querySelector('.progress-text');
+    
+    // Setze Farbe basierend auf Verfügbarkeit
+    bar.classList.remove('bg-success', 'bg-warning', 'bg-danger');
+    if (requested > 0 && available < requested) {
+        bar.classList.add('bg-danger');
+    } else if (availablePercent < 30) {
+        bar.classList.add('bg-warning');
+    } else {
+        bar.classList.add('bg-success');
+    }
+    
+    // Animiere Fortschrittsbalken
+    bar.style.width = `${availablePercent}%`;
+    text.textContent = requested > 0 ? `${available} von ${total}` : `${available} verfügbar`;
+}
+
+// Gesamtstatus der Ressourcen-Verfügbarkeit
+function updateResourceStatus(data, domainCount, ipv4Count, phoneCount) {
+    const statusElement = document.getElementById('resourceStatus');
+    let hasIssue = false;
+    let message = '';
+    
+    if (domainCount > 0 && data.domain_available < domainCount) {
+        hasIssue = true;
+        message = '⚠️ Nicht genügend Domains';
+    } else if (ipv4Count > 0 && data.ipv4_available < ipv4Count) {
+        hasIssue = true;
+        message = '⚠️ Nicht genügend IPv4-Adressen';
+    } else if (phoneCount > 0 && data.phone_available < phoneCount) {
+        hasIssue = true;
+        message = '⚠️ Nicht genügend Telefonnummern';
+    } else {
+        message = '✅ Alle Ressourcen verfügbar';
+    }
+    
+    statusElement.textContent = message;
+    statusElement.className = hasIssue ? 'text-danger' : 'text-success';
+}
+</script>
+{% endblock %}
diff --git a/v2_nginx/nginx.conf b/v2_nginx/nginx.conf
index 5830a2f..c3bbf80 100644
--- a/v2_nginx/nginx.conf
+++ b/v2_nginx/nginx.conf
@@ -1,99 +1,99 @@
-events {
-    worker_connections 1024;
-}
-
-http {
-    # Moderne SSL-Einstellungen für maximale Sicherheit
-    ssl_protocols TLSv1.2 TLSv1.3;
-    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
-    ssl_prefer_server_ciphers off;
-    
-    # SSL Session Einstellungen
-    ssl_session_timeout 1d;
-    ssl_session_cache shared:SSL:10m;
-    ssl_session_tickets off;
-    
-    # OCSP Stapling
-    ssl_stapling on;
-    ssl_stapling_verify on;
-    resolver 8.8.8.8 8.8.4.4 valid=300s;
-    resolver_timeout 5s;
-    
-    # DH parameters für Perfect Forward Secrecy
-    ssl_dhparam /etc/nginx/ssl/dhparam.pem;
-
-    # Admin Panel
-    server {
-        listen 80;
-        server_name admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com;
-        
-        # Redirect HTTP to HTTPS
-        return 301 https://$server_name$request_uri;
-    }
-
-    server {
-        listen 443 ssl;
-        server_name admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com;
-
-        # SSL-Zertifikate (echte Zertifikate)
-        ssl_certificate /etc/nginx/ssl/fullchain.pem;
-        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
-
-        # Security Headers
-        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-        add_header X-Content-Type-Options "nosniff" always;
-        add_header X-Frame-Options "SAMEORIGIN" always;
-        add_header X-XSS-Protection "1; mode=block" always;
-        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
-
-        # Proxy-Einstellungen
-        location / {
-            proxy_pass http://admin-panel:5000;
-            proxy_set_header Host $host;
-            proxy_set_header X-Real-IP $remote_addr;
-            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-            proxy_set_header X-Forwarded-Proto $scheme;
-            
-            # WebSocket support (falls benötigt)
-            proxy_http_version 1.1;
-            proxy_set_header Upgrade $http_upgrade;
-            proxy_set_header Connection "upgrade";
-        }
-    }
-
-    # API Server (für später)
-    server {
-        listen 80;
-        server_name api-software-undso.z5m7q9dk3ah2v1plx6ju.com;
-        
-        return 301 https://$server_name$request_uri;
-    }
-
-    server {
-        listen 443 ssl;
-        server_name api-software-undso.z5m7q9dk3ah2v1plx6ju.com;
-
-        ssl_certificate /etc/nginx/ssl/fullchain.pem;
-        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
-
-        # Security Headers
-        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
-        add_header X-Content-Type-Options "nosniff" always;
-        add_header X-Frame-Options "SAMEORIGIN" always;
-        add_header X-XSS-Protection "1; mode=block" always;
-        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
-
-        location / {
-            proxy_pass http://license-server:8443;
-            proxy_set_header Host $host;
-            proxy_set_header X-Real-IP $remote_addr;
-            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-            proxy_set_header X-Forwarded-Proto $scheme;
-            
-            # WebSocket support (falls benötigt)
-            proxy_http_version 1.1;
-            proxy_set_header Upgrade $http_upgrade;
-            proxy_set_header Connection "upgrade";
-        }
-    }
+events {
+    worker_connections 1024;
+}
+
+http {
+    # Moderne SSL-Einstellungen für maximale Sicherheit
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
+    ssl_prefer_server_ciphers off;
+    
+    # SSL Session Einstellungen
+    ssl_session_timeout 1d;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_tickets off;
+    
+    # OCSP Stapling
+    ssl_stapling on;
+    ssl_stapling_verify on;
+    resolver 8.8.8.8 8.8.4.4 valid=300s;
+    resolver_timeout 5s;
+    
+    # DH parameters für Perfect Forward Secrecy
+    ssl_dhparam /etc/nginx/ssl/dhparam.pem;
+
+    # Admin Panel
+    server {
+        listen 80;
+        server_name admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com;
+        
+        # Redirect HTTP to HTTPS
+        return 301 https://$server_name$request_uri;
+    }
+
+    server {
+        listen 443 ssl;
+        server_name admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com;
+
+        # SSL-Zertifikate (echte Zertifikate)
+        ssl_certificate /etc/nginx/ssl/fullchain.pem;
+        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+        # Security Headers
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+        add_header X-Content-Type-Options "nosniff" always;
+        add_header X-Frame-Options "SAMEORIGIN" always;
+        add_header X-XSS-Protection "1; mode=block" always;
+        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+        # Proxy-Einstellungen
+        location / {
+            proxy_pass http://admin-panel:5000;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            
+            # WebSocket support (falls benötigt)
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "upgrade";
+        }
+    }
+
+    # API Server (für später)
+    server {
+        listen 80;
+        server_name api-software-undso.z5m7q9dk3ah2v1plx6ju.com;
+        
+        return 301 https://$server_name$request_uri;
+    }
+
+    server {
+        listen 443 ssl;
+        server_name api-software-undso.z5m7q9dk3ah2v1plx6ju.com;
+
+        ssl_certificate /etc/nginx/ssl/fullchain.pem;
+        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
+
+        # Security Headers
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+        add_header X-Content-Type-Options "nosniff" always;
+        add_header X-Frame-Options "SAMEORIGIN" always;
+        add_header X-XSS-Protection "1; mode=block" always;
+        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+        location / {
+            proxy_pass http://license-server:8443;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header X-Forwarded-Proto $scheme;
+            
+            # WebSocket support (falls benötigt)
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "upgrade";
+        }
+    }
 }
\ No newline at end of file
