Dieser Commit ist enthalten in:
2025-06-09 04:09:59 +02:00
Ursprung f124b5a5fd
Commit 888d27442c
17 geänderte Dateien mit 4874 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -49,7 +49,9 @@
"Bash(openssl x509:*)",
"Bash(cat:*)",
"Bash(openssl dhparam:*)",
"Bash(rg:*)"
"Bash(rg:*)",
"Bash(docker cp:*)",
"Bash(docker-compose:*)"
],
"deny": []
}

Datei anzeigen

@@ -1033,7 +1033,7 @@ Die Session-Daten werden erst gefüllt, wenn der License Server API implementier
- ✅ Audit-Log-Integration
- ✅ Navigation aktualisiert
## 2025-01-06: Implementierung Searchable Dropdown für Kundenauswahl
## 2025-06-06: Implementierung Searchable Dropdown für Kundenauswahl
**Problem:**
- 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
- ✅ Sowohl Einzellizenz als auch Batch unterstützt
## 2025-01-06: Automatische Ablaufdatum-Berechnung
## 2025-06-06: Automatische Ablaufdatum-Berechnung
**Problem:**
- 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
- ✅ 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:**
- 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
- ✅ 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:**
- 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
- ✅ 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:**
- 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
- ✅ Sicherheit erhöht durch zentrale Verwaltung
### 2025-01-08: Live-Filtering implementiert
### 2025-06-09: Live-Filtering implementiert
**Problem:**
- Benutzer mussten immer auf "Filter anwenden" klicken
@@ -1565,4 +1565,469 @@ ALTER TABLE login_attempts ALTER COLUMN blocked_until TYPE TIMESTAMP WITH TIME Z
**Status:**
- ✅ Live-Filtering auf allen Hauptseiten implementiert
- ✅ 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

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

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

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

Datei anzeigen

@@ -102,3 +102,80 @@ BEGIN
UPDATE licenses SET created_at = valid_from WHERE created_at IS NULL;
END IF;
END $$;
-- ===================== RESOURCE POOL SYSTEM =====================
-- Haupttabelle für den Resource Pool
CREATE TABLE IF NOT EXISTS resource_pools (
id SERIAL PRIMARY KEY,
resource_type VARCHAR(20) NOT NULL CHECK (resource_type IN ('domain', 'ipv4', 'phone')),
resource_value VARCHAR(255) NOT NULL,
status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'allocated', 'quarantine')),
allocated_to_license INTEGER REFERENCES licenses(id) ON DELETE SET NULL,
status_changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
status_changed_by VARCHAR(50),
quarantine_reason VARCHAR(100) CHECK (quarantine_reason IN ('abuse', 'defect', 'maintenance', 'blacklisted', 'expired', 'review', NULL)),
quarantine_until TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
notes TEXT,
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;

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@@ -220,15 +220,23 @@
</style>
</head>
<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">
<a href="/" class="navbar-brand text-decoration-none">🎛️ AccountForger - Admin Panel</a>
<div class="d-flex align-items-center">
<div id="session-timer" class="timer-normal me-3">
⏱️ <span id="timer-display">5:00</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></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>
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
</div>
</div>
</nav>
@@ -247,8 +255,8 @@
</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://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>

Datei anzeigen

@@ -91,6 +91,67 @@
</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">
<button type="submit" class="btn btn-primary btn-lg">
🔑 Batch generieren
@@ -243,6 +304,15 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('customerName').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
@@ -269,5 +339,96 @@ document.getElementById('quantity').addEventListener('input', function(e) {
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>
{% endblock %}

Datei anzeigen

@@ -256,6 +256,67 @@
</div>
{% 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">
<!-- Bald ablaufende Lizenzen -->
<div class="col-md-6">

Datei anzeigen

@@ -75,6 +75,64 @@
</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">
<button type="submit" class="btn btn-primary"> Lizenz erstellen</button>
</div>
@@ -282,6 +340,77 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('customerName').required = false;
document.getElementById('email').required = false;
});
// Resource Availability Check
checkResourceAvailability();
// Event Listener für Resource Count Änderungen
document.getElementById('domainCount').addEventListener('change', checkResourceAvailability);
document.getElementById('ipv4Count').addEventListener('change', checkResourceAvailability);
document.getElementById('phoneCount').addEventListener('change', checkResourceAvailability);
});
// Funktion zur Prüfung der Ressourcen-Verfügbarkeit
function checkResourceAvailability() {
const domainCount = parseInt(document.getElementById('domainCount').value) || 0;
const ipv4Count = parseInt(document.getElementById('ipv4Count').value) || 0;
const phoneCount = parseInt(document.getElementById('phoneCount').value) || 0;
// API-Call zur Verfügbarkeitsprüfung
fetch(`/api/resources/check-availability?domain=${domainCount}&ipv4=${ipv4Count}&phone=${phoneCount}`)
.then(response => response.json())
.then(data => {
// Update der Verfügbarkeitsanzeigen
updateAvailabilityDisplay('domainsAvailable', data.domain_available, domainCount);
updateAvailabilityDisplay('ipv4Available', data.ipv4_available, ipv4Count);
updateAvailabilityDisplay('phoneAvailable', data.phone_available, phoneCount);
// Gesamtstatus aktualisieren
updateResourceStatus(data, domainCount, ipv4Count, phoneCount);
})
.catch(error => {
console.error('Fehler bei Verfügbarkeitsprüfung:', error);
});
}
// Hilfsfunktion zur Anzeige der Verfügbarkeit
function updateAvailabilityDisplay(elementId, available, requested) {
const element = document.getElementById(elementId);
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>
{% endblock %}

Datei anzeigen

@@ -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 %}
&nbsp;&bull;&nbsp; <i class="fas fa-globe"></i> {{ event.ip_address }}
{% endif %}
{% if event.license_id %}
&nbsp;&bull;&nbsp;
<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 %}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@@ -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">&nbsp;</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 %}

Datei anzeigen

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