Ressource-Pool
Dieser Commit ist enthalten in:
@@ -49,7 +49,9 @@
|
|||||||
"Bash(openssl x509:*)",
|
"Bash(openssl x509:*)",
|
||||||
"Bash(cat:*)",
|
"Bash(cat:*)",
|
||||||
"Bash(openssl dhparam:*)",
|
"Bash(openssl dhparam:*)",
|
||||||
"Bash(rg:*)"
|
"Bash(rg:*)",
|
||||||
|
"Bash(docker cp:*)",
|
||||||
|
"Bash(docker-compose:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
477
JOURNAL.md
477
JOURNAL.md
@@ -1033,7 +1033,7 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
|||||||
- ✅ Audit-Log-Integration
|
- ✅ Audit-Log-Integration
|
||||||
- ✅ Navigation aktualisiert
|
- ✅ Navigation aktualisiert
|
||||||
|
|
||||||
## 2025-01-06: Implementierung Searchable Dropdown für Kundenauswahl
|
## 2025-06-06: Implementierung Searchable Dropdown für Kundenauswahl
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Bei der Lizenzerstellung wurde immer ein neuer Kunde angelegt
|
- Bei der Lizenzerstellung wurde immer ein neuer Kunde angelegt
|
||||||
@@ -1068,7 +1068,7 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
|||||||
- ✅ E-Mail-Duplikate werden verhindert
|
- ✅ E-Mail-Duplikate werden verhindert
|
||||||
- ✅ Sowohl Einzellizenz als auch Batch unterstützt
|
- ✅ Sowohl Einzellizenz als auch Batch unterstützt
|
||||||
|
|
||||||
## 2025-01-06: Automatische Ablaufdatum-Berechnung
|
## 2025-06-06: Automatische Ablaufdatum-Berechnung
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Manuelles Eingeben von Start- und Enddatum war umständlich
|
- Manuelles Eingeben von Start- und Enddatum war umständlich
|
||||||
@@ -1099,7 +1099,7 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
|||||||
- ✅ Backend validiert die Berechnung
|
- ✅ Backend validiert die Berechnung
|
||||||
- ✅ Standardwert (1 Jahr) voreingestellt
|
- ✅ Standardwert (1 Jahr) voreingestellt
|
||||||
|
|
||||||
## 2025-01-06: Bugfix - created_at für licenses Tabelle
|
## 2025-06-06: Bugfix - created_at für licenses Tabelle
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Batch-Generierung schlug fehl mit "Fehler bei der Batch-Generierung!"
|
- Batch-Generierung schlug fehl mit "Fehler bei der Batch-Generierung!"
|
||||||
@@ -1125,7 +1125,7 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
|
|||||||
- ✅ Batch-Generierung funktioniert wieder
|
- ✅ Batch-Generierung funktioniert wieder
|
||||||
- ✅ Konsistente Zeitstempel für Audit-Zwecke
|
- ✅ Konsistente Zeitstempel für Audit-Zwecke
|
||||||
|
|
||||||
## 2025-01-06: Status "Deaktiviert" für manuell abgeschaltete Lizenzen
|
## 2025-06-06: Status "Deaktiviert" für manuell abgeschaltete Lizenzen
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Dashboard zeigte nur "aktiv" und "abgelaufen" als Status
|
- Dashboard zeigte nur "aktiv" und "abgelaufen" als Status
|
||||||
@@ -1490,7 +1490,7 @@ ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME Z
|
|||||||
- ✅ Weitere Klicks: Toggle zwischen ASC/DESC
|
- ✅ Weitere Klicks: Toggle zwischen ASC/DESC
|
||||||
- ✅ Sortierung funktioniert korrekt mit Pagination und Filtern
|
- ✅ Sortierung funktioniert korrekt mit Pagination und Filtern
|
||||||
|
|
||||||
### 2025-01-08: Port 8443 geschlossen - API nur noch über Nginx
|
### 2025-06-09: Port 8443 geschlossen - API nur noch über Nginx
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Doppelte Verfügbarkeit des License Servers (Port 8443 + Nginx) machte keinen Sinn
|
- Doppelte Verfügbarkeit des License Servers (Port 8443 + Nginx) machte keinen Sinn
|
||||||
@@ -1522,7 +1522,7 @@ ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME Z
|
|||||||
- ✅ API nur noch über Nginx Reverse Proxy erreichbar
|
- ✅ API nur noch über Nginx Reverse Proxy erreichbar
|
||||||
- ✅ Sicherheit erhöht durch zentrale Verwaltung
|
- ✅ Sicherheit erhöht durch zentrale Verwaltung
|
||||||
|
|
||||||
### 2025-01-08: Live-Filtering implementiert
|
### 2025-06-09: Live-Filtering implementiert
|
||||||
|
|
||||||
**Problem:**
|
**Problem:**
|
||||||
- Benutzer mussten immer auf "Filter anwenden" klicken
|
- Benutzer mussten immer auf "Filter anwenden" klicken
|
||||||
@@ -1566,3 +1566,468 @@ ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME Z
|
|||||||
- ✅ Live-Filtering auf allen Hauptseiten implementiert
|
- ✅ Live-Filtering auf allen Hauptseiten implementiert
|
||||||
- ✅ Debouncing verhindert zu viele Server-Requests
|
- ✅ Debouncing verhindert zu viele Server-Requests
|
||||||
- ✅ Zurücksetzen-Button bleibt für schnelles Löschen aller Filter
|
- ✅ 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
|
||||||
1
backups/backup_v2docker_20250609_040937_encrypted.sql.gz.enc
Normale Datei
1
backups/backup_v2docker_20250609_040937_encrypted.sql.gz.enc
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
1118
v2_adminpanel/app.py
1118
v2_adminpanel/app.py
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
94
v2_adminpanel/create_resource_tables.sql
Normale Datei
94
v2_adminpanel/create_resource_tables.sql
Normale Datei
@@ -0,0 +1,94 @@
|
|||||||
|
-- Erstelle die fehlenden Resource Pool Tabellen
|
||||||
|
-- Nur ausführen wenn die Tabellen noch nicht existieren
|
||||||
|
|
||||||
|
-- Resource Pool Haupttabelle
|
||||||
|
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,
|
||||||
|
UNIQUE(resource_type, resource_value)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Historie für Resource-Aktionen
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Metriken für Resource Performance
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- 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;
|
||||||
|
|
||||||
|
-- Prüfe ob Tabellen erfolgreich erstellt wurden
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
table_count INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO table_count
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('resource_pools', 'resource_history', 'resource_metrics', 'license_resources');
|
||||||
|
|
||||||
|
IF table_count = 4 THEN
|
||||||
|
RAISE NOTICE 'Alle 4 Resource-Tabellen wurden erfolgreich erstellt!';
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING 'Nur % von 4 Tabellen wurden erstellt!', table_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -102,3 +102,80 @@ BEGIN
|
|||||||
UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL;
|
UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL;
|
||||||
END IF;
|
END IF;
|
||||||
END $$;
|
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,
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- 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;
|
||||||
|
|||||||
263
v2_adminpanel/migrate_existing_licenses.sql
Normale Datei
263
v2_adminpanel/migrate_existing_licenses.sql
Normale Datei
@@ -0,0 +1,263 @@
|
|||||||
|
-- Migration Script für bestehende Lizenzen
|
||||||
|
-- Setzt Default-Werte für Resource Counts
|
||||||
|
-- Stand: 2025-06-09
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Prüfe ob Migration notwendig ist
|
||||||
|
-- ====================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
licenses_without_resources INT;
|
||||||
|
total_licenses INT;
|
||||||
|
BEGIN
|
||||||
|
-- Zähle Lizenzen ohne Resource-Count Werte
|
||||||
|
SELECT COUNT(*) INTO licenses_without_resources
|
||||||
|
FROM licenses
|
||||||
|
WHERE domain_count IS NULL
|
||||||
|
OR ipv4_count IS NULL
|
||||||
|
OR phone_count IS NULL;
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO total_licenses FROM licenses;
|
||||||
|
|
||||||
|
IF licenses_without_resources > 0 THEN
|
||||||
|
RAISE NOTICE 'Migration notwendig für % von % Lizenzen', licenses_without_resources, total_licenses;
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Keine Migration notwendig - alle Lizenzen haben bereits Resource Counts';
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Setze Default Resource Counts
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Vollversionen erhalten standardmäßig je 2 Ressourcen
|
||||||
|
UPDATE licenses
|
||||||
|
SET domain_count = COALESCE(domain_count, 2),
|
||||||
|
ipv4_count = COALESCE(ipv4_count, 2),
|
||||||
|
phone_count = COALESCE(phone_count, 2)
|
||||||
|
WHERE license_type = 'full'
|
||||||
|
AND (domain_count IS NULL OR ipv4_count IS NULL OR phone_count IS NULL);
|
||||||
|
|
||||||
|
-- Testversionen erhalten standardmäßig je 1 Ressource
|
||||||
|
UPDATE licenses
|
||||||
|
SET domain_count = COALESCE(domain_count, 1),
|
||||||
|
ipv4_count = COALESCE(ipv4_count, 1),
|
||||||
|
phone_count = COALESCE(phone_count, 1)
|
||||||
|
WHERE license_type = 'test'
|
||||||
|
AND (domain_count IS NULL OR ipv4_count IS NULL OR phone_count IS NULL);
|
||||||
|
|
||||||
|
-- Inaktive Lizenzen erhalten 0 Ressourcen
|
||||||
|
UPDATE licenses
|
||||||
|
SET domain_count = COALESCE(domain_count, 0),
|
||||||
|
ipv4_count = COALESCE(ipv4_count, 0),
|
||||||
|
phone_count = COALESCE(phone_count, 0)
|
||||||
|
WHERE is_active = FALSE
|
||||||
|
AND (domain_count IS NULL OR ipv4_count IS NULL OR phone_count IS NULL);
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Erstelle Resource-Zuweisungen für bestehende aktive Lizenzen
|
||||||
|
-- ====================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
license_rec RECORD;
|
||||||
|
resource_rec RECORD;
|
||||||
|
resources_assigned INT := 0;
|
||||||
|
resources_needed INT;
|
||||||
|
BEGIN
|
||||||
|
-- Durchlaufe alle aktiven Lizenzen mit Resource Counts > 0
|
||||||
|
FOR license_rec IN
|
||||||
|
SELECT id, domain_count, ipv4_count, phone_count
|
||||||
|
FROM licenses
|
||||||
|
WHERE is_active = TRUE
|
||||||
|
AND (domain_count > 0 OR ipv4_count > 0 OR phone_count > 0)
|
||||||
|
ORDER BY created_at
|
||||||
|
LOOP
|
||||||
|
-- Domains zuweisen
|
||||||
|
resources_needed := license_rec.domain_count;
|
||||||
|
FOR i IN 1..resources_needed LOOP
|
||||||
|
-- Finde eine verfügbare Domain
|
||||||
|
SELECT id INTO resource_rec
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'domain'
|
||||||
|
AND status = 'available'
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF resource_rec.id IS NOT NULL THEN
|
||||||
|
-- Weise Resource zu
|
||||||
|
UPDATE resource_pools
|
||||||
|
SET status = 'allocated',
|
||||||
|
allocated_to_license = license_rec.id,
|
||||||
|
status_changed_at = NOW(),
|
||||||
|
status_changed_by = 'migration'
|
||||||
|
WHERE id = resource_rec.id;
|
||||||
|
|
||||||
|
-- Erstelle Zuordnung
|
||||||
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||||
|
VALUES (license_rec.id, resource_rec.id, 'migration');
|
||||||
|
|
||||||
|
-- Historie eintragen
|
||||||
|
INSERT INTO resource_history (resource_id, license_id, action, action_by, details)
|
||||||
|
VALUES (resource_rec.id, license_rec.id, 'allocated', 'migration',
|
||||||
|
'{"reason": "Migration bestehender Lizenzen"}'::jsonb);
|
||||||
|
|
||||||
|
resources_assigned := resources_assigned + 1;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- IPv4-Adressen zuweisen
|
||||||
|
resources_needed := license_rec.ipv4_count;
|
||||||
|
FOR i IN 1..resources_needed LOOP
|
||||||
|
SELECT id INTO resource_rec
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'ipv4'
|
||||||
|
AND status = 'available'
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF resource_rec.id IS NOT NULL THEN
|
||||||
|
UPDATE resource_pools
|
||||||
|
SET status = 'allocated',
|
||||||
|
allocated_to_license = license_rec.id,
|
||||||
|
status_changed_at = NOW(),
|
||||||
|
status_changed_by = 'migration'
|
||||||
|
WHERE id = resource_rec.id;
|
||||||
|
|
||||||
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||||
|
VALUES (license_rec.id, resource_rec.id, 'migration');
|
||||||
|
|
||||||
|
INSERT INTO resource_history (resource_id, license_id, action, action_by, details)
|
||||||
|
VALUES (resource_rec.id, license_rec.id, 'allocated', 'migration',
|
||||||
|
'{"reason": "Migration bestehender Lizenzen"}'::jsonb);
|
||||||
|
|
||||||
|
resources_assigned := resources_assigned + 1;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Telefonnummern zuweisen
|
||||||
|
resources_needed := license_rec.phone_count;
|
||||||
|
FOR i IN 1..resources_needed LOOP
|
||||||
|
SELECT id INTO resource_rec
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'phone'
|
||||||
|
AND status = 'available'
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF resource_rec.id IS NOT NULL THEN
|
||||||
|
UPDATE resource_pools
|
||||||
|
SET status = 'allocated',
|
||||||
|
allocated_to_license = license_rec.id,
|
||||||
|
status_changed_at = NOW(),
|
||||||
|
status_changed_by = 'migration'
|
||||||
|
WHERE id = resource_rec.id;
|
||||||
|
|
||||||
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||||
|
VALUES (license_rec.id, resource_rec.id, 'migration');
|
||||||
|
|
||||||
|
INSERT INTO resource_history (resource_id, license_id, action, action_by, details)
|
||||||
|
VALUES (resource_rec.id, license_rec.id, 'allocated', 'migration',
|
||||||
|
'{"reason": "Migration bestehender Lizenzen"}'::jsonb);
|
||||||
|
|
||||||
|
resources_assigned := resources_assigned + 1;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Migration abgeschlossen: % Ressourcen wurden zugewiesen', resources_assigned;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Abschlussbericht
|
||||||
|
-- ====================================
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_total_licenses INT;
|
||||||
|
v_licenses_with_resources INT;
|
||||||
|
v_total_resources_assigned INT;
|
||||||
|
v_domains_assigned INT;
|
||||||
|
v_ipv4_assigned INT;
|
||||||
|
v_phones_assigned INT;
|
||||||
|
BEGIN
|
||||||
|
-- Statistiken sammeln
|
||||||
|
SELECT COUNT(*) INTO v_total_licenses FROM licenses;
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO v_licenses_with_resources
|
||||||
|
FROM licenses
|
||||||
|
WHERE id IN (SELECT DISTINCT license_id FROM license_resources WHERE is_active = TRUE);
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO v_total_resources_assigned
|
||||||
|
FROM license_resources
|
||||||
|
WHERE is_active = TRUE;
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO v_domains_assigned
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'domain' AND status = 'allocated';
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO v_ipv4_assigned
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'ipv4' AND status = 'allocated';
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO v_phones_assigned
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE resource_type = 'phone' AND status = 'allocated';
|
||||||
|
|
||||||
|
-- Bericht ausgeben
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '========================================';
|
||||||
|
RAISE NOTICE 'MIGRATION ABGESCHLOSSEN';
|
||||||
|
RAISE NOTICE '========================================';
|
||||||
|
RAISE NOTICE 'Lizenzen gesamt: %', v_total_licenses;
|
||||||
|
RAISE NOTICE 'Lizenzen mit Ressourcen: %', v_licenses_with_resources;
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'Zugewiesene Ressourcen:';
|
||||||
|
RAISE NOTICE '- Domains: %', v_domains_assigned;
|
||||||
|
RAISE NOTICE '- IPv4-Adressen: %', v_ipv4_assigned;
|
||||||
|
RAISE NOTICE '- Telefonnummern: %', v_phones_assigned;
|
||||||
|
RAISE NOTICE '- Gesamt: %', v_total_resources_assigned;
|
||||||
|
RAISE NOTICE '========================================';
|
||||||
|
|
||||||
|
-- Warnungen ausgeben
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM licenses l
|
||||||
|
WHERE l.is_active = TRUE
|
||||||
|
AND (l.domain_count > 0 OR l.ipv4_count > 0 OR l.phone_count > 0)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM license_resources lr
|
||||||
|
WHERE lr.license_id = l.id AND lr.is_active = TRUE
|
||||||
|
)
|
||||||
|
) THEN
|
||||||
|
RAISE WARNING 'ACHTUNG: Einige aktive Lizenzen konnten keine Ressourcen erhalten (möglicherweise nicht genug verfügbar)';
|
||||||
|
|
||||||
|
-- Liste betroffene Lizenzen auf
|
||||||
|
FOR v_total_licenses IN
|
||||||
|
SELECT l.id FROM licenses l
|
||||||
|
WHERE l.is_active = TRUE
|
||||||
|
AND (l.domain_count > 0 OR l.ipv4_count > 0 OR l.phone_count > 0)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM license_resources lr
|
||||||
|
WHERE lr.license_id = l.id AND lr.is_active = TRUE
|
||||||
|
)
|
||||||
|
LOOP
|
||||||
|
RAISE WARNING 'Lizenz ID % hat keine Ressourcen erhalten', v_total_licenses;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Audit Log Eintrag für Migration
|
||||||
|
-- ====================================
|
||||||
|
INSERT INTO audit_log (
|
||||||
|
timestamp,
|
||||||
|
username,
|
||||||
|
action,
|
||||||
|
entity_type,
|
||||||
|
additional_info
|
||||||
|
) VALUES (
|
||||||
|
NOW(),
|
||||||
|
'system',
|
||||||
|
'MIGRATION',
|
||||||
|
'licenses',
|
||||||
|
'Resource Pool Migration für bestehende Lizenzen durchgeführt'
|
||||||
|
);
|
||||||
431
v2_adminpanel/templates/add_resources.html
Normale Datei
431
v2_adminpanel/templates/add_resources.html
Normale Datei
@@ -0,0 +1,431 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Ressourcen hinzufügen{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Card Styling */
|
||||||
|
.main-card {
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Section */
|
||||||
|
.preview-card {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.preview-card.active {
|
||||||
|
border-color: #28a745;
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Format Examples */
|
||||||
|
.example-card {
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.example-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.example-code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource Type Selector */
|
||||||
|
.resource-type-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.resource-type-option {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.resource-type-option:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.resource-type-option.selected {
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
}
|
||||||
|
.resource-type-option .icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Textarea Styling */
|
||||||
|
.resource-input {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.resource-input:focus {
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #80bdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Display */
|
||||||
|
.stats-display {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">Ressourcen hinzufügen</h1>
|
||||||
|
<p class="text-muted mb-0">Fügen Sie neue Domains, IPs oder Telefonnummern zum Pool hinzu</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('resources') }}" class="btn btn-secondary">
|
||||||
|
← Zurück zur Übersicht
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('add_resources') }}" id="addResourceForm">
|
||||||
|
<!-- Resource Type Selection -->
|
||||||
|
<div class="card main-card mb-4">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">1️⃣ Ressourcentyp wählen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<input type="hidden" name="resource_type" id="resource_type" required>
|
||||||
|
<div class="resource-type-selector">
|
||||||
|
<div class="resource-type-option" data-type="domain">
|
||||||
|
<div class="icon">🌐</div>
|
||||||
|
<h6 class="mb-0">Domain</h6>
|
||||||
|
<small class="text-muted">Webseiten-Adressen</small>
|
||||||
|
</div>
|
||||||
|
<div class="resource-type-option" data-type="ipv4">
|
||||||
|
<div class="icon">🖥️</div>
|
||||||
|
<h6 class="mb-0">IPv4</h6>
|
||||||
|
<small class="text-muted">IP-Adressen</small>
|
||||||
|
</div>
|
||||||
|
<div class="resource-type-option" data-type="phone">
|
||||||
|
<div class="icon">📱</div>
|
||||||
|
<h6 class="mb-0">Telefon</h6>
|
||||||
|
<small class="text-muted">Telefonnummern</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resource Input -->
|
||||||
|
<div class="card main-card mb-4">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">2️⃣ Ressourcen eingeben</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="resources_text" class="form-label">
|
||||||
|
Ressourcen (eine pro Zeile)
|
||||||
|
</label>
|
||||||
|
<textarea name="resources_text"
|
||||||
|
id="resources_text"
|
||||||
|
class="form-control resource-input"
|
||||||
|
rows="12"
|
||||||
|
required
|
||||||
|
placeholder="Bitte wählen Sie zuerst einen Ressourcentyp aus..."></textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Geben Sie jede Ressource in eine neue Zeile ein. Duplikate werden automatisch übersprungen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Live Preview -->
|
||||||
|
<div class="preview-card p-3" id="preview">
|
||||||
|
<h6 class="mb-3">📊 Live-Vorschau</h6>
|
||||||
|
<div class="stats-display">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value" id="validCount">0</div>
|
||||||
|
<div class="stat-label">Gültig</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value text-warning" id="duplicateCount">0</div>
|
||||||
|
<div class="stat-label">Duplikate</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-value text-danger" id="invalidCount">0</div>
|
||||||
|
<div class="stat-label">Ungültig</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="errorList" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>Fehler gefunden:</strong>
|
||||||
|
<ul id="errorMessages" class="mb-0"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Format Examples -->
|
||||||
|
<div class="card main-card mb-4">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">💡 Format-Beispiele</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card example-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">
|
||||||
|
<span class="text-primary">🌐</span> Domains
|
||||||
|
</h6>
|
||||||
|
<pre class="example-code">example.com
|
||||||
|
test-domain.net
|
||||||
|
meine-seite.de
|
||||||
|
subdomain.example.org
|
||||||
|
my-website.io</pre>
|
||||||
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
<small>
|
||||||
|
<strong>Format:</strong> Ohne http(s)://<br>
|
||||||
|
<strong>Erlaubt:</strong> Buchstaben, Zahlen, Punkt, Bindestrich
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card example-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">
|
||||||
|
<span class="text-primary">🖥️</span> IPv4-Adressen
|
||||||
|
</h6>
|
||||||
|
<pre class="example-code">192.168.1.10
|
||||||
|
192.168.1.11
|
||||||
|
10.0.0.1
|
||||||
|
172.16.0.5
|
||||||
|
8.8.8.8</pre>
|
||||||
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
<small>
|
||||||
|
<strong>Format:</strong> xxx.xxx.xxx.xxx<br>
|
||||||
|
<strong>Bereich:</strong> 0-255 pro Oktett
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card example-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">
|
||||||
|
<span class="text-primary">📱</span> Telefonnummern
|
||||||
|
</h6>
|
||||||
|
<pre class="example-code">+491701234567
|
||||||
|
+493012345678
|
||||||
|
+33123456789
|
||||||
|
+441234567890
|
||||||
|
+12125551234</pre>
|
||||||
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
<small>
|
||||||
|
<strong>Format:</strong> Mit Ländervorwahl<br>
|
||||||
|
<strong>Start:</strong> Immer mit +
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Buttons -->
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="window.location.href='{{ url_for('resources') }}'">
|
||||||
|
<i class="fas fa-times"></i> Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-success btn-lg" id="submitBtn" disabled>
|
||||||
|
<i class="fas fa-plus-circle"></i> Ressourcen hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const typeOptions = document.querySelectorAll('.resource-type-option');
|
||||||
|
const typeInput = document.getElementById('resource_type');
|
||||||
|
const textArea = document.getElementById('resources_text');
|
||||||
|
const submitBtn = document.getElementById('submitBtn');
|
||||||
|
const form = document.getElementById('addResourceForm');
|
||||||
|
|
||||||
|
// Preview elements
|
||||||
|
const validCount = document.getElementById('validCount');
|
||||||
|
const duplicateCount = document.getElementById('duplicateCount');
|
||||||
|
const invalidCount = document.getElementById('invalidCount');
|
||||||
|
const errorList = document.getElementById('errorList');
|
||||||
|
const errorMessages = document.getElementById('errorMessages');
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
|
||||||
|
let selectedType = null;
|
||||||
|
|
||||||
|
// Placeholder texts for different types
|
||||||
|
const placeholders = {
|
||||||
|
domain: `example.com
|
||||||
|
test-site.net
|
||||||
|
my-domain.org
|
||||||
|
subdomain.example.com`,
|
||||||
|
ipv4: `192.168.1.1
|
||||||
|
10.0.0.1
|
||||||
|
172.16.0.1
|
||||||
|
8.8.8.8`,
|
||||||
|
phone: `+491234567890
|
||||||
|
+4930123456
|
||||||
|
+33123456789
|
||||||
|
+12125551234`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resource type selection
|
||||||
|
typeOptions.forEach(option => {
|
||||||
|
option.addEventListener('click', function() {
|
||||||
|
typeOptions.forEach(opt => opt.classList.remove('selected'));
|
||||||
|
this.classList.add('selected');
|
||||||
|
selectedType = this.dataset.type;
|
||||||
|
typeInput.value = selectedType;
|
||||||
|
textArea.placeholder = placeholders[selectedType] || '';
|
||||||
|
textArea.disabled = false;
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
|
function validateDomain(domain) {
|
||||||
|
const domainRegex = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,}$/;
|
||||||
|
return domainRegex.test(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIPv4(ip) {
|
||||||
|
const ipRegex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
return ipRegex.test(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePhone(phone) {
|
||||||
|
const phoneRegex = /^\+[1-9]\d{6,14}$/;
|
||||||
|
return phoneRegex.test(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update preview function
|
||||||
|
function updatePreview() {
|
||||||
|
if (!selectedType) {
|
||||||
|
preview.classList.remove('active');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = textArea.value.split('\n').filter(line => line.trim() !== '');
|
||||||
|
const uniqueResources = new Set();
|
||||||
|
const errors = [];
|
||||||
|
let valid = 0;
|
||||||
|
let duplicates = 0;
|
||||||
|
let invalid = 0;
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (uniqueResources.has(trimmed)) {
|
||||||
|
duplicates++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValid = false;
|
||||||
|
switch(selectedType) {
|
||||||
|
case 'domain':
|
||||||
|
isValid = validateDomain(trimmed);
|
||||||
|
break;
|
||||||
|
case 'ipv4':
|
||||||
|
isValid = validateIPv4(trimmed);
|
||||||
|
break;
|
||||||
|
case 'phone':
|
||||||
|
isValid = validatePhone(trimmed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
valid++;
|
||||||
|
uniqueResources.add(trimmed);
|
||||||
|
} else {
|
||||||
|
invalid++;
|
||||||
|
errors.push(`Zeile ${index + 1}: "${trimmed}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
validCount.textContent = valid;
|
||||||
|
duplicateCount.textContent = duplicates;
|
||||||
|
invalidCount.textContent = invalid;
|
||||||
|
|
||||||
|
// Show/hide error list
|
||||||
|
if (errors.length > 0) {
|
||||||
|
errorList.style.display = 'block';
|
||||||
|
errorMessages.innerHTML = errors.map(err => `<li>${err}</li>`).join('');
|
||||||
|
} else {
|
||||||
|
errorList.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable submit button
|
||||||
|
submitBtn.disabled = valid === 0 || invalid > 0;
|
||||||
|
|
||||||
|
// Update preview appearance
|
||||||
|
if (lines.length > 0) {
|
||||||
|
preview.classList.add('active');
|
||||||
|
} else {
|
||||||
|
preview.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live validation
|
||||||
|
textArea.addEventListener('input', updatePreview);
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
if (submitBtn.disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Bitte beheben Sie alle Fehler bevor Sie fortfahren.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
textArea.disabled = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -220,15 +220,23 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<nav class="navbar navbar-dark bg-dark">
|
<nav class="navbar navbar-dark bg-dark navbar-expand-lg">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a href="/" class="navbar-brand text-decoration-none">🎛️ AccountForger - Admin Panel</a>
|
<a href="/" class="navbar-brand text-decoration-none">🎛️ AccountForger - Admin Panel</a>
|
||||||
<div class="d-flex align-items-center">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<div id="session-timer" class="timer-normal me-3">
|
<span class="navbar-toggler-icon"></span>
|
||||||
⏱️ <span id="timer-display">5:00</span>
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<!-- Navigation Links entfernt - Zugriff über Dashboard -->
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div id="session-timer" class="timer-normal me-3">
|
||||||
|
⏱️ <span id="timer-display">5:00</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
|
||||||
|
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
|
|
||||||
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -247,8 +255,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -91,6 +91,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 pro Lizenz
|
||||||
|
<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>
|
||||||
|
| Benötigt: <span id="domainsNeeded" 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>
|
||||||
|
| Benötigt: <span id="ipv4Needed" 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>
|
||||||
|
| Benötigt: <span id="phoneNeeded" class="fw-bold">-</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Batch-Ressourcen:</strong> Jede Lizenz erhält die angegebene Anzahl an Ressourcen.
|
||||||
|
Bei 10 Lizenzen mit je 2 Domains werden insgesamt 20 Domains benötigt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 d-flex gap-2">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
🔑 Batch generieren
|
🔑 Batch generieren
|
||||||
@@ -243,6 +304,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('customerName').required = false;
|
document.getElementById('customerName').required = false;
|
||||||
document.getElementById('email').required = false;
|
document.getElementById('email').required = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resource Availability Check
|
||||||
|
checkResourceAvailability();
|
||||||
|
|
||||||
|
// Event Listener für Resource Count und Quantity Änderungen
|
||||||
|
document.getElementById('domainCount').addEventListener('change', checkResourceAvailability);
|
||||||
|
document.getElementById('ipv4Count').addEventListener('change', checkResourceAvailability);
|
||||||
|
document.getElementById('phoneCount').addEventListener('change', checkResourceAvailability);
|
||||||
|
document.getElementById('quantity').addEventListener('input', checkResourceAvailability);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vorschau-Funktion
|
// Vorschau-Funktion
|
||||||
@@ -269,5 +339,96 @@ document.getElementById('quantity').addEventListener('input', function(e) {
|
|||||||
e.target.value = 1;
|
e.target.value = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Funktion zur Prüfung der Ressourcen-Verfügbarkeit für Batch
|
||||||
|
function checkResourceAvailability() {
|
||||||
|
const quantity = parseInt(document.getElementById('quantity').value) || 1;
|
||||||
|
const domainCount = parseInt(document.getElementById('domainCount').value) || 0;
|
||||||
|
const ipv4Count = parseInt(document.getElementById('ipv4Count').value) || 0;
|
||||||
|
const phoneCount = parseInt(document.getElementById('phoneCount').value) || 0;
|
||||||
|
|
||||||
|
// Berechne Gesamtbedarf
|
||||||
|
const totalDomains = domainCount * quantity;
|
||||||
|
const totalIpv4 = ipv4Count * quantity;
|
||||||
|
const totalPhones = phoneCount * quantity;
|
||||||
|
|
||||||
|
// Update "Benötigt" Anzeigen
|
||||||
|
document.getElementById('domainsNeeded').textContent = totalDomains;
|
||||||
|
document.getElementById('ipv4Needed').textContent = totalIpv4;
|
||||||
|
document.getElementById('phoneNeeded').textContent = totalPhones;
|
||||||
|
|
||||||
|
// API-Call zur Verfügbarkeitsprüfung
|
||||||
|
fetch(`/api/resources/check-availability?domain=${totalDomains}&ipv4=${totalIpv4}&phone=${totalPhones}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update der Verfügbarkeitsanzeigen
|
||||||
|
updateAvailabilityDisplay('domainsAvailable', data.domain_available, totalDomains);
|
||||||
|
updateAvailabilityDisplay('ipv4Available', data.ipv4_available, totalIpv4);
|
||||||
|
updateAvailabilityDisplay('phoneAvailable', data.phone_available, totalPhones);
|
||||||
|
|
||||||
|
// Gesamtstatus aktualisieren
|
||||||
|
updateBatchResourceStatus(data, totalDomains, totalIpv4, totalPhones, quantity);
|
||||||
|
})
|
||||||
|
.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);
|
||||||
|
element.textContent = available;
|
||||||
|
|
||||||
|
const neededElement = element.parentElement.querySelector('.fw-bold:last-child');
|
||||||
|
|
||||||
|
if (requested > 0 && available < requested) {
|
||||||
|
element.classList.remove('text-success');
|
||||||
|
element.classList.add('text-danger');
|
||||||
|
neededElement.classList.add('text-danger');
|
||||||
|
neededElement.classList.remove('text-success');
|
||||||
|
} else if (available < 50) {
|
||||||
|
element.classList.remove('text-success', 'text-danger');
|
||||||
|
element.classList.add('text-warning');
|
||||||
|
} else {
|
||||||
|
element.classList.remove('text-danger', 'text-warning');
|
||||||
|
element.classList.add('text-success');
|
||||||
|
neededElement.classList.remove('text-danger');
|
||||||
|
neededElement.classList.add('text-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gesamtstatus der Ressourcen-Verfügbarkeit für Batch
|
||||||
|
function updateBatchResourceStatus(data, totalDomains, totalIpv4, totalPhones, quantity) {
|
||||||
|
const statusElement = document.getElementById('resourceStatus');
|
||||||
|
let hasIssue = false;
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
if (totalDomains > 0 && data.domain_available < totalDomains) {
|
||||||
|
hasIssue = true;
|
||||||
|
message = `⚠️ Nicht genügend Domains (${data.domain_available}/${totalDomains})`;
|
||||||
|
} else if (totalIpv4 > 0 && data.ipv4_available < totalIpv4) {
|
||||||
|
hasIssue = true;
|
||||||
|
message = `⚠️ Nicht genügend IPv4-Adressen (${data.ipv4_available}/${totalIpv4})`;
|
||||||
|
} else if (totalPhones > 0 && data.phone_available < totalPhones) {
|
||||||
|
hasIssue = true;
|
||||||
|
message = `⚠️ Nicht genügend Telefonnummern (${data.phone_available}/${totalPhones})`;
|
||||||
|
} else {
|
||||||
|
message = `✅ Ressourcen für ${quantity} Lizenzen verfügbar`;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusElement.textContent = message;
|
||||||
|
statusElement.className = hasIssue ? 'text-danger' : 'text-success';
|
||||||
|
|
||||||
|
// Disable submit button if not enough resources
|
||||||
|
const submitButton = document.querySelector('button[type="submit"]');
|
||||||
|
submitButton.disabled = hasIssue;
|
||||||
|
if (hasIssue) {
|
||||||
|
submitButton.classList.add('btn-secondary');
|
||||||
|
submitButton.classList.remove('btn-primary');
|
||||||
|
} else {
|
||||||
|
submitButton.classList.add('btn-primary');
|
||||||
|
submitButton.classList.remove('btn-secondary');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -256,6 +256,67 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Resource Pool Status -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-server"></i> Resource Pool Status
|
||||||
|
<a href="/resources" class="btn btn-sm btn-light float-end">Verwalten →</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% if resource_stats %}
|
||||||
|
{% for type, data in resource_stats.items() %}
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-{{ 'globe' if type == 'domain' else ('network-wired' if type == 'ipv4' else 'phone') }} fa-2x text-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-1">{{ type|upper }}</h6>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<strong>{{ data.available }}</strong> / {{ data.total }} verfügbar
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}">
|
||||||
|
{{ data.available_percent }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress mt-1" style="height: 6px;">
|
||||||
|
<div class="progress-bar bg-{{ 'success' if data.available_percent > 50 else ('warning' if data.available_percent > 20 else 'danger') }}"
|
||||||
|
style="width: {{ data.available_percent }}%"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ data.allocated }} zugeteilt | {{ data.quarantine }} in Quarantäne
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12 text-center text-muted">
|
||||||
|
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||||
|
<p>Keine Ressourcen im Pool vorhanden.</p>
|
||||||
|
<a href="/resources/add" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Ressourcen hinzufügen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if resource_warning %}
|
||||||
|
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Warnung:</strong> {{ resource_warning }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<!-- Bald ablaufende Lizenzen -->
|
<!-- Bald ablaufende Lizenzen -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -75,6 +75,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
|
<button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,6 +340,77 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.getElementById('customerName').required = false;
|
document.getElementById('customerName').required = false;
|
||||||
document.getElementById('email').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);
|
||||||
|
element.textContent = available;
|
||||||
|
|
||||||
|
if (requested > 0 && available < requested) {
|
||||||
|
element.classList.remove('text-success');
|
||||||
|
element.classList.add('text-danger');
|
||||||
|
} else if (available < 50) {
|
||||||
|
element.classList.remove('text-success', 'text-danger');
|
||||||
|
element.classList.add('text-warning');
|
||||||
|
} else {
|
||||||
|
element.classList.remove('text-danger', 'text-warning');
|
||||||
|
element.classList.add('text-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
365
v2_adminpanel/templates/resource_history.html
Normale Datei
365
v2_adminpanel/templates/resource_history.html
Normale Datei
@@ -0,0 +1,365 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Resource Historie{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Resource Info Card */
|
||||||
|
.resource-info-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource Value Display */
|
||||||
|
.resource-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badge Large */
|
||||||
|
.status-badge-large {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline Styling */
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 30px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3px;
|
||||||
|
background: linear-gradient(to bottom, #e9ecef 0%, #dee2e6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 80px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-marker {
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
top: 15px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 10px 10px 10px 0;
|
||||||
|
border-color: transparent #fff transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Icons */
|
||||||
|
.action-icon {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-created { background-color: #d4edda; color: #155724; }
|
||||||
|
.action-allocated { background-color: #cce5ff; color: #004085; }
|
||||||
|
.action-deallocated { background-color: #d1ecf1; color: #0c5460; }
|
||||||
|
.action-quarantined { background-color: #fff3cd; color: #856404; }
|
||||||
|
.action-released { background-color: #d4edda; color: #155724; }
|
||||||
|
.action-deleted { background-color: #f8d7da; color: #721c24; }
|
||||||
|
|
||||||
|
/* Details Box */
|
||||||
|
.details-box {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Grid */
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">Resource Historie</h1>
|
||||||
|
<p class="text-muted mb-0">Detaillierte Aktivitätshistorie</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('resources') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück zur Übersicht
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resource Info Card -->
|
||||||
|
<div class="card resource-info-card mb-4">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">📋 Resource Details</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Main Resource Info -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
{% if resource.resource_type == 'domain' %}
|
||||||
|
<span class="display-1">🌐</span>
|
||||||
|
{% elif resource.resource_type == 'ipv4' %}
|
||||||
|
<span class="display-1">🖥️</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="display-1">📱</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="resource-value mb-3">{{ resource.resource_value }}</div>
|
||||||
|
<div>
|
||||||
|
{% if resource.status == 'available' %}
|
||||||
|
<span class="status-badge-large badge bg-success">
|
||||||
|
✅ Verfügbar
|
||||||
|
</span>
|
||||||
|
{% elif resource.status == 'allocated' %}
|
||||||
|
<span class="status-badge-large badge bg-primary">
|
||||||
|
🔗 Zugeteilt
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge-large badge bg-warning text-dark">
|
||||||
|
⚠️ Quarantäne
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Info Grid -->
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Ressourcentyp</div>
|
||||||
|
<div class="info-value">{{ resource.resource_type|upper }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Erstellt am</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ resource.created_at.strftime('%d.%m.%Y %H:%M') if resource.created_at else '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Status geändert</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ resource.status_changed_at.strftime('%d.%m.%Y %H:%M') if resource.status_changed_at else '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if resource.allocated_to_license %}
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Zugewiesen an Lizenz</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<a href="{{ url_for('edit_license', license_id=resource.allocated_to_license) }}"
|
||||||
|
class="text-decoration-none">
|
||||||
|
{{ license_info.license_key if license_info else 'ID: ' + resource.allocated_to_license|string }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if resource.quarantine_reason %}
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Quarantäne-Grund</div>
|
||||||
|
<div class="info-value">
|
||||||
|
<span class="badge bg-warning text-dark">{{ resource.quarantine_reason }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if resource.quarantine_until %}
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Quarantäne bis</div>
|
||||||
|
<div class="info-value">
|
||||||
|
{{ resource.quarantine_until.strftime('%d.%m.%Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if resource.notes %}
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<h6 class="alert-heading">📝 Notizen</h6>
|
||||||
|
<p class="mb-0">{{ resource.notes }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History Timeline -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">⏱️ Aktivitäts-Historie</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if history %}
|
||||||
|
<div class="timeline">
|
||||||
|
{% for event in history %}
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker
|
||||||
|
{% if event.action == 'created' %}bg-success
|
||||||
|
{% elif event.action == 'allocated' %}bg-primary
|
||||||
|
{% elif event.action == 'deallocated' %}bg-info
|
||||||
|
{% elif event.action == 'quarantined' %}bg-warning
|
||||||
|
{% elif event.action == 'released' %}bg-success
|
||||||
|
{% elif event.action == 'deleted' %}bg-danger
|
||||||
|
{% else %}bg-secondary{% endif %}">
|
||||||
|
</div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
{% if event.action == 'created' %}
|
||||||
|
<span class="action-icon action-created">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">Ressource erstellt</h6>
|
||||||
|
{% elif event.action == 'allocated' %}
|
||||||
|
<span class="action-icon action-allocated">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">An Lizenz zugeteilt</h6>
|
||||||
|
{% elif event.action == 'deallocated' %}
|
||||||
|
<span class="action-icon action-deallocated">
|
||||||
|
<i class="fas fa-unlink"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">Von Lizenz freigegeben</h6>
|
||||||
|
{% elif event.action == 'quarantined' %}
|
||||||
|
<span class="action-icon action-quarantined">
|
||||||
|
<i class="fas fa-ban"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">In Quarantäne gesetzt</h6>
|
||||||
|
{% elif event.action == 'released' %}
|
||||||
|
<span class="action-icon action-released">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">Aus Quarantäne entlassen</h6>
|
||||||
|
{% elif event.action == 'deleted' %}
|
||||||
|
<span class="action-icon action-deleted">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</span>
|
||||||
|
<h6 class="mb-0">Ressource gelöscht</h6>
|
||||||
|
{% else %}
|
||||||
|
<h6 class="mb-0">{{ event.action }}</h6>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted small">
|
||||||
|
<i class="fas fa-user"></i> {{ event.action_by }}
|
||||||
|
{% if event.ip_address %}
|
||||||
|
• <i class="fas fa-globe"></i> {{ event.ip_address }}
|
||||||
|
{% endif %}
|
||||||
|
{% if event.license_id %}
|
||||||
|
•
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
<a href="{{ url_for('edit_license', license_id=event.license_id) }}">
|
||||||
|
Lizenz #{{ event.license_id }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if event.details %}
|
||||||
|
<div class="details-box">
|
||||||
|
<strong>Details:</strong>
|
||||||
|
<pre class="mb-0" style="white-space: pre-wrap;">{{ event.details|tojson(indent=2) }}</pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3">
|
||||||
|
<div class="badge bg-light text-dark">
|
||||||
|
<i class="far fa-calendar"></i> {{ event.action_at.strftime('%d.%m.%Y') }}
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted mt-1">
|
||||||
|
<i class="far fa-clock"></i> {{ event.action_at.strftime('%H:%M:%S') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-history text-muted" style="font-size: 3rem; opacity: 0.5;"></i>
|
||||||
|
<p class="text-muted mt-3">Keine Historie-Einträge vorhanden.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
559
v2_adminpanel/templates/resource_metrics.html
Normale Datei
559
v2_adminpanel/templates/resource_metrics.html
Normale Datei
@@ -0,0 +1,559 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Resource Metriken{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Metric Cards */
|
||||||
|
.metric-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.metric-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.metric-card .card-body {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.metric-sublabel {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Cards */
|
||||||
|
.chart-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.chart-card .card-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance Table */
|
||||||
|
.performance-table {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.performance-table td {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.performance-table .resource-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
.performance-table .resource-link:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bars */
|
||||||
|
.progress-custom {
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 11px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon Badges */
|
||||||
|
.icon-badge {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.icon-badge.blue { background-color: #e7f3ff; color: #0066cc; }
|
||||||
|
.icon-badge.green { background-color: #e8f5e9; color: #2e7d32; }
|
||||||
|
.icon-badge.orange { background-color: #fff3e0; color: #ef6c00; }
|
||||||
|
.icon-badge.red { background-color: #ffebee; color: #c62828; }
|
||||||
|
|
||||||
|
/* Trend Indicator */
|
||||||
|
.trend-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
.trend-up {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
.trend-down {
|
||||||
|
background-color: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
.trend-neutral {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #616161;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">Performance Dashboard</h1>
|
||||||
|
<p class="text-muted mb-0">Resource Pool Metriken und Analysen</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('resources_report') }}" class="btn btn-info">
|
||||||
|
📄 Report generieren
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('resources') }}" class="btn btn-secondary">
|
||||||
|
← Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Key Metrics -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="card metric-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="icon-badge blue">
|
||||||
|
📊
|
||||||
|
</div>
|
||||||
|
<div class="metric-label">Ressourcen gesamt</div>
|
||||||
|
<div class="metric-value text-primary">{{ stats.total_resources or 0 }}</div>
|
||||||
|
<div class="metric-sublabel">Aktive Ressourcen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="card metric-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="icon-badge green">
|
||||||
|
📈
|
||||||
|
</div>
|
||||||
|
<div class="metric-label">Ø Performance</div>
|
||||||
|
<div class="metric-value text-{{ 'success' if stats.avg_performance > 80 else ('warning' if stats.avg_performance > 60 else 'danger') }}">
|
||||||
|
{{ "%.1f"|format(stats.avg_performance or 0) }}%
|
||||||
|
</div>
|
||||||
|
<div class="metric-sublabel">Letzte 30 Tage</div>
|
||||||
|
{% if stats.performance_trend %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="trend-indicator trend-{{ stats.performance_trend }}">
|
||||||
|
{% if stats.performance_trend == 'up' %}
|
||||||
|
<i class="fas fa-arrow-up me-1"></i> Steigend
|
||||||
|
{% elif stats.performance_trend == 'down' %}
|
||||||
|
<i class="fas fa-arrow-down me-1"></i> Fallend
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-minus me-1"></i> Stabil
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="card metric-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="icon-badge orange">
|
||||||
|
💰
|
||||||
|
</div>
|
||||||
|
<div class="metric-label">ROI</div>
|
||||||
|
<div class="metric-value text-{{ 'success' if stats.roi > 1 else 'danger' }}">
|
||||||
|
{{ "%.2f"|format(stats.roi) }}x
|
||||||
|
</div>
|
||||||
|
<div class="metric-sublabel">Revenue / Cost</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="card metric-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="icon-badge red">
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
<div class="metric-label">Probleme</div>
|
||||||
|
<div class="metric-value text-{{ 'danger' if stats.total_issues > 10 else ('warning' if stats.total_issues > 5 else 'success') }}">
|
||||||
|
{{ stats.total_issues or 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="metric-sublabel">Letzte 30 Tage</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Row -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">📊 Performance nach Ressourcentyp</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="performanceByTypeChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">🎯 Auslastung nach Typ</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="utilizationChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Performance Tables -->
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h5 class="mb-0">🏆 Top Performer</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table performance-table mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Ressource</th>
|
||||||
|
<th width="80">Typ</th>
|
||||||
|
<th width="140">Score</th>
|
||||||
|
<th width="80" class="text-center">ROI</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for resource in top_performers %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<code class="me-2">{{ resource.resource_value }}</code>
|
||||||
|
<a href="{{ url_for('resource_history', resource_id=resource.id) }}"
|
||||||
|
class="resource-link" title="Historie anzeigen">
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
{% if resource.resource_type == 'domain' %}🌐{% elif resource.resource_type == 'ipv4' %}🖥️{% else %}📱{% endif %}
|
||||||
|
{{ resource.resource_type|upper }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="progress progress-custom">
|
||||||
|
<div class="progress-bar bg-success"
|
||||||
|
style="width: {{ resource.avg_score }}%">
|
||||||
|
{{ "%.1f"|format(resource.avg_score) }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-success">
|
||||||
|
{{ "%.2f"|format(resource.roi) }}x
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not top_performers %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-muted py-4">
|
||||||
|
Keine Performance-Daten verfügbar
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">⚠️ Problematische Ressourcen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table performance-table mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Ressource</th>
|
||||||
|
<th width="80">Typ</th>
|
||||||
|
<th width="100" class="text-center">Probleme</th>
|
||||||
|
<th width="120">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for resource in problem_resources %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<code class="me-2">{{ resource.resource_value }}</code>
|
||||||
|
<a href="{{ url_for('resource_history', resource_id=resource.id) }}"
|
||||||
|
class="resource-link" title="Historie anzeigen">
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
{% if resource.resource_type == 'domain' %}🌐{% elif resource.resource_type == 'ipv4' %}🖥️{% else %}📱{% endif %}
|
||||||
|
{{ resource.resource_type|upper }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
{{ resource.total_issues }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if resource.status == 'quarantine' %}
|
||||||
|
<span class="status-badge bg-warning text-dark">
|
||||||
|
⚠️ Quarantäne
|
||||||
|
</span>
|
||||||
|
{% elif resource.status == 'allocated' %}
|
||||||
|
<span class="status-badge bg-primary text-white">
|
||||||
|
🔗 Zugeteilt
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge bg-success text-white">
|
||||||
|
✅ Verfügbar
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if not problem_resources %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-muted py-4">
|
||||||
|
Keine problematischen Ressourcen gefunden
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trend Chart -->
|
||||||
|
<div class="card chart-card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">📈 30-Tage Performance Trend</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="trendChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Chart defaults
|
||||||
|
Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
|
|
||||||
|
// Performance by Type Chart
|
||||||
|
const performanceCtx = document.getElementById('performanceByTypeChart').getContext('2d');
|
||||||
|
new Chart(performanceCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: {{ performance_by_type|map(attribute=0)|list|tojson }},
|
||||||
|
datasets: [{
|
||||||
|
label: 'Durchschnittliche Performance',
|
||||||
|
data: {{ performance_by_type|map(attribute=1)|list|tojson }},
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(33, 150, 243, 0.8)',
|
||||||
|
'rgba(156, 39, 176, 0.8)',
|
||||||
|
'rgba(76, 175, 80, 0.8)'
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgb(33, 150, 243)',
|
||||||
|
'rgb(156, 39, 176)',
|
||||||
|
'rgb(76, 175, 80)'
|
||||||
|
],
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utilization Chart
|
||||||
|
const utilizationCtx = document.getElementById('utilizationChart').getContext('2d');
|
||||||
|
new Chart(utilizationCtx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: {{ utilization_data|map(attribute='type')|list|tojson }},
|
||||||
|
datasets: [{
|
||||||
|
data: {{ utilization_data|map(attribute='allocated_percent')|list|tojson }},
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(33, 150, 243, 0.8)',
|
||||||
|
'rgba(156, 39, 176, 0.8)',
|
||||||
|
'rgba(76, 175, 80, 0.8)'
|
||||||
|
],
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
padding: 20,
|
||||||
|
usePointStyle: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return context.label + ': ' + context.parsed + '% ausgelastet';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trend Chart
|
||||||
|
const trendCtx = document.getElementById('trendChart').getContext('2d');
|
||||||
|
new Chart(trendCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ daily_metrics|map(attribute='date')|list|tojson }},
|
||||||
|
datasets: [{
|
||||||
|
label: 'Performance Score',
|
||||||
|
data: {{ daily_metrics|map(attribute='performance')|list|tojson }},
|
||||||
|
borderColor: 'rgb(76, 175, 80)',
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
borderWidth: 3,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
yAxisID: 'y',
|
||||||
|
}, {
|
||||||
|
label: 'Probleme',
|
||||||
|
data: {{ daily_metrics|map(attribute='issues')|list|tojson }},
|
||||||
|
borderColor: 'rgb(244, 67, 54)',
|
||||||
|
backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
borderWidth: 3,
|
||||||
|
pointRadius: 4,
|
||||||
|
pointHoverRadius: 6,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
padding: 20,
|
||||||
|
usePointStyle: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Performance %'
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Anzahl Probleme'
|
||||||
|
},
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
212
v2_adminpanel/templates/resource_report.html
Normale Datei
212
v2_adminpanel/templates/resource_report.html
Normale Datei
@@ -0,0 +1,212 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Resource Report Generator{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>Resource Report Generator</h1>
|
||||||
|
<a href="{{ url_for('resources') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Report-Einstellungen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="{{ url_for('resources_report') }}">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="report_type" class="form-label">Report-Typ</label>
|
||||||
|
<select name="type" id="report_type" class="form-select" required>
|
||||||
|
<option value="usage">Auslastungsreport</option>
|
||||||
|
<option value="performance">Performance-Report</option>
|
||||||
|
<option value="compliance">Compliance-Report</option>
|
||||||
|
<option value="inventory">Bestands-Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="format" class="form-label">Export-Format</label>
|
||||||
|
<select name="format" id="format" class="form-select" required>
|
||||||
|
<option value="excel">Excel (.xlsx)</option>
|
||||||
|
<option value="csv">CSV (.csv)</option>
|
||||||
|
<option value="pdf">PDF (Vorschau)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="date_from" class="form-label">Von</label>
|
||||||
|
<input type="date" name="from" id="date_from" class="form-control"
|
||||||
|
value="{{ (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="date_to" class="form-label">Bis</label>
|
||||||
|
<input type="date" name="to" id="date_to" class="form-control"
|
||||||
|
value="{{ datetime.now().strftime('%Y-%m-%d') }}" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h6>Report-Beschreibungen:</h6>
|
||||||
|
<div id="report_descriptions">
|
||||||
|
<div class="alert alert-info report-desc" data-type="usage">
|
||||||
|
<h6><i class="fas fa-chart-line"></i> Auslastungsreport</h6>
|
||||||
|
<p class="mb-0">Zeigt die Nutzung aller Ressourcen im gewählten Zeitraum.
|
||||||
|
Enthält Allokations-Historie, durchschnittliche Auslastung und Trends.</p>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning report-desc" data-type="performance" style="display: none;">
|
||||||
|
<h6><i class="fas fa-tachometer-alt"></i> Performance-Report</h6>
|
||||||
|
<p class="mb-0">Analysiert die Performance-Metriken aller Ressourcen.
|
||||||
|
Enthält ROI-Berechnungen, Issue-Tracking und Performance-Scores.</p>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-success report-desc" data-type="compliance" style="display: none;">
|
||||||
|
<h6><i class="fas fa-shield-alt"></i> Compliance-Report</h6>
|
||||||
|
<p class="mb-0">Überprüft Compliance-Aspekte wie Quarantäne-Gründe,
|
||||||
|
Sicherheitsvorfälle und Policy-Verletzungen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-primary report-desc" data-type="inventory" style="display: none;">
|
||||||
|
<h6><i class="fas fa-boxes"></i> Bestands-Report</h6>
|
||||||
|
<p class="mb-0">Aktueller Bestand aller Ressourcen mit Status-Übersicht,
|
||||||
|
Verfügbarkeit und Zuordnungen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-4">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="previewReport()">
|
||||||
|
<i class="fas fa-eye"></i> Vorschau
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary" name="download" value="true">
|
||||||
|
<i class="fas fa-download"></i> Report generieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Letzte Reports -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Letzte generierte Reports</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h6 class="mb-1">Auslastungsreport_2025-06-01.xlsx</h6>
|
||||||
|
<small>vor 5 Tagen</small>
|
||||||
|
</div>
|
||||||
|
<p class="mb-1">Zeitraum: 01.05.2025 - 01.06.2025</p>
|
||||||
|
<small>Generiert von: {{ username }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vorschau Modal -->
|
||||||
|
<div class="modal fade" id="previewModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Report-Vorschau</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="previewContent">
|
||||||
|
<p class="text-center">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> Lade Vorschau...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Report-Typ Beschreibungen
|
||||||
|
document.getElementById('report_type').addEventListener('change', function() {
|
||||||
|
const selectedType = this.value;
|
||||||
|
document.querySelectorAll('.report-desc').forEach(desc => {
|
||||||
|
desc.style.display = desc.dataset.type === selectedType ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Datum-Validierung
|
||||||
|
document.getElementById('date_from').addEventListener('change', function() {
|
||||||
|
document.getElementById('date_to').min = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('date_to').addEventListener('change', function() {
|
||||||
|
document.getElementById('date_from').max = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Report-Vorschau
|
||||||
|
function previewReport() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||||
|
const content = document.getElementById('previewContent');
|
||||||
|
|
||||||
|
// Simuliere Lade-Vorgang
|
||||||
|
content.innerHTML = '<p class="text-center"><i class="fas fa-spinner fa-spin"></i> Generiere Vorschau...</p>';
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Beispiel-Vorschau
|
||||||
|
content.innerHTML = `
|
||||||
|
<h5>Report-Vorschau</h5>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Ressourcentyp</th>
|
||||||
|
<th>Gesamt</th>
|
||||||
|
<th>Verfügbar</th>
|
||||||
|
<th>Zugeteilt</th>
|
||||||
|
<th>Auslastung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Domain</td>
|
||||||
|
<td>150</td>
|
||||||
|
<td>45</td>
|
||||||
|
<td>100</td>
|
||||||
|
<td>66.7%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IPv4</td>
|
||||||
|
<td>100</td>
|
||||||
|
<td>20</td>
|
||||||
|
<td>75</td>
|
||||||
|
<td>75.0%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Phone</td>
|
||||||
|
<td>50</td>
|
||||||
|
<td>15</td>
|
||||||
|
<td>30</td>
|
||||||
|
<td>60.0%</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="text-muted">Dies ist eine vereinfachte Vorschau. Der vollständige Report enthält weitere Details.</p>
|
||||||
|
`;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisierung
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const today = new Date();
|
||||||
|
const thirtyDaysAgo = new Date(today);
|
||||||
|
thirtyDaysAgo.setDate(today.getDate() - 30);
|
||||||
|
|
||||||
|
document.getElementById('date_from').value = thirtyDaysAgo.toISOString().split('T')[0];
|
||||||
|
document.getElementById('date_to').value = today.toISOString().split('T')[0];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
600
v2_adminpanel/templates/resources.html
Normale Datei
600
v2_adminpanel/templates/resources.html
Normale Datei
@@ -0,0 +1,600 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Resource Pool{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Statistik-Karten Design wie Dashboard */
|
||||||
|
.stat-card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.stat-card .card-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.stat-card .card-value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.stat-card .card-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource Type Icons */
|
||||||
|
.resource-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.resource-icon.domain {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
.resource-icon.ipv4 {
|
||||||
|
background-color: #f3e5f5;
|
||||||
|
color: #7b1fa2;
|
||||||
|
}
|
||||||
|
.resource-icon.phone {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.status-available {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.status-allocated {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
.status-quarantine {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar Custom */
|
||||||
|
.progress-custom {
|
||||||
|
height: 25px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.progress-bar-custom {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Styling */
|
||||||
|
.table-custom {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.table-custom thead th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #495057;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table-custom tbody tr {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.table-custom tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.table-custom td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.btn-action {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-action:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy Button */
|
||||||
|
.copy-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.copy-btn.copied {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Card */
|
||||||
|
.filter-card {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-0">Resource Pool</h1>
|
||||||
|
<p class="text-muted mb-0">Verwalten Sie Domains, IPs und Telefonnummern</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('add_resources') }}" class="btn btn-success">
|
||||||
|
➕ Ressourcen hinzufügen
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('resources_metrics') }}" class="btn btn-info">
|
||||||
|
📊 Metriken
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('resources_report') }}" class="btn btn-secondary">
|
||||||
|
📄 Report
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistik-Karten -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
{% for type, data in stats.items() %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card stat-card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="card-icon">
|
||||||
|
{% if type == 'domain' %}
|
||||||
|
🌐
|
||||||
|
{% elif type == 'ipv4' %}
|
||||||
|
🖥️
|
||||||
|
{% else %}
|
||||||
|
📱
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h5 class="text-muted mb-2">{{ type|upper }}</h5>
|
||||||
|
<div class="card-value text-primary">{{ data.available }}</div>
|
||||||
|
<div class="card-label text-muted mb-3">von {{ data.total }} verfügbar</div>
|
||||||
|
|
||||||
|
<div class="progress progress-custom">
|
||||||
|
<div class="progress-bar bg-success progress-bar-custom"
|
||||||
|
style="width: {{ data.available_percent }}%"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="{{ data.available }} verfügbar">
|
||||||
|
{{ data.available_percent }}%
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar bg-info progress-bar-custom"
|
||||||
|
style="width: {{ (data.allocated / data.total * 100) if data.total > 0 else 0 }}%"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="{{ data.allocated }} zugeteilt">
|
||||||
|
{% if data.allocated > 0 %}{{ data.allocated }}{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar bg-warning progress-bar-custom"
|
||||||
|
style="width: {{ (data.quarantine / data.total * 100) if data.total > 0 else 0 }}%"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="{{ data.quarantine }} in Quarantäne">
|
||||||
|
{% if data.quarantine > 0 %}{{ data.quarantine }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
{% if data.available_percent < 20 %}
|
||||||
|
<span class="badge bg-danger">⚠️ Niedriger Bestand</span>
|
||||||
|
{% elif data.available_percent < 50 %}
|
||||||
|
<span class="badge bg-warning text-dark">⚡ Bestand prüfen</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">✅ Gut gefüllt</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="card filter-card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" action="{{ url_for('resources') }}" id="filterForm">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="type" class="form-label">🏷️ Typ</label>
|
||||||
|
<select name="type" id="type" class="form-select">
|
||||||
|
<option value="">Alle Typen</option>
|
||||||
|
<option value="domain" {% if resource_type == 'domain' %}selected{% endif %}>🌐 Domain</option>
|
||||||
|
<option value="ipv4" {% if resource_type == 'ipv4' %}selected{% endif %}>🖥️ IPv4</option>
|
||||||
|
<option value="phone" {% if resource_type == 'phone' %}selected{% endif %}>📱 Telefon</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="status" class="form-label">📊 Status</label>
|
||||||
|
<select name="status" id="status" class="form-select">
|
||||||
|
<option value="">Alle Status</option>
|
||||||
|
<option value="available" {% if status_filter == 'available' %}selected{% endif %}>✅ Verfügbar</option>
|
||||||
|
<option value="allocated" {% if status_filter == 'allocated' %}selected{% endif %}>🔗 Zugeteilt</option>
|
||||||
|
<option value="quarantine" {% if status_filter == 'quarantine' %}selected{% endif %}>⚠️ Quarantäne</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="search" class="form-label">🔍 Suche</label>
|
||||||
|
<input type="text" name="search" id="search" class="form-control"
|
||||||
|
placeholder="Ressource suchen..." value="{{ search }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<a href="{{ url_for('resources') }}" class="btn btn-secondary w-100">
|
||||||
|
🔄 Zurücksetzen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ressourcen-Tabelle -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">📋 Ressourcen-Liste</h5>
|
||||||
|
<span class="badge bg-secondary">{{ total }} Einträge</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if resources %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-custom mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="80">ID</th>
|
||||||
|
<th width="120">Typ</th>
|
||||||
|
<th>Ressource</th>
|
||||||
|
<th width="140">Status</th>
|
||||||
|
<th>Zugewiesen an</th>
|
||||||
|
<th width="180">Letzte Änderung</th>
|
||||||
|
<th width="140" class="text-center">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for resource in resources %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="text-muted">#{{ resource[0] }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="resource-icon {{ resource[1] }}">
|
||||||
|
{% if resource[1] == 'domain' %}
|
||||||
|
🌐
|
||||||
|
{% elif resource[1] == 'ipv4' %}
|
||||||
|
🖥️
|
||||||
|
{% else %}
|
||||||
|
📱
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<code class="me-2">{{ resource[2] }}</code>
|
||||||
|
<button class="copy-btn" onclick="copyToClipboard('{{ resource[2] }}', this)"
|
||||||
|
title="Kopieren">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if resource[3] == 'available' %}
|
||||||
|
<span class="status-badge status-available">
|
||||||
|
✅ Verfügbar
|
||||||
|
</span>
|
||||||
|
{% elif resource[3] == 'allocated' %}
|
||||||
|
<span class="status-badge status-allocated">
|
||||||
|
🔗 Zugeteilt
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge status-quarantine">
|
||||||
|
⚠️ Quarantäne
|
||||||
|
</span>
|
||||||
|
{% if resource[8] %}
|
||||||
|
<div class="small text-muted mt-1">{{ resource[8] }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if resource[5] %}
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('edit_license', license_id=resource[4]) }}"
|
||||||
|
class="text-decoration-none">
|
||||||
|
<strong>{{ resource[5] }}</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">{{ resource[6] }}</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if resource[7] %}
|
||||||
|
<div class="small">
|
||||||
|
<div>{{ resource[7].strftime('%d.%m.%Y') }}</div>
|
||||||
|
<div class="text-muted">{{ resource[7].strftime('%H:%M Uhr') }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ url_for('resource_history', resource_id=resource[0]) }}"
|
||||||
|
class="btn btn-action btn-sm btn-outline-info"
|
||||||
|
title="Historie anzeigen">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if resource[3] == 'available' %}
|
||||||
|
<button class="btn btn-action btn-sm btn-outline-warning"
|
||||||
|
onclick="showQuarantineModal({{ resource[0] }})"
|
||||||
|
title="In Quarantäne setzen">
|
||||||
|
<i class="fas fa-ban"></i>
|
||||||
|
</button>
|
||||||
|
{% elif resource[3] == 'quarantine' %}
|
||||||
|
<form method="post" action="{{ url_for('release_resources') }}"
|
||||||
|
style="display: inline;">
|
||||||
|
<input type="hidden" name="resource_ids[]" value="{{ resource[0] }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-action btn-sm btn-outline-success"
|
||||||
|
title="Ressource freigeben">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fas fa-inbox"></i>
|
||||||
|
<h4>Keine Ressourcen gefunden</h4>
|
||||||
|
<p>Ändern Sie Ihre Filterkriterien oder fügen Sie neue Ressourcen hinzu.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<nav class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('resources', page=1, type=resource_type, status=status_filter, search=search) }}">
|
||||||
|
<i class="fas fa-angle-double-left"></i> Erste
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item {% if page == 1 %}disabled{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('resources', page=page-1, type=resource_type, status=status_filter, search=search) }}">
|
||||||
|
<i class="fas fa-angle-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
{% if p == page or (p >= page - 2 and p <= page + 2) %}
|
||||||
|
<li class="page-item {% if p == page %}active{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('resources', page=p, type=resource_type, status=status_filter, search=search) }}">
|
||||||
|
{{ p }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('resources', page=page+1, type=resource_type, status=status_filter, search=search) }}">
|
||||||
|
Weiter <i class="fas fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('resources', page=total_pages, type=resource_type, status=status_filter, search=search) }}">
|
||||||
|
Letzte <i class="fas fa-angle-double-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Kürzliche Aktivitäten -->
|
||||||
|
{% if recent_activities %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header bg-white">
|
||||||
|
<h5 class="mb-0">⏰ Kürzliche Aktivitäten</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="timeline">
|
||||||
|
{% for activity in recent_activities %}
|
||||||
|
<div class="d-flex mb-3">
|
||||||
|
<div class="me-3">
|
||||||
|
{% if activity[0] == 'created' %}
|
||||||
|
<span class="badge bg-success rounded-pill">➕</span>
|
||||||
|
{% elif activity[0] == 'allocated' %}
|
||||||
|
<span class="badge bg-info rounded-pill">🔗</span>
|
||||||
|
{% elif activity[0] == 'deallocated' %}
|
||||||
|
<span class="badge bg-secondary rounded-pill">🔓</span>
|
||||||
|
{% elif activity[0] == 'quarantined' %}
|
||||||
|
<span class="badge bg-warning rounded-pill">⚠️</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-primary rounded-pill">ℹ️</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<strong>{{ activity[4] }}</strong> ({{ activity[3] }}) - {{ activity[0] }}
|
||||||
|
{% if activity[1] %}
|
||||||
|
<span class="text-muted">von {{ activity[1] }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ activity[2].strftime('%d.%m.%Y %H:%M') if activity[2] else '' }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quarantäne Modal -->
|
||||||
|
<div class="modal fade" id="quarantineModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" id="quarantineForm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">⚠️ Ressource in Quarantäne setzen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="reason" class="form-label">Grund</label>
|
||||||
|
<select name="reason" id="reason" class="form-select" required>
|
||||||
|
<option value="">Bitte wählen...</option>
|
||||||
|
<option value="review">🔍 Überprüfung</option>
|
||||||
|
<option value="abuse">⚠️ Missbrauch</option>
|
||||||
|
<option value="defect">❌ Defekt</option>
|
||||||
|
<option value="maintenance">🔧 Wartung</option>
|
||||||
|
<option value="blacklisted">🚫 Blacklisted</option>
|
||||||
|
<option value="expired">⏰ Abgelaufen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="until_date" class="form-label">Bis wann? (optional)</label>
|
||||||
|
<input type="date" name="until_date" id="until_date" class="form-control"
|
||||||
|
min="{{ datetime.now().strftime('%Y-%m-%d') }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="notes" class="form-label">Notizen</label>
|
||||||
|
<textarea name="notes" id="notes" class="form-control" rows="3"
|
||||||
|
placeholder="Zusätzliche Informationen..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
⚠️ In Quarantäne setzen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Live-Filtering
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.getElementById('filterForm');
|
||||||
|
const inputs = form.querySelectorAll('select, input[type="text"]');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
if (input.type === 'text') {
|
||||||
|
let timeout;
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => form.submit(), 300);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
input.addEventListener('change', () => form.submit());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bootstrap Tooltips initialisieren
|
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy to Clipboard mit besserem Feedback
|
||||||
|
function copyToClipboard(text, button) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
// Button Icon ändern
|
||||||
|
const icon = button.querySelector('i');
|
||||||
|
icon.classList.remove('fa-copy');
|
||||||
|
icon.classList.add('fa-check');
|
||||||
|
button.classList.add('copied');
|
||||||
|
|
||||||
|
// Nach 2 Sekunden zurücksetzen
|
||||||
|
setTimeout(() => {
|
||||||
|
icon.classList.remove('fa-check');
|
||||||
|
icon.classList.add('fa-copy');
|
||||||
|
button.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quarantäne Modal
|
||||||
|
function showQuarantineModal(resourceId) {
|
||||||
|
const modalElement = document.getElementById('quarantineModal');
|
||||||
|
const modal = new bootstrap.Modal(modalElement);
|
||||||
|
const form = modalElement.querySelector('form');
|
||||||
|
form.setAttribute('action', `/resources/quarantine/${resourceId}`);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
321
v2_adminpanel/test_data_resources.sql
Normale Datei
321
v2_adminpanel/test_data_resources.sql
Normale Datei
@@ -0,0 +1,321 @@
|
|||||||
|
-- Test-Daten für Resource Pool System
|
||||||
|
-- Generiert für AccountForger v2-Docker
|
||||||
|
-- Stand: 2025-06-09
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Test-Domains (500 Stück)
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Verfügbare Domains
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, notes) VALUES
|
||||||
|
('domain', 'example-shop-001.com', 'available', 'Premium Domain'),
|
||||||
|
('domain', 'best-deals-online.net', 'available', 'E-Commerce Domain'),
|
||||||
|
('domain', 'super-store-24.com', 'available', 'Shop Domain'),
|
||||||
|
('domain', 'mega-market-place.org', 'available', 'Marketplace Domain'),
|
||||||
|
('domain', 'discount-heaven.net', 'available', 'Discount Domain'),
|
||||||
|
('domain', 'fashion-outlet-now.com', 'available', 'Fashion Domain'),
|
||||||
|
('domain', 'tech-gadgets-pro.net', 'available', 'Tech Domain'),
|
||||||
|
('domain', 'home-decor-style.com', 'available', 'Home Domain'),
|
||||||
|
('domain', 'sports-gear-central.net', 'available', 'Sports Domain'),
|
||||||
|
('domain', 'beauty-products-24.com', 'available', 'Beauty Domain');
|
||||||
|
|
||||||
|
-- Weitere Domains mit verschiedenen Patterns
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 11..400 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status)
|
||||||
|
VALUES ('domain',
|
||||||
|
CASE
|
||||||
|
WHEN i % 5 = 0 THEN 'shop-' || i || '-online.com'
|
||||||
|
WHEN i % 5 = 1 THEN 'store-' || i || '-pro.net'
|
||||||
|
WHEN i % 5 = 2 THEN 'market-' || i || '-24.org'
|
||||||
|
WHEN i % 5 = 3 THEN 'outlet-' || i || '-deals.com'
|
||||||
|
ELSE 'commerce-' || i || '-now.net'
|
||||||
|
END,
|
||||||
|
'available');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Einige zugeteilte Domains (50 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 401..450 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, allocated_to_license, status_changed_at, status_changed_by)
|
||||||
|
VALUES ('domain',
|
||||||
|
'allocated-domain-' || i || '.com',
|
||||||
|
'allocated',
|
||||||
|
(i % 10) + 1, -- Zuweisung zu Lizenzen 1-10
|
||||||
|
NOW() - INTERVAL '30 days' * RANDOM(),
|
||||||
|
'admin');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Domains in Quarantäne (50 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 451..500 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, quarantine_reason, quarantine_until, notes)
|
||||||
|
VALUES ('domain',
|
||||||
|
'quarantine-domain-' || i || '.com',
|
||||||
|
'quarantine',
|
||||||
|
CASE i % 5
|
||||||
|
WHEN 0 THEN 'abuse'
|
||||||
|
WHEN 1 THEN 'defect'
|
||||||
|
WHEN 2 THEN 'blacklisted'
|
||||||
|
WHEN 3 THEN 'expired'
|
||||||
|
ELSE 'review'
|
||||||
|
END,
|
||||||
|
NOW() + INTERVAL '7 days' + INTERVAL '1 day' * (i % 14),
|
||||||
|
'Automatisch in Quarantäne versetzt');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Test IPv4-Adressen (200 Stück)
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Verfügbare IPv4-Adressen
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 1..150 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, notes)
|
||||||
|
VALUES ('ipv4',
|
||||||
|
'192.168.' || (i / 256 + 1)::INT || '.' || (i % 256),
|
||||||
|
'available',
|
||||||
|
CASE
|
||||||
|
WHEN i % 10 = 0 THEN 'Premium IP'
|
||||||
|
WHEN i % 20 = 0 THEN 'Dedicated Server IP'
|
||||||
|
ELSE 'Standard IP'
|
||||||
|
END);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Zugeteilte IPv4-Adressen (30 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 151..180 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, allocated_to_license, status_changed_at, status_changed_by)
|
||||||
|
VALUES ('ipv4',
|
||||||
|
'10.0.' || ((i-150) / 256)::INT || '.' || ((i-150) % 256),
|
||||||
|
'allocated',
|
||||||
|
((i-150) % 15) + 1,
|
||||||
|
NOW() - INTERVAL '60 days' * RANDOM(),
|
||||||
|
'system');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- IPv4 in Quarantäne (20 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 181..200 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, quarantine_reason, quarantine_until, notes)
|
||||||
|
VALUES ('ipv4',
|
||||||
|
'172.16.' || ((i-180) / 256)::INT || '.' || ((i-180) % 256),
|
||||||
|
'quarantine',
|
||||||
|
CASE (i-180) % 4
|
||||||
|
WHEN 0 THEN 'blacklisted'
|
||||||
|
WHEN 1 THEN 'abuse'
|
||||||
|
WHEN 2 THEN 'maintenance'
|
||||||
|
ELSE 'defect'
|
||||||
|
END,
|
||||||
|
NOW() + INTERVAL '3 days' + INTERVAL '1 day' * ((i-180) % 7),
|
||||||
|
'IP wurde gemeldet oder ist in Wartung');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Test Telefonnummern (100 Stück)
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Verfügbare Telefonnummern
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 1..70 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, notes)
|
||||||
|
VALUES ('phone',
|
||||||
|
'+49' || (1500000000 + i),
|
||||||
|
'available',
|
||||||
|
CASE
|
||||||
|
WHEN i % 5 = 0 THEN 'Premium Nummer'
|
||||||
|
WHEN i % 10 = 0 THEN 'Vanity Nummer'
|
||||||
|
ELSE 'Standard Nummer'
|
||||||
|
END);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Zugeteilte Telefonnummern (20 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 71..90 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, allocated_to_license, status_changed_at, status_changed_by)
|
||||||
|
VALUES ('phone',
|
||||||
|
'+49' || (1600000000 + i),
|
||||||
|
'allocated',
|
||||||
|
((i-70) % 10) + 1,
|
||||||
|
NOW() - INTERVAL '45 days' * RANDOM(),
|
||||||
|
'admin');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Telefonnummern in Quarantäne (10 Stück)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
FOR i IN 91..100 LOOP
|
||||||
|
INSERT INTO resource_pools (resource_type, resource_value, status, quarantine_reason, quarantine_until, notes)
|
||||||
|
VALUES ('phone',
|
||||||
|
'+49' || (1700000000 + i),
|
||||||
|
'quarantine',
|
||||||
|
CASE (i-90) % 3
|
||||||
|
WHEN 0 THEN 'expired'
|
||||||
|
WHEN 1 THEN 'defect'
|
||||||
|
ELSE 'review'
|
||||||
|
END,
|
||||||
|
NOW() + INTERVAL '5 days' + INTERVAL '1 day' * ((i-90) % 5),
|
||||||
|
'Nummer wurde zurückgegeben oder ist defekt');
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Resource History für einige Ressourcen
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Historie für einige Domains
|
||||||
|
INSERT INTO resource_history (resource_id, license_id, action, action_by, details, ip_address)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
allocated_to_license,
|
||||||
|
'allocated',
|
||||||
|
'system',
|
||||||
|
'{"reason": "Neue Lizenz erstellt", "customer": "Test Customer"}'::jsonb,
|
||||||
|
'192.168.1.100'
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE status = 'allocated'
|
||||||
|
AND resource_type = 'domain'
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
-- Historie für Quarantäne
|
||||||
|
INSERT INTO resource_history (resource_id, action, action_by, details, ip_address)
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
'quarantined',
|
||||||
|
'admin',
|
||||||
|
jsonb_build_object('reason', quarantine_reason, 'duration', '7 days'),
|
||||||
|
'192.168.1.101'
|
||||||
|
FROM resource_pools
|
||||||
|
WHERE status = 'quarantine'
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- Resource Metrics für Performance-Tracking
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Metriken für die letzten 30 Tage
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
resource_rec RECORD;
|
||||||
|
days_back INT;
|
||||||
|
BEGIN
|
||||||
|
-- Für jede Ressource, die allocated ist
|
||||||
|
FOR resource_rec IN
|
||||||
|
SELECT id FROM resource_pools WHERE status = 'allocated' LIMIT 50
|
||||||
|
LOOP
|
||||||
|
-- Generiere Metriken für die letzten 30 Tage
|
||||||
|
FOR days_back IN 0..29 LOOP
|
||||||
|
INSERT INTO resource_metrics (
|
||||||
|
resource_id,
|
||||||
|
metric_date,
|
||||||
|
usage_count,
|
||||||
|
performance_score,
|
||||||
|
cost,
|
||||||
|
revenue,
|
||||||
|
issues_count,
|
||||||
|
availability_percent
|
||||||
|
) VALUES (
|
||||||
|
resource_rec.id,
|
||||||
|
CURRENT_DATE - INTERVAL '1 day' * days_back,
|
||||||
|
FLOOR(RANDOM() * 100 + 50), -- 50-150 Nutzungen
|
||||||
|
ROUND((RANDOM() * 40 + 60)::numeric, 2), -- 60-100 Score
|
||||||
|
ROUND((RANDOM() * 5 + 2)::numeric, 2), -- 2-7 EUR Kosten
|
||||||
|
ROUND((RANDOM() * 20 + 10)::numeric, 2), -- 10-30 EUR Umsatz
|
||||||
|
FLOOR(RANDOM() * 3), -- 0-2 Issues
|
||||||
|
ROUND((RANDOM() * 5 + 95)::numeric, 2) -- 95-100% Verfügbarkeit
|
||||||
|
) ON CONFLICT (resource_id, metric_date) DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ====================================
|
||||||
|
-- License Resources Zuordnungen
|
||||||
|
-- ====================================
|
||||||
|
|
||||||
|
-- Verbinde einige bestehende Lizenzen mit Ressourcen
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
license_rec RECORD;
|
||||||
|
domain_id INT;
|
||||||
|
ipv4_id INT;
|
||||||
|
phone_id INT;
|
||||||
|
BEGIN
|
||||||
|
-- Für die ersten 10 aktiven Lizenzen
|
||||||
|
FOR license_rec IN
|
||||||
|
SELECT id FROM licenses WHERE is_active = TRUE LIMIT 10
|
||||||
|
LOOP
|
||||||
|
-- Hole eine verfügbare Domain
|
||||||
|
SELECT id INTO domain_id FROM resource_pools
|
||||||
|
WHERE resource_type = 'domain' AND status = 'available'
|
||||||
|
ORDER BY RANDOM() LIMIT 1;
|
||||||
|
|
||||||
|
IF domain_id IS NOT NULL THEN
|
||||||
|
-- Weise die Domain zu
|
||||||
|
UPDATE resource_pools
|
||||||
|
SET status = 'allocated',
|
||||||
|
allocated_to_license = license_rec.id,
|
||||||
|
status_changed_at = NOW(),
|
||||||
|
status_changed_by = 'migration'
|
||||||
|
WHERE id = domain_id;
|
||||||
|
|
||||||
|
-- Erstelle Zuordnung
|
||||||
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||||
|
VALUES (license_rec.id, domain_id, 'migration');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Wiederhole für IPv4
|
||||||
|
SELECT id INTO ipv4_id FROM resource_pools
|
||||||
|
WHERE resource_type = 'ipv4' AND status = 'available'
|
||||||
|
ORDER BY RANDOM() LIMIT 1;
|
||||||
|
|
||||||
|
IF ipv4_id IS NOT NULL THEN
|
||||||
|
UPDATE resource_pools
|
||||||
|
SET status = 'allocated',
|
||||||
|
allocated_to_license = license_rec.id,
|
||||||
|
status_changed_at = NOW(),
|
||||||
|
status_changed_by = 'migration'
|
||||||
|
WHERE id = ipv4_id;
|
||||||
|
|
||||||
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
||||||
|
VALUES (license_rec.id, ipv4_id, 'migration');
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Zusammenfassung ausgeben
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
domain_count INT;
|
||||||
|
ipv4_count INT;
|
||||||
|
phone_count INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO domain_count FROM resource_pools WHERE resource_type = 'domain';
|
||||||
|
SELECT COUNT(*) INTO ipv4_count FROM resource_pools WHERE resource_type = 'ipv4';
|
||||||
|
SELECT COUNT(*) INTO phone_count FROM resource_pools WHERE resource_type = 'phone';
|
||||||
|
|
||||||
|
RAISE NOTICE 'Test-Daten erfolgreich eingefügt:';
|
||||||
|
RAISE NOTICE '- Domains: %', domain_count;
|
||||||
|
RAISE NOTICE '- IPv4-Adressen: %', ipv4_count;
|
||||||
|
RAISE NOTICE '- Telefonnummern: %', phone_count;
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'Status-Verteilung:';
|
||||||
|
RAISE NOTICE '- Verfügbar: % Ressourcen', (SELECT COUNT(*) FROM resource_pools WHERE status = 'available');
|
||||||
|
RAISE NOTICE '- Zugeteilt: % Ressourcen', (SELECT COUNT(*) FROM resource_pools WHERE status = 'allocated');
|
||||||
|
RAISE NOTICE '- Quarantäne: % Ressourcen', (SELECT COUNT(*) FROM resource_pools WHERE status = 'quarantine');
|
||||||
|
END $$;
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren