diff --git a/JOURNAL.md b/JOURNAL.md index 8fc864f..0ce55b7 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -2186,4 +2186,95 @@ docker-compose up -d - `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 \ No newline at end of file +**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert + +### 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 \ No newline at end of file diff --git a/THE_ROAD_SO_FAR.md b/THE_ROAD_SO_FAR.md index 1927ab4..02ad0c7 100644 --- a/THE_ROAD_SO_FAR.md +++ b/THE_ROAD_SO_FAR.md @@ -1,5 +1,5 @@ # The Road So Far -Stand: 09.06.2025 - 14:45 Uhr +Stand: 09.06.2025 - 15:39 Uhr ## 🚀 Aktueller Status @@ -20,7 +20,7 @@ Stand: 09.06.2025 - 14:45 Uhr 3. **Admin Panel (Vollständig implementiert)** - Flask-Anwendung mit Session-Management - Login für 2 Admin-User (rac00n, w@rh@mm3r) - - Dashboard mit Statistiken + - Dashboard mit Statistiken (ohne Testdaten) - Komplette CRUD-Funktionalität für Lizenzen und Kunden - Session-Tracking und -Verwaltung - Audit-Log für alle Aktionen @@ -33,6 +33,7 @@ Stand: 09.06.2025 - 14:45 Uhr - 2FA (Two-Factor Authentication) - Passwort-Änderung - Resource Pool Management + - **NEU: Test-Flag System für Lizenzen** 4. **Internet-Zugriff** - Admin Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com ✅ @@ -54,6 +55,15 @@ Stand: 09.06.2025 - 14:45 Uhr - UI für Management, Historie, Metriken - Integration in Lizenzerstellung (0-10 Ressourcen pro Typ) +7. **Test-Daten Management** (ERWEITERT!) + - `is_test` Flag für Lizenzen, Kunden und Resource Pools + - Testdaten werden in allen Statistiken ausgefiltert + - UI-Kennzeichnung mit 🧪 Badge überall + - Filter für "Testdaten" und "Live-Daten" + - Automatische Verknüpfung: Test-Kunde → Test-Lizenz → Test-Ressourcen + - Konsistente Test/Live-Trennung über alle Ebenen + - Alle bestehenden Daten als Test markiert (19 Lizenzen, 5 Kunden, 20 Ressourcen) + ### ❌ Was noch fehlt 1. **License Server API** (Hauptaufgabe!) @@ -64,20 +74,71 @@ Stand: 09.06.2025 - 14:45 Uhr ## 🎯 Nächste Schritte (Priorität Hoch) -1. **License Server API implementieren** +### Admin Panel Änderungen (NEUE PRIORITÄT!) + +1. **Kunden ohne Lizenz anlegen** + - Kundenerstellung ohne Lizenzzwang ermöglichen + - UI-Anpassung im Customer-Form + - Backend-Validierung anpassen + +2. **Audit Log IP-Problem beheben** + - IP-Adressen werden nicht korrekt gespeichert/angezeigt + - Proxy-Header (X-Forwarded-For) korrekt auslesen + - Real-IP Ermittlung hinter Nginx + +3. **E-Mail Benachrichtigungen für ablaufende Lizenzen** + - IMAP-Einstellungen im Profil (pro Admin-User) + - Benachrichtigungszeitpunkte: 1 Monat, 3 Wochen, 2 Wochen, 1 Woche, 3 Tage, 1 Tag vor Ablauf + - Tabelle für IMAP-Settings erstellen + - Background-Job für E-Mail-Versand + - Template für Ablauf-E-Mails + +4. **Backup-Löschfunktion** + - Delete-Button für Backups hinzufügen + - Sicherheitsabfrage vor Löschung + - Audit-Log Eintrag bei Löschung + +### Weitere geplante Features + +1. **Verlängerungs-System im Admin Panel** + - Verlängerungs-Vorlagen (manual, standard, upgrade_path, trial_to_full) + - Lizenzbasierte Verlängerungseinstellungen (nicht kundenbasiert) + - Tabelle `renewal_templates` erstellen + - `licenses` Tabelle erweitern (renewal_template, renewal_count) + - UI für Verlängerungs-Verwaltung pro Lizenz + - Bulk-Änderung von Verlängerungseinstellungen + - Dashboard-Widget für anstehende Verlängerungen + - Verlängerungs-Historie pro Lizenz anzeigen + - Automatische Verlängerung nach Vorlage + +5. **Device-Management im Admin Panel** (Vorbereitung für License Server) + - `licenses` Tabelle erweitern (max_devices INTEGER DEFAULT 1) + - UI zum Setzen der Geräte-Limits pro Lizenz + - Tabelle `device_registrations` erstellen + - Geräte-Verwaltung UI (Liste registrierter Geräte pro Lizenz) + - Support-Funktionen (Geräte entfernen, zurücksetzen) + - Dashboard-Widget für Geräte-Auslastung + - Verschiedene Lizenzmodelle (Einzelplatz=1, Team=3, Business=5, Enterprise=unbegrenzt) + +6. **License Server API implementieren** - Flask-Anwendung mit PostgreSQL-Anbindung - `/api/version` - Versionscheck - - `/api/validate` - Lizenzvalidierung + - `/api/validate` - Lizenzvalidierung (mit `is_test` Check) - `/api/heartbeat` - Session-Management - - API-Key Authentifizierung + - `/api/register-device` - Geräte-Registrierung (NEU!) + - Device-Token Generation statt globalem API-Key + - Hardware-ID Validierung + - API-Key Authentifizierung (oder Device-Token) - Rate Limiting - Dockerfile anpassen - requirements.txt erstellen -3. **Testing** +7. **Testing** - API-Endpunkte testen - Integration mit Admin Panel verifizieren - Session-Management prüfen + - Test-Flag Funktionalität verifizieren + - Device-Registrierung testen ## 📋 Offene Aufgaben (Priorität Mittel) @@ -89,8 +150,8 @@ Stand: 09.06.2025 - 14:45 Uhr 2. **Lizenz-Features** - Bulk-Import (CSV/Excel Upload) - Lizenz-Templates - - Automatische Verlängerung - Lizenz-Historie + - Flexible Geräte-Limits pro Lizenz (verschiedene Lizenzmodelle: Einzelplatz, Team, Business, Enterprise) 3. **Benachrichtigungen** - E-Mail bei ablaufenden Lizenzen diff --git a/backups/backup_v2docker_20250609_145347_encrypted.sql.gz.enc b/backups/backup_v2docker_20250609_145347_encrypted.sql.gz.enc new file mode 100644 index 0000000..c3e4eb7 --- /dev/null +++ b/backups/backup_v2docker_20250609_145347_encrypted.sql.gz.enc @@ -0,0 +1 @@ +gAAAAABoRtlbzRM_KAWmOXLbfQTGXD163aDi0Cp0GH9xaKsgnG57O_YQzN_adA2EZCc3IWmRhPENQOHw393AZ_kudvQGzK-N-Xpz1A7-lL8som1E2gAH7FpquiqSEIIb6f5N7QNMcnzqI5Qy5vqahEOVRkfEWj6n3kyeF8pPVoNZXP9LgzotE3G_ROC9t9QojxcApfyUWo0mY87c0iN3FWayK8AveuQm-GR3OUkMS3XvBrZb7INtZ4gk1juwBKBDKc5FKP30TRuhwoDdsXeq6cIK3i9aBLcFvcGQTHAlyMpfIkbJxPaExrppT0TFix0bvY69c7OmTcds2SZkICJIzVo49j75bP8LHvEqBzFE6oGSAtPE-wCgYMaNwdHc_MCvvadGxW9rYTOJIWFXdrWj8gBty4eOY2m3m9MhNC29VDR1KY0fvWBoUpzLKWIXZvm_uyrLY6Le4EHJL3PmkQ_XTSpozuNFrzewphk1qO6rU9x8Q3Y4DANNcg2i8jUI8JhKEOVqv1NuUUdrJSzhgjnzSfm-WyQ_0gQi6cAH4ywpTfJPnZ3avrcpm1bskAI5rk5hhIXeB9U-dJ3wTurM0gvS1TPTpurZiHKBrp3kB_lqVmLDfblfOWYw7zdNtvS-SmbrBQbWZbOOzPodhS34u5nJQOUzn0_8bxBtwnC7L-0v4TqLCA5fBQhmZ6CUokU5aEFlTSYbGkgZCcP-KEjiVAovkWsmaTEViYraPfAzwT8FdXD7Sbxvf-a1DiXTt-tWdjAyhLVYBNZh3wKeAFzNF_6L_d0dtL6FKIOpyy__mbzE0nw0eEF7ZM21tVY-krFu4Tm4u_8SxkHO_RBDtflddha5ZMi7XVP0C5TjWm3qbLI4FOLWbU6ltcKo72_G6ZGdOhrQpzFvQCsplerFCIWjW2WHZFcZaqxPAdAjxh1GF9b3eGWLYlWeu4HGp-7JUdLnzGQ9Qlw3s-4ZPb_jj1tVmwaDVybgGKbqo8XO3DSuvxO1hE3abinH0-9krhTedOH4pvrnPacqRXCuAk7k2VcDS2m-3hlg82jOO9PX4anYvfRjiWg_NqUsPaKjSI8t4SW4rUVK8dw7016L6x389OebHYt43_2B0cgDv48qqA5yfwRVH6C28kKR2DlPkeGGGpQ2EsEY_uV4nw15rWzXOVJjHzHz0kSw64iWpVc0aka1TEy8M5ePSGeBgUXhVoLcpu6NStbts3l1O7CsvlpvYP74ksDmG765KxA5eGaziOhj-4YAlksD0CQ7Vg6XVTeYyZsxpg9d5DOmrunB6P25GIAbEpPkN61aJ6-UHuVisxTfxp-Ac3ADuMyIRkpzlDp3es_ocqXk1Vozo2xcSObV50vANCrT4L5dQLbA_tVqGtrK9l0602sAG1BV0_gFDp6s-KhaIlG7EgX1Ow-xjrDUhVcEbc275kHTy1-YFR4eMqPj1hvpM4_5MdMbjsFvCQBaRNwh12XMQEaaMB6SPcqJcOfri_C778XWrQqjNFWpnY6qaM8bWqN1vc2yyY4bNpCioC-nnQLlB3dmF13LUHT-2mOpGwo4QsyH5ooQ41YGEgJMFchtZMesxYZpE8k16vgBiLJswWxTm5I6f4gznk-ANjxZCu325D8-iKr9wtnwkEeUKAztzYabmEytV817R0nmVOlvDQIVTT5oLTWL3ZiUFGEhYVcxv1xsA2DEBrhiagv6h-akhe6Jo5ThkzXx8dPRfUFRBw77OpLKNo7Q4Ajk23QLi6SH1RMqAE5C7ol3bK4z8PyX7G-Z5nljd7YZxvaIaOUUict7yt3h4hsdQPg1YtAk06BpF0XAigFgccGD6AGc1IFU9_5UpiUE4AQQBiAs98MPePbBuN0-fz8_xwn_UOAsqea1DJj173j5mrvDLZvRDladT_-0ucGqkagzPgONA1JDTTkQ9E9wEfn0wubed2tOk1pa63WUDp8S73rzxqHp3mvWb3izCdGxO_DR__B5PpD6sYDQUB-J0ViSKFAca9ysI9ZqwoiXwwClblPDN1hBmPLsXJrt9ouQaq_ibpbOSxrVh8WfXL5pK28We1vASFap1PeOiFeKKsSzfURkzDs7lQExfuVKqEMJsYLPORE2-96NQ2_pXj0aefM7voUxsFxOQu1z2dKmE63Pir3QqCX7hq4TVyan_R1URcCQqS5yG9W-PMHMaFmur7IMxwub9fNodX4MlxXwlGdO9C9zgmk16iqphnGVKrVnlvRDKug_lcbCjwJSg_HyMavOMXZjJ1URtfjdfgub2wBAUuSA-wMoQJa1elMywY4UINiXR7fJGW8BL8TUOrPgMztQuTrxL2BTSRMOeZvC06cRYUUuEYDbv5JBSI_KvHd1AvZVFMSy-hfx41t4KwLicTJN5IOLkh8oD6Rhp79oms0vmh7CX5rDVgiyQZJ4rNhy2_v-D4Lh2NPFD3Qci4x6lQaM33KU4avlok3ZZiddGTbNyNZT2J1OnBsjItQGuLoehLnPNS39VX4xV3a5obhZ4_zc0GxFzNR85UqyIQHeIVkPLN68TwIg3FP63lQjA56dcoNT01n9j61XVfmRPogF9vQMauT10fhOf-LzlHGRp_rMuYn_WmC_o1PATDq0eKKnVrU3tiOMSSMKJtN5k2l03u208XXGl6Hq8rcotghf2u-YGKZrG4BuXhMwgZicpiOhk6OleMxarjvb2A3wkdKuP6iP2XMwuOW3K3TnGim6-4p_vvz83bvnPe-inwLyVkGHqLQVnTLG95_Y1arLQGPBtIYpKlIvfP2VsdUlhcCSXKKZQ4vsBFwMGbOVYAwFAXc174Mq99hBk-1j0Y1KzMO-2Pzb1jGiuX6ZQfNnE1o5czRLVhMxJ_cD8otcYBvGA0rMJmFoSP8VUFaTtdWRyZQYPPobB3Hi_6VTF6uT4QK-LTHQUhdo62aVKzzomqebOgESGWOSIQ4Ja-utenEpZZJ8Zc7XA5Os95D5K0ssS1Z7h_l-3njq70Ly9VxCv9hQV1TX8zIAHKOA9BWf469MC7SSNbHMqxGGd99DVDFwd4KgrxzuJtmvrf5EMIbfs68muR7mOA6GMV7XGiZ1a9cYlI8D0iXwBAld62Qn0iSnbI3FjrPHgUF1sRS28nXJdIb7SHZ0yg0VaAjogD2BimcWq9I93LiSvbFELQcrbouwJCbqDtY6fT_KJQGOCvlwWI7LUtgsI_HRweW-ix14UFKmt3EMtKRfvVUiegPqhVU2kxy_2M6CYbllXQX2riR4-UUm2gIAxcrdkN32N2MqiHNoNbX-UnowfbSXbLgu0cQhJf8_of76JSQ9B9jRpeE6Aenh4WQYFFvdE2_cBgtuc0pfeG86iAmWFxtwXcCQ0EKHaSe5MYle7_LlwArte3XalxGdPAov7i1T35ChP3QKCbztQ2Olb4F1n2maI4D1YgJOfmsm75z8_jq-L08aZ77Ir815WG37jPPsJfgDrMBerqRefr_rQt9zBErGdMsNiU0cnPB6LBj2BtdJcIbhUIwPs592MKiVswqIq4Mmv8lbXd2k4wuGAPF_BqSZgWG3Rp5yW18J1JNFyXdPBPhD5yF__aI1vCjsvnbgfDMjeuFllVESj2OWMgtpGpsF6CKgN6z95UMolA-2sYmpGrgLuqZy2zMaK86GQMAIqdmpEWMF4ot0jKYYmR0UaWKaZSMEkOtM30WNi3onDwTl4Kc-Urn3VgwRH2Cy3fWlgw3zNy8guZ_egyGLiC5RuyKjKifSxgEwsxYL7-3Z8ol6w51BA8AGNU7vCiLV0tf4MDmkrqdAHutddVvp6EEUlqoSM2iHqLO9ikZ8QRpfvYyf0s87SN9z9betRjSUkLCWk0A1Fnu8mxGePghiVByrWl3L4-L4AiQFtXQugg5MSazYp3q5_3uAazAG5vjBXnuOkG_4g9rBjwVFF4NaVJZtGlmV0hqnd4dZDDNZASJOUu2F9vJWfMwBiI18VJOtIQ0k2K8ozi8SDydg_Yqv6zpjPpaa2KzMnWkT0u7Q04nML7yGhJDOAxCXr4iIrFZlu7MKRrowLn18Exr_viXUXTuvMCFK9dyiR7DmUypi2RoLHYMqcQt9RoJCCVkhuoVYp3h9axJvwqNpHbJGb9L8vA02O-aPhfzx7ETIbIuZuboak9D8DC2EbphLoeGO7OoxOzLKAEhyFXfQS236uoW33AGihEWRGMAg3657Fg0sFyYd4NzbH9GmQNlHj2LjWeBqSBgZdCnsLaQPxZgVe3ta86ESlFo9g8JI5m0tABPJ1IkY67n6CNEDfXu1IP8N9UL7f-TY_HGbl9_MlfjdY_kMzt2qnt985CsBWXCpXkS9mAn0DdpXxG3R6yrIF2i2qzUpJlvjb0HdV1eN-KUyD4xBfpcdO-RTkg_XQvIihUITnuAm40q6-vuFb7MqwYlg9OVgGlsrhRTge3cUnxzMkDL-kjbahAqLwHTIpFxT23kMRsJHniJMXW8nELGHXEJ4rLiUGe2Vf2-mlR0W4AzwkZftdIO_opADyKVC0Pl5ZLWZGfuRlFt2SgTYaysQFqDdqq3tz6sHYTcvEyNlDI0KAULikYxH_wNI1-TcjopMblHxlzCAtHag6odm_Q8imfqN-gTNwGCtdD4VX_sjuItq_snY71H4pwK8mfnZBh_LyJzuYoXDMMr4A-zKJsTCIJ2AkOv6X2NtIL1sYxxEApmo7gjIgQ5PdYTGIC6wYbk97bJb5ndiYqGgEFwBEp6GxbhyTWfBDgJRcNZLGmeG61p-xwvlnNASOhPzh_UulSuPFUJnG9xYPxds3uBX_6hvRksWaalgFEDeJO8Iwc4C5gbOL3l5A_NZG4T-mjK-W4q3c40oesGp0xnLc_KuluR16lyvHbc9n6DIKYwVimgsj1IXE4g76VSBJELqpN_nsNJ9rY3njS7-oLjsZ4eavfdk2HZNG0fMMwQ2F7u2Edul1DltygMadNE438Tef1nkRtzNjxFlxYbsBWd8UqKYsgM7O5OffWJU3jVuYIYc1Y1pD5HUK2gFhbuYPO70Ysg7hXAg600_N9ATWD4TvKs5m_4ziDrlTg-DdZsrsE9e9ioriCpwAb7xzn8PfGt0SU3EPpjqjykK-flqwI-FkThbUBKJGe1hurLm_G-bRFIeDxw5rOJXT49nR_IPmPBfW9gB4S_p5--NqIQQTFKPAVE09wZmrbi908N3vurtHjsddIryz3_OLU6KBZaKstVDFdCd3vmmfvXjLy_z1pOps3UrY6u5yFUg2GYrPh-IorJyZ8Ubo_1WlgzW-X_3amURfBNq0Lf1RHELCbTBcyTUgRQEQQsNveHrwRvYZMmQhrEEtpUVK_4BbFca0U4owrtvcGHbox6LTqWITc_bzLnkdLOlvYfnmUURrNFxHO73P6corNymxXrruXyEm35wZjiRmXeaHqZbWVCX4fcOD11Df6hYG5fxZdHKqnhMXK1Gmrs5JUMn5Om_2w7TEdeE_iBo7h9cdOqHemZPQaBYqxaweT80zUpKAMBK4W0uTgNNpZEU_myucHgeGVSaQeN47Edz4aFO0yRpiPLbUydnod2M3xqkJ0JpEz19lnruJr7kqfizA1Y0CVXwotyjenv0jj-N-b5ucFYdR1q0Z3wJTFf_RK2XqOtpryOq-WIXbPvQ2hb_lKyYWBBiPiVqq-T7etNCV_4RiwDJhRAomBFf19nwvgNa_6rLDhvlP6Omu7SEhSLTNhShM8i-H1U-ez-JXB5BCRDDBZjGCtPDB8vC5JJ-NFW2l-ca5ABnCnXjYfin4dxS9NMLUh53zuCCL26EsQQBsCKbM3KcIqov6PZ3pDFlvwUCBiN3Jiihr1fQrAtiNBmoA9l4p4cdLtowIYHLnc103h9iyUov88ngTQnpi6uGT3GzwYzM2liTKd0c6ZxjsSENlnLmav-ESixo1jDYmeYIFofAEhIjJSu1kIla44pbTVBLrj5U3DPe6JuOHtAXOOtrzP1e1fMUYweAFrqY9Sb7vVcTg1kDLdVgXp346mJaZQ_5uF6-ZvxXJ4yP3Abi9VK8sEA2igyxPOLE6MSqFngIijkWB6ez4_8YXlsFphIY6ELw1_13Tzcay1YV5JzpHl0PdNuZ9b9efvlSa4VAuGKAxUtyAxfXrNr2RuGyhP2dmpQoph16b114kAmroe8A1MX6sedNQKa9BCfcjSqkKY44h8gCBOxm1hABsPNUlsV2oVtN_yZPE3HHG6zV28nLE8RfXF838oMc2UFjkdY_TMXM1sG-I9Fll4U2Y2YPEJaOwuE3VtI2lLEwyA6CDnnyKdzucBYjZ_Y4fOpjDzoHYrNTxtokBQ1bUAQ5YscRsdfDn3pfKJ7V5wmOfisVhzCg7mfAUqEUdi4MEx2zHRNqx2XDpCqdTGhoLcgJdRu-gvN410KQnEetywFWG16rSP2UMQfRfDJGbpDVxItmZTmjowuHC2oAyH6iIlG7xubydi0VCCdgaUHRDVw6vMLf0ezOuY0XkFoVWMuwA1fEsYHf-kkEIGOjmC2TIXdFNlr4ViDKvtLkae5X1JXLKnBSj4SkpcIbT0X2BXUuathaq_05--QfzNjrsHCPPDgVviS-v7u2GRx2_Bv4Ce9qB1-qV4xtLA0L7Ls-x3wv3q9jsAGhzMHLqWtUkkPOLf28Hs5N-WsBEX2YmpdIPeHlg3zhAdOIuP4tB8AXgGsJcKnWPyZG4rKyyCtzFQKdOIAKCHGwqG6WtAdV254K5HiuIp-W1krHDxRmsfBTiEXjlleVuEo-EgYMlLskvvzBMaTFf0xULx4SppqFPbZW7RJDS2CxfCDDq6BK5xRo5T7gdlBoIT05oKoEBq2omyzbHuo2PhuS_aHcF61a-dgj7OQ4ImD-yl4WwIg-N7owPTFZ1DXanlXljJWVoRD1wqRbOTug1OvatfdOSSshf2IRGgYto4I2wVHMdzVsmrrYH477QHXPL8noEvhe3Vy1LHRL1TedAoiezYnWVBYxVK0gRL6ydNc2p8ltMIxXnHb87tYDmyHtJSpzrW5gUwU9CJ6DRDGylW0CHHyLIPRS7AHb0sMNF1iVfTdYgdX_hgBhZlwpUOO4vVzSY89yC3eR1AWzc0shVfquj5a5Q5jAdl7paNhH59rwN_eo-fcjvTG3Iw3F1zVdYsnVP0b2nGNMeaIyeQCd-lPAH2StH8Opcuvvs5Z57qAvDwHgJbbJyJalWbIVNcdIZbVHGlNIMsmeYBefNjuaF6q8wvV6QKela0sJJ4qUHqmYJPPSdWxVB3Y9hQJQLjcnYPqAGBMhdfAQRL9m-j8dKXp9btjNKU1QDBSiwRJa_QZsSL3bR_pPOH5EQ23WWvjMnwe0-9X5N2kuNxw_tcm3O-G3lJVwMt6Z4IQDOkz66R3AYqiSy_tR3RNGJPeXUZrRT3Hv-t4YjXAPor5l--_--C0rU7xNJHs9lPLy0lhlvjd0lB2762yucoAKWQxwm8xn-Q8GQvwir-KtNzENoHKKdNP27nQlShREewtPdMCtOybF7bJFS34Cq9h49Us2eIB3QA9yLIJ9TtNDxVD9b9UhJcJkQ9VrS8aAKHq3j7_Glq-5kltBEBIOagsY8f8eNwJKlCYVCYUSmbtw3QdlsUrG5Yu-S2_FFBO8ZNAqTww0x1R1ywe6RDJXRQ-AyD7I5T8UpTcSFYwYo1damXdUJhQnKzM69iVBMLsNTEiwnn1jiYcNSzMSsCylh9M9EeTLSkBc5hH8EsqYkbEM_lTNiBxO2sfTTYveW_gdnUd4v822nS1E5yIRk2e3G4RTNXnpbS8ys7SKfVRf2esAqLvKE9660hBH1F7X8l-T62JDCA_id3umudDGgevYxFEDFddrvgk0LMel_seFQ6k7f2zjgGgkQl-DSQd2To3BZFKZi_B8hPk16XtWrIANOazwOmyTSsY1CptJq2s3nQg_f6b9-TLDFCZJFdnmURQirSyrX67J-Z2BL6amI4gJiUV6plIVI9OCW1_gutzQ0YizwXLcnVdf-wdt2_yA8Js34Rq_5EJ_pUKgufU2q8ZiQetthDgrB25rk95wtBWemW9-cFAEqQXaGXgmYvD95OdNUVp8_oIvbzX0T0RP2ryi8LFJmSrtv0X8AuNPp946A6g2s-kff4VfFlBZU0NpH4bS6wHHdZzZVp6b4WdcnYLjvUfFQN4zpjKwfoB6YZvCKDuK4mWFBwx83ghHHKoNOeGqsbx4wt77V_88MR8-Q__fhwERZngkXSOaebfyFgtJ6dY8KpuOVoA1M4kFw4qbeU6nm2WSm_nxDe8ZjQ4IdRNWRerrgUAlzXcwiX3F_O2HrfQ9T7iG8xa8BjVuLwOtbW3GY7e7dfyEi5vBM2u4uYkHXmuqI7hMyr84r9ltdZxgngQWVKiHiccN8IOt9rn88nrqBlPckOxu9EhinTs_oJqxXht39C8v9j8qzOzuuO07Gfadm3HF7VIAFRCNED9HPbzrd32TJbwkLH3u0utCzqm29KPCZemT_YDKhbD_nEtAp2C53kzlqBCiJ5j9STKWxTTne3O3ZUtNFOElkywLAu2B3_7CO73r9yKIMVymWiy8Qbwnh8R67qRMH7begZ1OfAjo1brKDPaJe0ITYLS9EXCkxcwSvJJtZbB0yR5a2Z9GlDCqKgxOnGrcZRCYps8oS2SUkmztYvuytfIHJkAUPJOvK2_JrpAMBGt2avM6LcIZj2CtiH0XG3DtIOCZ9vioXYydbwUzKAhLD2eG8Kxz-kgba21CWX0uA52l1LRBqCyP_R3dywvYC-6ApY0di-FUYoYXZn8LyYUpAVHBUbdfTwO2Ix0xPk_RyWO3gs2qZ2w-AGmyVddhOaDogu24Cr0loI-KpFQR5PTHLE6jSNhgepnLectEM3l-IHP3nKgFo3RhYJ6nqEu96juAE3JuQ4XBwURTADCfIWYHytn96yIHfV3SZSbJu8Z6hMTgr5iM7lLTBxAwQPsWmA5QE1Q7flWzyzFginJBuJu_XPiGtsYbWXwjh1ga7xiB3R4xxRkREUrv9jXpkrYzIOCs7vlNLDkyMPwAc37o3ehZa2JIi0vno0G_XZ87mcfDusAtfe0UVa8J9cFbfdezfuaBq9LbbOG9Ffcq2gi7QgBzhlj_rnX39YGi4jug7b33BWUeB0yWA0K5_sURbOoGA00m89ptiQc4ZF5O3o8QekP0EZ48q0ijxAmkW48Yon4VG8J8w-nDpgvCP_9ZZhhJ9GAiqIwBa1cvRYL9GcZUKuPywPnw6u9tqRj2Xav10pe4hEzga3OLVUPYEW4gOpKwn2cgGRif1M2bBGypUTU80AcFWDIzktiWB5gDmuJ_ufP-cdXpwLtrP7-Utqz8RYkd1oLVcI9dfWosEjOWzpuTQq8-zZmAKXkPEhuwT5Ck-7mWh0wkLl14cAh-N_w3DkEm1RIbFOKObESdS38iinnesnrTo3iyE-sj_jPJlFOO6CQkcOsXMm6hEzAF2YF39zIWcnATZJx1NflwfgnrOKTZtKFnP3sp6DZCe2L-HRxu3-4sBHiySG3cdIk-uwoq6Jsi074B6QAej8ZTucUGAmhcT_nqELuY94Y80Iikjmn4TxPcA62w6Cl7p9R9VWCOJiCk80KsSkVleGrpgnNLkDQAnsOma8ghPpjaqRk9zMr9EP_pGP9gASkoW5WV42o2wxUXsbEg_FeXafLtiTAUEiNTrTrWceMYcjsreSnoZCm9K6pJhNFTBmuChS1KL6brS6wXzXoSEeP_d665drx0H7DSjNW-ZK0q09o23oSOU_82HXWIPccNbGkT-dkHStUX2vwbOO1ef97_ZYHsgCnRVUubY0v0dOlHJ_aKXPAOWhMFz94vztEHcD_nMwPz_uvHQU0inJATvPtAFiTMNFDCrCWL1pQE9IjHi4jCcNHvIbb2d2ig4xnEsu3i__8h_QtyzAvnxMXk_9b2t11gLK3_yGbbFy3EVlCBkqHho-neJySmsxB1qF2Eb1Gg-NLG4ktPEr4YmWFwnxQh-QqUQeAdsdgGldC82CeWu7Yor2iNuYEJ8tkGROhm0sUf1ctkwLH_-8zUuXW5OEPNAJyFc9aIq6h5UueVnE6lglIK6nS9ysK3VDaVUlk5aFssVkb2mZiMZ42oyhpsNL6kqrIPbANc9fmjMGTSFSlKPmAiyGPgSFzbVuGkDzCuhQZAx4FX6WNDImZsZcdIQrjDdlmAH4QEBYq3EuwW0yiqcfQ2VjbkmneDY_hsXj8faT30NxeXKsQPg73QI-_PFl0qJnxa2WuY-9QkD4f924MUfJ4HvAIwNkQ1hW5fJRCbGoZ4HJPcrK6niKl5YOCbVNUztSdaef-RqYKu90hX_0p2xKkAbYIKnTGGM1GoneDqTj076wCz-nCZruxDR-tawaJzMPbwL7iCCriOTMwz3i2Zwm5WHBu9RZtrQCSxH07XKvHgWLN5p0a14NX3JOpmbzm4uyxauNl6ppWNLEhLfa4b90OYncumCCtDe4pCk59_cRaLNFeZ_lUebq_wHMxwhaWWcQphUP7YyjKviJO5W7zbeo0gk-3hdX6j0iayzY-jzb8I11DIhw0lkIJ5bDXeLv3sckpfkEOzYGjuvb7MoJuFoTJNzT_ZtCHFbAgVsrWDWRZmcIb5yI71o0FgberxH1-jemDLJjWo09gWMQ8AAIC3XO-_kW_QHqNtjBlhB_lHgYzX03cMmKJ7TZMtdcqpnEqv39_IcTmGwB_S-XSDHcqVR0XFFs2LaAGdw4u8eMKw91s-CfZXocSTgQ81d7m1XZPIuLxiU71Ag_jAeBGyZP3DRfqoyV3JUI5-yr-aJo9SMw5mstwdVvT_Mer9HRPaFVq49GWYxJhtH2jLnXJmwFv6nxu0blHx43DjaD8MTo5E_7qCuRO2NzGsZKkSe8LfauxIAR4srhagdj-5T-Mz415NSUYHRq_S59Ba4AtNFJ9KnDSDZBTN8sHFSizeRO8i1KFfHriY94WPFUblCurHP80d1C-p-m0c9gyxCWz4MDqiIfegIZLSnQ81gmP9jXQuTRrw5BR8YVwfQFXMsrq443EGD8dbMbo9zju2BRGqEDFlsMjcqIHtHyX_RBovCqobhgTtfNVQs0igr9rzuhuQy6wkKKCw7JxBG3n4igz8tlarY9z4cnORejaC1GGVim11CPKK6Y6aCdARS36e6eMjxLK8hhOKzM1RlmAEf2aZI2vvkpHYZGCrcrm6NMgD701Wj5du5F-Ct7tTofVvQKGAIe2leOrWkcFothK9ZIEZjCLY65zU1mZENRW4TOK6xYHEzHMp02ra9mpx-mVOgAUmPrY8Bk3gsF1_VzzuMfpbPEnBYCQHjVLLz1soMiCKaP0ZKFvAgOsnAUWFVNjMgdpQKMYfjjuAGV5BjlzbwLa7-L4tqsEmgSjIPA6GYNjwE0vJJAFIrd02Ti8x9gbx0ZdLDh6rHzCJWNmdjIFht7sqXSKlnPE41_OiCRPpsjqlxUyPwmsuY8svZOQSwc9KS5iKsqoM1hvPyYVViO1ZS93GytnOfDUxLXlE7Uk426wvWZ7ToJ2mFjnEguBXjeV8rQ__NuGWgjiSXUeHhxrP35GXmnG04fN6zOv4MPzo381YjhbBvJclTkfdgqOIqxMY827tdgxUeQgfRY8HGro-qyr-wmhzxyzDUR4J9yT9MxfnDmEsqfEoh7_ImYd3d7YuMngL0Ps0wU5xuYszCWjOO4xn5VAvIDfB6qOix3K-C4064DlcchUhS9KzOv_BJo9FI2ce-Y-vjTm073hufv0RSpCFbYig5axIsWB02jh4iQ6vFQaa8hWtUP8ZMJuUyLXgkdlkPewTSbAJwdiAESxXzq8aQls9C2ZZ3d7aI3xD26SNke33nEgpNkVVE788dLAc-jQMP3iy0kRin7yvXcrBp-7UfMO8fCfcxWS-mulXmfuH59EIL0rm398qMPI-raxE_vaz48V5UGEA4IM44WZl0iku_Jr_aCiTc6SQ3dll5DiDnesbUbLx8Fa-lIYbbEDRQeeVj-t1UpHaGKzQWDFGgVmrXMWkTg-x_AatqM0MqRWkyY1rKiDxNt9Wo5IH2O7gLcNdJcizBfNHIi_p2p0WshFftjGq8xQatWmoqUZbry5Le-DmyFYCZKTkKV_oAsjZUToZ9IuSNSzH_TqBuihAoO1Mau_U9qsTwai15Sjqs6k4Icw0yCJ1YjAvyphE9t90Ff16RYWjhDmcbUB_5tiMue2S0F6rcejc2shlhCEPYJL_FmNg_7oBlHYYyvyH1sZVqnE2ZmlvNfZjeRCNfLzd9hWDWlLfR27J1l6SPRH-2FJygs14UHyY92xCXsjSDZduipqGfHVh22vbPjEiKYrmXp0tbCG_3x9W1k7iB7TvAGROW2fc7r4Jgk_7XayCjowUXwnMSRKm3iLQ0i7Dl4m1wKmX2lYx7tnw9BlUOQg_KJwGw8S_p060vuSS2WJFMPto9wtaOP1UYAJXwrrvLInyhAVS8q6_vLlQRILiHbN4eE60OkaVPKrPn7E8PiCM-UlHsDjjE70ptdh5c7jwhKlqZerxtYm_9Fw8jS5skf5AQxG6qYsqZAb6_Uwp8LWN6XYovr3uQHj7y6Fefl7IMgruK1pNSVTchNrVbtk8YubCkC1XbS5FLiOTPMQLI2XwNwi1Qj_I4F6JqgL_Z1LU0v7wKV1Rn70rZ8-ZBRG0rwVOMr8KZrtcNim7mpKJBcAMGuUWl1-coh1uYmWT6BLWewZ8OjDwA5ld8u6OhLpkxKumcdTRc6RfIBCx_qvds27d3kEVDG0zT28dV3mtZdk8rRMY5b2n5GBFCbHD32stDE9pos-YvpoaYu9WF0T5Yh5eOZbNhp1xJe4Do5GVRt_15gFEK5EFyZMrkNW8QLYVSlQ4TWNAIveWO4F_65cRJFyoGZAc7f7KY0tQ4sty9nrs863HcZP7rubEdwemTIK7nDsvHGXb6zOaiR68T5CZMYYdedSPRxfgzafw9M1wsVZMxaxWoBxXVdip5FknnLvhwTuA_-rR46ZbIEjGRt-5F4WMrPl2YMAD6ZLlhPx5qeS0_IwqhGFdET9detHI6sQJDFRGiPB4LeQMinGW7LvJxRlHxYEDaDa21Aq3UR12oHT0UK8KIR5ortDr-S0GU16NNFLzL2BQc3sPc3OR5kFRGJ5Tdi0ggA_N7nvr8zVQrOs7aHaCOyOgW3YnkbuMODm4nFC4i9JakUWKLV9eAB0Ilw== \ No newline at end of file diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index ca23bb2..87ddf1f 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -1174,18 +1174,18 @@ def dashboard(): cur = conn.cursor() # Statistiken abrufen - # Gesamtanzahl Kunden - cur.execute("SELECT COUNT(*) FROM customers") + # Gesamtanzahl Kunden (ohne Testdaten) + cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE") total_customers = cur.fetchone()[0] - # Gesamtanzahl Lizenzen - cur.execute("SELECT COUNT(*) FROM licenses") + # 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) + # 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 + WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE """) active_licenses = cur.fetchone()[0] @@ -1193,38 +1193,52 @@ def dashboard(): cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE") active_sessions_count = cur.fetchone()[0] - # Abgelaufene Lizenzen + # Abgelaufene Lizenzen (ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses - WHERE valid_until < CURRENT_DATE + WHERE valid_until < CURRENT_DATE AND is_test = FALSE """) expired_licenses = cur.fetchone()[0] - # Deaktivierte Lizenzen + # Deaktivierte Lizenzen (ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses - WHERE is_active = FALSE + WHERE is_active = FALSE AND is_test = FALSE """) inactive_licenses = cur.fetchone()[0] - # Lizenzen die in den nächsten 30 Tagen ablaufen + # 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 + # 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()) - # Letzte 5 erstellten Lizenzen + # 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 @@ -1235,12 +1249,13 @@ def dashboard(): 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) + # 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 @@ -1249,6 +1264,7 @@ def dashboard(): 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 """) @@ -1358,6 +1374,9 @@ def dashboard(): '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, @@ -1385,6 +1404,7 @@ def create_license(): 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)) @@ -1438,35 +1458,39 @@ def create_license(): flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error') return redirect(url_for('create_license')) - # Kunde einfügen + # Kunde einfügen (erbt Test-Status von Lizenz) cur.execute(""" - INSERT INTO customers (name, email, created_at) - VALUES (%s, %s, NOW()) + INSERT INTO customers (name, email, is_test, created_at) + VALUES (%s, %s, %s, NOW()) RETURNING id - """, (name, email)) + """, (name, email, is_test)) customer_id = cur.fetchone()[0] - customer_info = {'name': name, 'email': email} + 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}) + new_values={'name': name, 'email': email, 'is_test': is_test}) else: # Bestehender Kunde - hole Infos für Audit-Log - cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) + 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) - VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s) + domain_count, ipv4_count, phone_count, is_test) + VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s) RETURNING id """, (license_key, customer_id, license_type, valid_from, valid_until, - domain_count, ipv4_count, phone_count)) + domain_count, ipv4_count, phone_count, is_test)) license_id = cur.fetchone()[0] # Ressourcen zuweisen @@ -1474,10 +1498,10 @@ def create_license(): # Prüfe Verfügbarkeit cur.execute(""" SELECT - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available') as domains, - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available') as ipv4s, - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available') as phones - """) + (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: @@ -1491,9 +1515,9 @@ def create_license(): if domain_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'domain' AND status = 'available' + WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (domain_count,)) + """, (is_test, domain_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1516,9 +1540,9 @@ def create_license(): if ipv4_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'ipv4' AND status = 'available' + WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (ipv4_count,)) + """, (is_test, ipv4_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1541,9 +1565,9 @@ def create_license(): if phone_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'phone' AND status = 'available' + WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (phone_count,)) + """, (is_test, phone_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1577,7 +1601,8 @@ def create_license(): 'customer_email': customer_info['email'], 'license_type': license_type, 'valid_from': valid_from, - 'valid_until': valid_until + 'valid_until': valid_until, + 'is_test': is_test }) flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success') @@ -1604,6 +1629,7 @@ def batch_licenses(): 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)) @@ -1657,26 +1683,30 @@ def batch_licenses(): flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error') return redirect(url_for('batch_licenses')) - # Kunde einfügen + # Kunde einfügen (erbt Test-Status von Lizenz) cur.execute(""" - INSERT INTO customers (name, email, created_at) - VALUES (%s, %s, NOW()) + INSERT INTO customers (name, email, is_test, created_at) + VALUES (%s, %s, %s, NOW()) RETURNING id - """, (name, email)) + """, (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}) + new_values={'name': name, 'email': email, 'is_test': is_test}) else: # Bestehender Kunde - hole Infos - cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) + 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 @@ -1685,10 +1715,10 @@ def batch_licenses(): cur.execute(""" SELECT - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available') as domains, - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available') as ipv4s, - (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available') as phones - """) + (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: @@ -1715,12 +1745,12 @@ def batch_licenses(): # Lizenz einfügen cur.execute(""" - INSERT INTO licenses (license_key, customer_id, license_type, + INSERT INTO licenses (license_key, customer_id, license_type, is_test, valid_from, valid_until, is_active, domain_count, ipv4_count, phone_count) - VALUES (%s, %s, %s, %s, %s, true, %s, %s, %s) + VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s) RETURNING id - """, (license_key, customer_id, license_type, valid_from, valid_until, + """, (license_key, customer_id, license_type, is_test, valid_from, valid_until, domain_count, ipv4_count, phone_count)) license_id = cur.fetchone()[0] @@ -1729,9 +1759,9 @@ def batch_licenses(): if domain_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'domain' AND status = 'available' + WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (domain_count,)) + """, (is_test, domain_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1754,9 +1784,9 @@ def batch_licenses(): if ipv4_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'ipv4' AND status = 'available' + WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (ipv4_count,)) + """, (is_test, ipv4_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1779,9 +1809,9 @@ def batch_licenses(): if phone_count > 0: cur.execute(""" SELECT id FROM resource_pools - WHERE resource_type = 'phone' AND status = 'available' + WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE - """, (phone_count,)) + """, (is_test, phone_count)) for (resource_id,) in cur.fetchall(): cur.execute(""" UPDATE resource_pools @@ -1921,7 +1951,7 @@ def licenses(): # SQL Query mit optionaler Suche und Filtern query = """ SELECT l.id, l.license_key, c.name, c.email, l.license_type, - l.valid_from, l.valid_until, l.is_active, + 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' @@ -1947,8 +1977,13 @@ def licenses(): # Typ-Filter if filter_type: - query += " AND l.license_type = %s" - params.append(filter_type) + if filter_type == 'test_data': + query += " AND l.is_test = TRUE" + elif filter_type == 'live_data': + query += " AND l.is_test = FALSE" + else: + query += " AND l.license_type = %s AND l.is_test = FALSE" + params.append(filter_type) # Status-Filter if filter_status == 'active': @@ -2013,7 +2048,7 @@ def edit_license(license_id): if request.method == "POST": # Alte Werte für Audit-Log abrufen cur.execute(""" - SELECT license_key, license_type, valid_from, valid_until, is_active + SELECT license_key, license_type, valid_from, valid_until, is_active, is_test FROM licenses WHERE id = %s """, (license_id,)) old_license = cur.fetchone() @@ -2024,13 +2059,14 @@ def edit_license(license_id): 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" cur.execute(""" UPDATE licenses SET license_key = %s, license_type = %s, valid_from = %s, - valid_until = %s, is_active = %s + valid_until = %s, is_active = %s, is_test = %s WHERE id = %s - """, (license_key, license_type, valid_from, valid_until, is_active, license_id)) + """, (license_key, license_type, valid_from, valid_until, is_active, is_test, license_id)) conn.commit() @@ -2041,14 +2077,16 @@ def edit_license(license_id): 'license_type': old_license[1], 'valid_from': str(old_license[2]), 'valid_until': str(old_license[3]), - 'is_active': old_license[4] + 'is_active': old_license[4], + 'is_test': old_license[5] }, new_values={ 'license_key': license_key, 'license_type': license_type, 'valid_from': valid_from, 'valid_until': valid_until, - 'is_active': is_active + 'is_active': is_active, + 'is_test': is_test }) cur.close() @@ -2059,7 +2097,7 @@ def edit_license(license_id): # 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.valid_from, l.valid_until, l.is_active, c.id, l.is_test FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.id = %s @@ -2140,7 +2178,7 @@ def customers(): # SQL Query mit optionaler Suche base_query = """ - SELECT c.id, c.name, c.email, c.created_at, + SELECT c.id, c.name, c.email, c.created_at, c.is_test, COUNT(l.id) as license_count, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses FROM customers c @@ -2173,7 +2211,7 @@ def customers(): # Pagination offset = (page - 1) * per_page query = base_query + f""" - GROUP BY c.id, c.name, c.email, c.created_at + GROUP BY c.id, c.name, c.email, c.created_at, c.is_test ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s """ @@ -2206,18 +2244,19 @@ def edit_customer(customer_id): if request.method == "POST": # Alte Werte für Audit-Log abrufen - cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) + 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 + SET name = %s, email = %s, is_test = %s WHERE id = %s - """, (name, email, customer_id)) + """, (name, email, is_test, customer_id)) conn.commit() @@ -2225,11 +2264,13 @@ def edit_customer(customer_id): log_audit('UPDATE', 'customer', customer_id, old_values={ 'name': old_customer[0], - 'email': old_customer[1] + 'email': old_customer[1], + 'is_test': old_customer[2] }, new_values={ 'name': name, - 'email': email + 'email': email, + 'is_test': is_test }) cur.close() @@ -2239,12 +2280,15 @@ def edit_customer(customer_id): # Get customer data with licenses cur.execute(""" - SELECT id, name, email, created_at - FROM customers - WHERE id = %s + SELECT id, name, email, 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(""" @@ -2409,10 +2453,12 @@ def export_licenses(): conn = get_connection() cur = conn.cursor() - # Alle Lizenzen mit Kundeninformationen abrufen - cur.execute(""" + # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht) + include_test = request.args.get('include_test', 'false').lower() == 'true' + + 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.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' @@ -2421,12 +2467,18 @@ def export_licenses(): END as status FROM licenses l JOIN customers c ON l.customer_id = c.id - ORDER BY l.id - """) + """ + + if not include_test: + query += " WHERE l.is_test = FALSE" + + query += " ORDER BY l.id" + + cur.execute(query) # Spaltennamen columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', - 'Gültig von', 'Gültig bis', 'Aktiv', 'Status'] + 'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status'] # Daten in DataFrame data = cur.fetchall() @@ -2439,6 +2491,7 @@ def export_licenses(): # 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() @@ -2498,12 +2551,12 @@ def export_customers(): conn = get_connection() cur = conn.cursor() - # Alle Kunden mit Lizenzstatistiken + # Alle Kunden mit Lizenzstatistiken (ohne Testdaten) cur.execute(""" 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 + 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 GROUP BY c.id, c.name, c.email, c.created_at @@ -2900,11 +2953,11 @@ def bulk_activate_licenses(): conn = get_connection() cur = conn.cursor() - # Update all selected licenses + # Update all selected licenses (nur Live-Daten) cur.execute(""" UPDATE licenses SET is_active = TRUE - WHERE id = ANY(%s) + WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount @@ -2936,11 +2989,11 @@ def bulk_deactivate_licenses(): conn = get_connection() cur = conn.cursor() - # Update all selected licenses + # Update all selected licenses (nur Live-Daten) cur.execute(""" UPDATE licenses SET is_active = FALSE - WHERE id = ANY(%s) + WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount @@ -2972,18 +3025,18 @@ def bulk_delete_licenses(): conn = get_connection() cur = conn.cursor() - # Get license info for audit log + # Get license info for audit log (nur Live-Daten) cur.execute(""" SELECT license_key FROM licenses - WHERE id = ANY(%s) + WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) license_keys = [row[0] for row in cur.fetchall()] - # Delete all selected licenses + # Delete all selected licenses (nur Live-Daten) cur.execute(""" DELETE FROM licenses - WHERE id = ANY(%s) + WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index 9fe7f20..6104129 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -8,6 +8,7 @@ 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) ); @@ -20,6 +21,7 @@ CREATE TABLE IF NOT EXISTS licenses ( 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 ); @@ -118,6 +120,7 @@ CREATE TABLE IF NOT EXISTS resource_pools ( 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) ); @@ -200,3 +203,48 @@ CREATE TABLE IF NOT EXISTS users ( -- 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/templates/batch_form.html b/v2_adminpanel/templates/batch_form.html index 3c06e71..b03fe3d 100644 --- a/v2_adminpanel/templates/batch_form.html +++ b/v2_adminpanel/templates/batch_form.html @@ -152,6 +152,15 @@ + +
+ + +
+
diff --git a/v2_adminpanel/templates/dashboard.html b/v2_adminpanel/templates/dashboard.html index d095ec6..ebc1afa 100644 --- a/v2_adminpanel/templates/dashboard.html +++ b/v2_adminpanel/templates/dashboard.html @@ -127,6 +127,16 @@

Testversionen

+ {% if stats.test_data_count > 0 or stats.test_customers_count > 0 or stats.test_resources_count > 0 %} +
+ + Testdaten: + {{ stats.test_data_count }} Lizenzen, + {{ stats.test_customers_count }} Kunden, + {{ stats.test_resources_count }} Ressourcen + +
+ {% endif %} diff --git a/v2_adminpanel/templates/edit_customer.html b/v2_adminpanel/templates/edit_customer.html index 34bf868..3e69e42 100644 --- a/v2_adminpanel/templates/edit_customer.html +++ b/v2_adminpanel/templates/edit_customer.html @@ -30,6 +30,14 @@ +
+ + +
+
Abbrechen diff --git a/v2_adminpanel/templates/edit_license.html b/v2_adminpanel/templates/edit_license.html index fb2d09a..905887b 100644 --- a/v2_adminpanel/templates/edit_license.html +++ b/v2_adminpanel/templates/edit_license.html @@ -54,6 +54,14 @@
+
+ + +
+
Abbrechen diff --git a/v2_adminpanel/templates/index.html b/v2_adminpanel/templates/index.html index 3636a65..635e8d0 100644 --- a/v2_adminpanel/templates/index.html +++ b/v2_adminpanel/templates/index.html @@ -133,6 +133,15 @@
+ +
+ + +
+
diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index f0c4c91..c53b98e 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -63,6 +63,8 @@ + +
@@ -129,7 +131,12 @@
- {{ license[2] }} + + {{ license[2] }} + {% if license[8] %} + 🧪 + {% endif %} + {{ license[3] or '-' }} {% if license[4] == 'full' %} @@ -141,10 +148,12 @@ {{ license[5].strftime('%d.%m.%Y') }} {{ license[6].strftime('%d.%m.%Y') }} - {% if license[8] == 'abgelaufen' %} + {% if license[9] == 'abgelaufen' %} ⚠️ Abgelaufen - {% elif license[8] == 'läuft bald ab' %} + {% elif license[9] == 'läuft bald ab' %} ⏰ Läuft bald ab + {% elif license[9] == 'deaktiviert' %} + ❌ Deaktiviert {% else %} ✅ Aktiv {% endif %}