Trennung Testdaten und Livedaten
Dieser Commit ist enthalten in:
91
JOURNAL.md
91
JOURNAL.md
@@ -2187,3 +2187,94 @@ docker-compose up -d
|
|||||||
- `v2_adminpanel/fix_license_keys.sql` - Korrektur-Script (temporär)
|
- `v2_adminpanel/fix_license_keys.sql` - Korrektur-Script (temporär)
|
||||||
|
|
||||||
**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert
|
**Status:** ✅ Alle Lizenzschlüssel erfolgreich migriert
|
||||||
|
|
||||||
|
### 2025-06-09: Test-Flag für Lizenzen implementiert
|
||||||
|
|
||||||
|
**Ziel:**
|
||||||
|
- Klare Trennung zwischen Testdaten und echten Produktivdaten
|
||||||
|
- Testdaten sollen von der Software ignoriert werden können
|
||||||
|
- Bessere Übersicht im Admin Panel
|
||||||
|
|
||||||
|
**Durchgeführte Änderungen:**
|
||||||
|
|
||||||
|
1. **Datenbank-Schema (init.sql):**
|
||||||
|
- Neue Spalte `is_test BOOLEAN DEFAULT FALSE` zur `licenses` Tabelle hinzugefügt
|
||||||
|
- Migration für bestehende Daten: Alle werden als `is_test = TRUE` markiert
|
||||||
|
- Index `idx_licenses_is_test` für bessere Performance
|
||||||
|
|
||||||
|
2. **Backend (app.py):**
|
||||||
|
- Dashboard-Queries filtern Testdaten mit `WHERE is_test = FALSE` aus
|
||||||
|
- Lizenz-Erstellung: Neues Checkbox-Feld für Test-Markierung
|
||||||
|
- Lizenz-Bearbeitung: Test-Status kann geändert werden
|
||||||
|
- Export: Optional mit/ohne Testdaten (`?include_test=true`)
|
||||||
|
- Bulk-Operationen: Nur auf Live-Daten anwendbar
|
||||||
|
- Neue Filter in Lizenzliste: "🧪 Testdaten" und "🚀 Live-Daten"
|
||||||
|
|
||||||
|
3. **Frontend Templates:**
|
||||||
|
- **index.html**: Checkbox "Als Testdaten markieren" bei Lizenzerstellung
|
||||||
|
- **edit_license.html**: Checkbox zum Ändern des Test-Status
|
||||||
|
- **licenses.html**: Badge 🧪 für Testdaten, neue Filteroptionen
|
||||||
|
- **dashboard.html**: Info-Box zeigt Anzahl der Testdaten
|
||||||
|
- **batch_form.html**: Option für Batch-Test-Lizenzen
|
||||||
|
|
||||||
|
4. **Audit-Log Integration:**
|
||||||
|
- `is_test` Feld wird bei CREATE/UPDATE geloggt
|
||||||
|
- Nachvollziehbarkeit von Test/Live-Status-Änderungen
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
- Testdaten werden in allen Statistiken ausgefiltert
|
||||||
|
- License Server API wird Lizenzen mit `is_test = TRUE` ignorieren
|
||||||
|
- Resource Pool bleibt unverändert (kann Test- und Live-Ressourcen verwalten)
|
||||||
|
|
||||||
|
**Migration der bestehenden Daten:**
|
||||||
|
```sql
|
||||||
|
UPDATE licenses SET is_test = TRUE; -- Alle aktuellen Daten sind Testdaten
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status:** ✅ Implementiert
|
||||||
|
|
||||||
|
### 2025-06-09: Test-Flag für Kunden und Resource Pools erweitert
|
||||||
|
|
||||||
|
**Ziel:**
|
||||||
|
- Konsistentes Test-Daten-Management über alle Entitäten
|
||||||
|
- Kunden und Resource Pools können ebenfalls als Testdaten markiert werden
|
||||||
|
- Automatische Verknüpfung: Test-Kunde → Test-Lizenzen → Test-Ressourcen
|
||||||
|
|
||||||
|
**Durchgeführte Änderungen:**
|
||||||
|
|
||||||
|
1. **Datenbank-Schema erweitert:**
|
||||||
|
- `customers.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
|
||||||
|
- `resource_pools.is_test BOOLEAN DEFAULT FALSE` hinzugefügt
|
||||||
|
- Indizes für bessere Performance erstellt
|
||||||
|
- Migrations in init.sql integriert
|
||||||
|
|
||||||
|
2. **Backend (app.py) - Erweiterte Logik:**
|
||||||
|
- Dashboard: Separate Zähler für Test-Kunden und Test-Ressourcen
|
||||||
|
- Kunde-Erstellung: Erbt Test-Status von Lizenz
|
||||||
|
- Test-Kunde erzwingt Test-Lizenzen
|
||||||
|
- Resource-Zuweisung: Test-Lizenzen bekommen nur Test-Ressourcen
|
||||||
|
- Customer-Management mit is_test Filter
|
||||||
|
|
||||||
|
3. **Frontend Updates:**
|
||||||
|
- **customers.html**: 🧪 Badge für Test-Kunden
|
||||||
|
- **edit_customer.html**: Checkbox für Test-Status
|
||||||
|
- **dashboard.html**: Erweiterte Test-Statistik (Lizenzen, Kunden, Ressourcen)
|
||||||
|
|
||||||
|
4. **Geschäftslogik:**
|
||||||
|
- Wenn neuer Kunde bei Test-Lizenz erstellt wird → automatisch Test-Kunde
|
||||||
|
- Wenn Test-Kunde gewählt wird → Lizenz automatisch als Test markiert
|
||||||
|
- Resource Pool Allocation prüft Test-Status für korrekte Zuweisung
|
||||||
|
|
||||||
|
**Migration der bestehenden Daten:**
|
||||||
|
```sql
|
||||||
|
UPDATE customers SET is_test = TRUE; -- 5 Kunden
|
||||||
|
UPDATE resource_pools SET is_test = TRUE; -- 20 Ressourcen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
- Konsistente Test/Live-Trennung über alle Ebenen
|
||||||
|
- Dashboard-Statistiken zeigen nur Live-Daten
|
||||||
|
- Test-Ressourcen werden nur Test-Lizenzen zugewiesen
|
||||||
|
- Alle bestehenden Daten sind jetzt als Test markiert
|
||||||
|
|
||||||
|
**Status:** ✅ Vollständig implementiert
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# The Road So Far
|
# The Road So Far
|
||||||
Stand: 09.06.2025 - 14:45 Uhr
|
Stand: 09.06.2025 - 15:39 Uhr
|
||||||
|
|
||||||
## 🚀 Aktueller Status
|
## 🚀 Aktueller Status
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Stand: 09.06.2025 - 14:45 Uhr
|
|||||||
3. **Admin Panel (Vollständig implementiert)**
|
3. **Admin Panel (Vollständig implementiert)**
|
||||||
- Flask-Anwendung mit Session-Management
|
- Flask-Anwendung mit Session-Management
|
||||||
- Login für 2 Admin-User (rac00n, w@rh@mm3r)
|
- Login für 2 Admin-User (rac00n, w@rh@mm3r)
|
||||||
- Dashboard mit Statistiken
|
- Dashboard mit Statistiken (ohne Testdaten)
|
||||||
- Komplette CRUD-Funktionalität für Lizenzen und Kunden
|
- Komplette CRUD-Funktionalität für Lizenzen und Kunden
|
||||||
- Session-Tracking und -Verwaltung
|
- Session-Tracking und -Verwaltung
|
||||||
- Audit-Log für alle Aktionen
|
- Audit-Log für alle Aktionen
|
||||||
@@ -33,6 +33,7 @@ Stand: 09.06.2025 - 14:45 Uhr
|
|||||||
- 2FA (Two-Factor Authentication)
|
- 2FA (Two-Factor Authentication)
|
||||||
- Passwort-Änderung
|
- Passwort-Änderung
|
||||||
- Resource Pool Management
|
- Resource Pool Management
|
||||||
|
- **NEU: Test-Flag System für Lizenzen**
|
||||||
|
|
||||||
4. **Internet-Zugriff**
|
4. **Internet-Zugriff**
|
||||||
- Admin Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com ✅
|
- Admin Panel: https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com ✅
|
||||||
@@ -54,6 +55,15 @@ Stand: 09.06.2025 - 14:45 Uhr
|
|||||||
- UI für Management, Historie, Metriken
|
- UI für Management, Historie, Metriken
|
||||||
- Integration in Lizenzerstellung (0-10 Ressourcen pro Typ)
|
- Integration in Lizenzerstellung (0-10 Ressourcen pro Typ)
|
||||||
|
|
||||||
|
7. **Test-Daten Management** (ERWEITERT!)
|
||||||
|
- `is_test` Flag für Lizenzen, Kunden und Resource Pools
|
||||||
|
- Testdaten werden in allen Statistiken ausgefiltert
|
||||||
|
- UI-Kennzeichnung mit 🧪 Badge überall
|
||||||
|
- Filter für "Testdaten" und "Live-Daten"
|
||||||
|
- Automatische Verknüpfung: Test-Kunde → Test-Lizenz → Test-Ressourcen
|
||||||
|
- Konsistente Test/Live-Trennung über alle Ebenen
|
||||||
|
- Alle bestehenden Daten als Test markiert (19 Lizenzen, 5 Kunden, 20 Ressourcen)
|
||||||
|
|
||||||
### ❌ Was noch fehlt
|
### ❌ Was noch fehlt
|
||||||
|
|
||||||
1. **License Server API** (Hauptaufgabe!)
|
1. **License Server API** (Hauptaufgabe!)
|
||||||
@@ -64,20 +74,71 @@ Stand: 09.06.2025 - 14:45 Uhr
|
|||||||
|
|
||||||
## 🎯 Nächste Schritte (Priorität Hoch)
|
## 🎯 Nächste Schritte (Priorität Hoch)
|
||||||
|
|
||||||
1. **License Server API implementieren**
|
### Admin Panel Änderungen (NEUE PRIORITÄT!)
|
||||||
|
|
||||||
|
1. **Kunden ohne Lizenz anlegen**
|
||||||
|
- Kundenerstellung ohne Lizenzzwang ermöglichen
|
||||||
|
- UI-Anpassung im Customer-Form
|
||||||
|
- Backend-Validierung anpassen
|
||||||
|
|
||||||
|
2. **Audit Log IP-Problem beheben**
|
||||||
|
- IP-Adressen werden nicht korrekt gespeichert/angezeigt
|
||||||
|
- Proxy-Header (X-Forwarded-For) korrekt auslesen
|
||||||
|
- Real-IP Ermittlung hinter Nginx
|
||||||
|
|
||||||
|
3. **E-Mail Benachrichtigungen für ablaufende Lizenzen**
|
||||||
|
- IMAP-Einstellungen im Profil (pro Admin-User)
|
||||||
|
- Benachrichtigungszeitpunkte: 1 Monat, 3 Wochen, 2 Wochen, 1 Woche, 3 Tage, 1 Tag vor Ablauf
|
||||||
|
- Tabelle für IMAP-Settings erstellen
|
||||||
|
- Background-Job für E-Mail-Versand
|
||||||
|
- Template für Ablauf-E-Mails
|
||||||
|
|
||||||
|
4. **Backup-Löschfunktion**
|
||||||
|
- Delete-Button für Backups hinzufügen
|
||||||
|
- Sicherheitsabfrage vor Löschung
|
||||||
|
- Audit-Log Eintrag bei Löschung
|
||||||
|
|
||||||
|
### Weitere geplante Features
|
||||||
|
|
||||||
|
1. **Verlängerungs-System im Admin Panel**
|
||||||
|
- Verlängerungs-Vorlagen (manual, standard, upgrade_path, trial_to_full)
|
||||||
|
- Lizenzbasierte Verlängerungseinstellungen (nicht kundenbasiert)
|
||||||
|
- Tabelle `renewal_templates` erstellen
|
||||||
|
- `licenses` Tabelle erweitern (renewal_template, renewal_count)
|
||||||
|
- UI für Verlängerungs-Verwaltung pro Lizenz
|
||||||
|
- Bulk-Änderung von Verlängerungseinstellungen
|
||||||
|
- Dashboard-Widget für anstehende Verlängerungen
|
||||||
|
- Verlängerungs-Historie pro Lizenz anzeigen
|
||||||
|
- Automatische Verlängerung nach Vorlage
|
||||||
|
|
||||||
|
5. **Device-Management im Admin Panel** (Vorbereitung für License Server)
|
||||||
|
- `licenses` Tabelle erweitern (max_devices INTEGER DEFAULT 1)
|
||||||
|
- UI zum Setzen der Geräte-Limits pro Lizenz
|
||||||
|
- Tabelle `device_registrations` erstellen
|
||||||
|
- Geräte-Verwaltung UI (Liste registrierter Geräte pro Lizenz)
|
||||||
|
- Support-Funktionen (Geräte entfernen, zurücksetzen)
|
||||||
|
- Dashboard-Widget für Geräte-Auslastung
|
||||||
|
- Verschiedene Lizenzmodelle (Einzelplatz=1, Team=3, Business=5, Enterprise=unbegrenzt)
|
||||||
|
|
||||||
|
6. **License Server API implementieren**
|
||||||
- Flask-Anwendung mit PostgreSQL-Anbindung
|
- Flask-Anwendung mit PostgreSQL-Anbindung
|
||||||
- `/api/version` - Versionscheck
|
- `/api/version` - Versionscheck
|
||||||
- `/api/validate` - Lizenzvalidierung
|
- `/api/validate` - Lizenzvalidierung (mit `is_test` Check)
|
||||||
- `/api/heartbeat` - Session-Management
|
- `/api/heartbeat` - Session-Management
|
||||||
- API-Key Authentifizierung
|
- `/api/register-device` - Geräte-Registrierung (NEU!)
|
||||||
|
- Device-Token Generation statt globalem API-Key
|
||||||
|
- Hardware-ID Validierung
|
||||||
|
- API-Key Authentifizierung (oder Device-Token)
|
||||||
- Rate Limiting
|
- Rate Limiting
|
||||||
- Dockerfile anpassen
|
- Dockerfile anpassen
|
||||||
- requirements.txt erstellen
|
- requirements.txt erstellen
|
||||||
|
|
||||||
3. **Testing**
|
7. **Testing**
|
||||||
- API-Endpunkte testen
|
- API-Endpunkte testen
|
||||||
- Integration mit Admin Panel verifizieren
|
- Integration mit Admin Panel verifizieren
|
||||||
- Session-Management prüfen
|
- Session-Management prüfen
|
||||||
|
- Test-Flag Funktionalität verifizieren
|
||||||
|
- Device-Registrierung testen
|
||||||
|
|
||||||
## 📋 Offene Aufgaben (Priorität Mittel)
|
## 📋 Offene Aufgaben (Priorität Mittel)
|
||||||
|
|
||||||
@@ -89,8 +150,8 @@ Stand: 09.06.2025 - 14:45 Uhr
|
|||||||
2. **Lizenz-Features**
|
2. **Lizenz-Features**
|
||||||
- Bulk-Import (CSV/Excel Upload)
|
- Bulk-Import (CSV/Excel Upload)
|
||||||
- Lizenz-Templates
|
- Lizenz-Templates
|
||||||
- Automatische Verlängerung
|
|
||||||
- Lizenz-Historie
|
- Lizenz-Historie
|
||||||
|
- Flexible Geräte-Limits pro Lizenz (verschiedene Lizenzmodelle: Einzelplatz, Team, Business, Enterprise)
|
||||||
|
|
||||||
3. **Benachrichtigungen**
|
3. **Benachrichtigungen**
|
||||||
- E-Mail bei ablaufenden Lizenzen
|
- E-Mail bei ablaufenden Lizenzen
|
||||||
|
|||||||
1
backups/backup_v2docker_20250609_145347_encrypted.sql.gz.enc
Normale Datei
1
backups/backup_v2docker_20250609_145347_encrypted.sql.gz.enc
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
@@ -1174,18 +1174,18 @@ def dashboard():
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Statistiken abrufen
|
# Statistiken abrufen
|
||||||
# Gesamtanzahl Kunden
|
# Gesamtanzahl Kunden (ohne Testdaten)
|
||||||
cur.execute("SELECT COUNT(*) FROM customers")
|
cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE")
|
||||||
total_customers = cur.fetchone()[0]
|
total_customers = cur.fetchone()[0]
|
||||||
|
|
||||||
# Gesamtanzahl Lizenzen
|
# Gesamtanzahl Lizenzen (ohne Testdaten)
|
||||||
cur.execute("SELECT COUNT(*) FROM licenses")
|
cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE")
|
||||||
total_licenses = cur.fetchone()[0]
|
total_licenses = cur.fetchone()[0]
|
||||||
|
|
||||||
# Aktive Lizenzen (nicht abgelaufen und is_active = true)
|
# Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT COUNT(*) FROM licenses
|
SELECT COUNT(*) FROM licenses
|
||||||
WHERE valid_until >= CURRENT_DATE AND is_active = TRUE
|
WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE
|
||||||
""")
|
""")
|
||||||
active_licenses = cur.fetchone()[0]
|
active_licenses = cur.fetchone()[0]
|
||||||
|
|
||||||
@@ -1193,38 +1193,52 @@ def dashboard():
|
|||||||
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
|
cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE")
|
||||||
active_sessions_count = cur.fetchone()[0]
|
active_sessions_count = cur.fetchone()[0]
|
||||||
|
|
||||||
# Abgelaufene Lizenzen
|
# Abgelaufene Lizenzen (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT COUNT(*) FROM licenses
|
SELECT COUNT(*) FROM licenses
|
||||||
WHERE valid_until < CURRENT_DATE
|
WHERE valid_until < CURRENT_DATE AND is_test = FALSE
|
||||||
""")
|
""")
|
||||||
expired_licenses = cur.fetchone()[0]
|
expired_licenses = cur.fetchone()[0]
|
||||||
|
|
||||||
# Deaktivierte Lizenzen
|
# Deaktivierte Lizenzen (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT COUNT(*) FROM licenses
|
SELECT COUNT(*) FROM licenses
|
||||||
WHERE is_active = FALSE
|
WHERE is_active = FALSE AND is_test = FALSE
|
||||||
""")
|
""")
|
||||||
inactive_licenses = cur.fetchone()[0]
|
inactive_licenses = cur.fetchone()[0]
|
||||||
|
|
||||||
# Lizenzen die in den nächsten 30 Tagen ablaufen
|
# Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT COUNT(*) FROM licenses
|
SELECT COUNT(*) FROM licenses
|
||||||
WHERE valid_until >= CURRENT_DATE
|
WHERE valid_until >= CURRENT_DATE
|
||||||
AND valid_until < CURRENT_DATE + INTERVAL '30 days'
|
AND valid_until < CURRENT_DATE + INTERVAL '30 days'
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
|
AND is_test = FALSE
|
||||||
""")
|
""")
|
||||||
expiring_soon = cur.fetchone()[0]
|
expiring_soon = cur.fetchone()[0]
|
||||||
|
|
||||||
# Testlizenzen vs Vollversionen
|
# Testlizenzen vs Vollversionen (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT license_type, COUNT(*)
|
SELECT license_type, COUNT(*)
|
||||||
FROM licenses
|
FROM licenses
|
||||||
|
WHERE is_test = FALSE
|
||||||
GROUP BY license_type
|
GROUP BY license_type
|
||||||
""")
|
""")
|
||||||
license_types = dict(cur.fetchall())
|
license_types = dict(cur.fetchall())
|
||||||
|
|
||||||
# Letzte 5 erstellten Lizenzen
|
# Anzahl Testdaten
|
||||||
|
cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE")
|
||||||
|
test_data_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Anzahl Test-Kunden
|
||||||
|
cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE")
|
||||||
|
test_customers_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Anzahl Test-Ressourcen
|
||||||
|
cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE")
|
||||||
|
test_resources_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Letzte 5 erstellten Lizenzen (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT l.id, l.license_key, c.name, l.valid_until,
|
SELECT l.id, l.license_key, c.name, l.valid_until,
|
||||||
CASE
|
CASE
|
||||||
@@ -1235,12 +1249,13 @@ def dashboard():
|
|||||||
END as status
|
END as status
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
|
WHERE l.is_test = FALSE
|
||||||
ORDER BY l.id DESC
|
ORDER BY l.id DESC
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
""")
|
""")
|
||||||
recent_licenses = cur.fetchall()
|
recent_licenses = cur.fetchall()
|
||||||
|
|
||||||
# Bald ablaufende Lizenzen (nächste 30 Tage)
|
# Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT l.id, l.license_key, c.name, l.valid_until,
|
SELECT l.id, l.license_key, c.name, l.valid_until,
|
||||||
l.valid_until - CURRENT_DATE as days_left
|
l.valid_until - CURRENT_DATE as days_left
|
||||||
@@ -1249,6 +1264,7 @@ def dashboard():
|
|||||||
WHERE l.valid_until >= CURRENT_DATE
|
WHERE l.valid_until >= CURRENT_DATE
|
||||||
AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
|
AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
|
||||||
AND l.is_active = TRUE
|
AND l.is_active = TRUE
|
||||||
|
AND l.is_test = FALSE
|
||||||
ORDER BY l.valid_until
|
ORDER BY l.valid_until
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
""")
|
""")
|
||||||
@@ -1358,6 +1374,9 @@ def dashboard():
|
|||||||
'expiring_soon': expiring_soon,
|
'expiring_soon': expiring_soon,
|
||||||
'full_licenses': license_types.get('full', 0),
|
'full_licenses': license_types.get('full', 0),
|
||||||
'test_licenses': license_types.get('test', 0),
|
'test_licenses': license_types.get('test', 0),
|
||||||
|
'test_data_count': test_data_count,
|
||||||
|
'test_customers_count': test_customers_count,
|
||||||
|
'test_resources_count': test_resources_count,
|
||||||
'recent_licenses': recent_licenses,
|
'recent_licenses': recent_licenses,
|
||||||
'expiring_licenses': expiring_licenses,
|
'expiring_licenses': expiring_licenses,
|
||||||
'active_sessions': active_sessions_count,
|
'active_sessions': active_sessions_count,
|
||||||
@@ -1385,6 +1404,7 @@ def create_license():
|
|||||||
license_key = request.form["license_key"].upper() # Immer Großbuchstaben
|
license_key = request.form["license_key"].upper() # Immer Großbuchstaben
|
||||||
license_type = request.form["license_type"]
|
license_type = request.form["license_type"]
|
||||||
valid_from = request.form["valid_from"]
|
valid_from = request.form["valid_from"]
|
||||||
|
is_test = request.form.get("is_test") == "on" # Checkbox value
|
||||||
|
|
||||||
# Berechne valid_until basierend auf Laufzeit
|
# Berechne valid_until basierend auf Laufzeit
|
||||||
duration = int(request.form.get("duration", 1))
|
duration = int(request.form.get("duration", 1))
|
||||||
@@ -1438,35 +1458,39 @@ def create_license():
|
|||||||
flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
|
flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
|
||||||
return redirect(url_for('create_license'))
|
return redirect(url_for('create_license'))
|
||||||
|
|
||||||
# Kunde einfügen
|
# Kunde einfügen (erbt Test-Status von Lizenz)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO customers (name, email, created_at)
|
INSERT INTO customers (name, email, is_test, created_at)
|
||||||
VALUES (%s, %s, NOW())
|
VALUES (%s, %s, %s, NOW())
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (name, email))
|
""", (name, email, is_test))
|
||||||
customer_id = cur.fetchone()[0]
|
customer_id = cur.fetchone()[0]
|
||||||
customer_info = {'name': name, 'email': email}
|
customer_info = {'name': name, 'email': email, 'is_test': is_test}
|
||||||
|
|
||||||
# Audit-Log für neuen Kunden
|
# Audit-Log für neuen Kunden
|
||||||
log_audit('CREATE', 'customer', customer_id,
|
log_audit('CREATE', 'customer', customer_id,
|
||||||
new_values={'name': name, 'email': email})
|
new_values={'name': name, 'email': email, 'is_test': is_test})
|
||||||
else:
|
else:
|
||||||
# Bestehender Kunde - hole Infos für Audit-Log
|
# Bestehender Kunde - hole Infos für Audit-Log
|
||||||
cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
|
cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
|
||||||
customer_data = cur.fetchone()
|
customer_data = cur.fetchone()
|
||||||
if not customer_data:
|
if not customer_data:
|
||||||
flash('Kunde nicht gefunden!', 'error')
|
flash('Kunde nicht gefunden!', 'error')
|
||||||
return redirect(url_for('create_license'))
|
return redirect(url_for('create_license'))
|
||||||
customer_info = {'name': customer_data[0], 'email': customer_data[1]}
|
customer_info = {'name': customer_data[0], 'email': customer_data[1]}
|
||||||
|
|
||||||
|
# Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren
|
||||||
|
if customer_data[2]: # is_test des Kunden
|
||||||
|
is_test = True
|
||||||
|
|
||||||
# Lizenz hinzufügen
|
# Lizenz hinzufügen
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
||||||
domain_count, ipv4_count, phone_count)
|
domain_count, ipv4_count, phone_count, is_test)
|
||||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
||||||
domain_count, ipv4_count, phone_count))
|
domain_count, ipv4_count, phone_count, is_test))
|
||||||
license_id = cur.fetchone()[0]
|
license_id = cur.fetchone()[0]
|
||||||
|
|
||||||
# Ressourcen zuweisen
|
# Ressourcen zuweisen
|
||||||
@@ -1474,10 +1498,10 @@ def create_license():
|
|||||||
# Prüfe Verfügbarkeit
|
# Prüfe Verfügbarkeit
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available') as domains,
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available') as ipv4s,
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available') as phones
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
|
||||||
""")
|
""", (is_test, is_test, is_test))
|
||||||
available = cur.fetchone()
|
available = cur.fetchone()
|
||||||
|
|
||||||
if available[0] < domain_count:
|
if available[0] < domain_count:
|
||||||
@@ -1491,9 +1515,9 @@ def create_license():
|
|||||||
if domain_count > 0:
|
if domain_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'domain' AND status = 'available'
|
WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (domain_count,))
|
""", (is_test, domain_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1516,9 +1540,9 @@ def create_license():
|
|||||||
if ipv4_count > 0:
|
if ipv4_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'ipv4' AND status = 'available'
|
WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (ipv4_count,))
|
""", (is_test, ipv4_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1541,9 +1565,9 @@ def create_license():
|
|||||||
if phone_count > 0:
|
if phone_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'phone' AND status = 'available'
|
WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (phone_count,))
|
""", (is_test, phone_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1577,7 +1601,8 @@ def create_license():
|
|||||||
'customer_email': customer_info['email'],
|
'customer_email': customer_info['email'],
|
||||||
'license_type': license_type,
|
'license_type': license_type,
|
||||||
'valid_from': valid_from,
|
'valid_from': valid_from,
|
||||||
'valid_until': valid_until
|
'valid_until': valid_until,
|
||||||
|
'is_test': is_test
|
||||||
})
|
})
|
||||||
|
|
||||||
flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
|
flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success')
|
||||||
@@ -1604,6 +1629,7 @@ def batch_licenses():
|
|||||||
license_type = request.form["license_type"]
|
license_type = request.form["license_type"]
|
||||||
quantity = int(request.form["quantity"])
|
quantity = int(request.form["quantity"])
|
||||||
valid_from = request.form["valid_from"]
|
valid_from = request.form["valid_from"]
|
||||||
|
is_test = request.form.get("is_test") == "on" # Checkbox value
|
||||||
|
|
||||||
# Berechne valid_until basierend auf Laufzeit
|
# Berechne valid_until basierend auf Laufzeit
|
||||||
duration = int(request.form.get("duration", 1))
|
duration = int(request.form.get("duration", 1))
|
||||||
@@ -1657,20 +1683,20 @@ def batch_licenses():
|
|||||||
flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
|
flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error')
|
||||||
return redirect(url_for('batch_licenses'))
|
return redirect(url_for('batch_licenses'))
|
||||||
|
|
||||||
# Kunde einfügen
|
# Kunde einfügen (erbt Test-Status von Lizenz)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO customers (name, email, created_at)
|
INSERT INTO customers (name, email, is_test, created_at)
|
||||||
VALUES (%s, %s, NOW())
|
VALUES (%s, %s, %s, NOW())
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (name, email))
|
""", (name, email, is_test))
|
||||||
customer_id = cur.fetchone()[0]
|
customer_id = cur.fetchone()[0]
|
||||||
|
|
||||||
# Audit-Log für neuen Kunden
|
# Audit-Log für neuen Kunden
|
||||||
log_audit('CREATE', 'customer', customer_id,
|
log_audit('CREATE', 'customer', customer_id,
|
||||||
new_values={'name': name, 'email': email})
|
new_values={'name': name, 'email': email, 'is_test': is_test})
|
||||||
else:
|
else:
|
||||||
# Bestehender Kunde - hole Infos
|
# Bestehender Kunde - hole Infos
|
||||||
cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
|
cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
|
||||||
customer_data = cur.fetchone()
|
customer_data = cur.fetchone()
|
||||||
if not customer_data:
|
if not customer_data:
|
||||||
flash('Kunde nicht gefunden!', 'error')
|
flash('Kunde nicht gefunden!', 'error')
|
||||||
@@ -1678,6 +1704,10 @@ def batch_licenses():
|
|||||||
name = customer_data[0]
|
name = customer_data[0]
|
||||||
email = customer_data[1]
|
email = customer_data[1]
|
||||||
|
|
||||||
|
# Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren
|
||||||
|
if customer_data[2]: # is_test des Kunden
|
||||||
|
is_test = True
|
||||||
|
|
||||||
# Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
|
# Prüfe Ressourcen-Verfügbarkeit für gesamten Batch
|
||||||
total_domains_needed = domain_count * quantity
|
total_domains_needed = domain_count * quantity
|
||||||
total_ipv4s_needed = ipv4_count * quantity
|
total_ipv4s_needed = ipv4_count * quantity
|
||||||
@@ -1685,10 +1715,10 @@ def batch_licenses():
|
|||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available') as domains,
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains,
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available') as ipv4s,
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s,
|
||||||
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available') as phones
|
(SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones
|
||||||
""")
|
""", (is_test, is_test, is_test))
|
||||||
available = cur.fetchone()
|
available = cur.fetchone()
|
||||||
|
|
||||||
if available[0] < total_domains_needed:
|
if available[0] < total_domains_needed:
|
||||||
@@ -1715,12 +1745,12 @@ def batch_licenses():
|
|||||||
|
|
||||||
# Lizenz einfügen
|
# Lizenz einfügen
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO licenses (license_key, customer_id, license_type,
|
INSERT INTO licenses (license_key, customer_id, license_type, is_test,
|
||||||
valid_from, valid_until, is_active,
|
valid_from, valid_until, is_active,
|
||||||
domain_count, ipv4_count, phone_count)
|
domain_count, ipv4_count, phone_count)
|
||||||
VALUES (%s, %s, %s, %s, %s, true, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
""", (license_key, customer_id, license_type, is_test, valid_from, valid_until,
|
||||||
domain_count, ipv4_count, phone_count))
|
domain_count, ipv4_count, phone_count))
|
||||||
license_id = cur.fetchone()[0]
|
license_id = cur.fetchone()[0]
|
||||||
|
|
||||||
@@ -1729,9 +1759,9 @@ def batch_licenses():
|
|||||||
if domain_count > 0:
|
if domain_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'domain' AND status = 'available'
|
WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (domain_count,))
|
""", (is_test, domain_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1754,9 +1784,9 @@ def batch_licenses():
|
|||||||
if ipv4_count > 0:
|
if ipv4_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'ipv4' AND status = 'available'
|
WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (ipv4_count,))
|
""", (is_test, ipv4_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1779,9 +1809,9 @@ def batch_licenses():
|
|||||||
if phone_count > 0:
|
if phone_count > 0:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id FROM resource_pools
|
SELECT id FROM resource_pools
|
||||||
WHERE resource_type = 'phone' AND status = 'available'
|
WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s
|
||||||
LIMIT %s FOR UPDATE
|
LIMIT %s FOR UPDATE
|
||||||
""", (phone_count,))
|
""", (is_test, phone_count))
|
||||||
for (resource_id,) in cur.fetchall():
|
for (resource_id,) in cur.fetchall():
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE resource_pools
|
UPDATE resource_pools
|
||||||
@@ -1921,7 +1951,7 @@ def licenses():
|
|||||||
# SQL Query mit optionaler Suche und Filtern
|
# SQL Query mit optionaler Suche und Filtern
|
||||||
query = """
|
query = """
|
||||||
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
||||||
l.valid_from, l.valid_until, l.is_active,
|
l.valid_from, l.valid_until, l.is_active, l.is_test,
|
||||||
CASE
|
CASE
|
||||||
WHEN l.is_active = FALSE THEN 'deaktiviert'
|
WHEN l.is_active = FALSE THEN 'deaktiviert'
|
||||||
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
|
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
|
||||||
@@ -1947,7 +1977,12 @@ def licenses():
|
|||||||
|
|
||||||
# Typ-Filter
|
# Typ-Filter
|
||||||
if filter_type:
|
if filter_type:
|
||||||
query += " AND l.license_type = %s"
|
if filter_type == 'test_data':
|
||||||
|
query += " AND l.is_test = TRUE"
|
||||||
|
elif filter_type == 'live_data':
|
||||||
|
query += " AND l.is_test = FALSE"
|
||||||
|
else:
|
||||||
|
query += " AND l.license_type = %s AND l.is_test = FALSE"
|
||||||
params.append(filter_type)
|
params.append(filter_type)
|
||||||
|
|
||||||
# Status-Filter
|
# Status-Filter
|
||||||
@@ -2013,7 +2048,7 @@ def edit_license(license_id):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# Alte Werte für Audit-Log abrufen
|
# Alte Werte für Audit-Log abrufen
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT license_key, license_type, valid_from, valid_until, is_active
|
SELECT license_key, license_type, valid_from, valid_until, is_active, is_test
|
||||||
FROM licenses WHERE id = %s
|
FROM licenses WHERE id = %s
|
||||||
""", (license_id,))
|
""", (license_id,))
|
||||||
old_license = cur.fetchone()
|
old_license = cur.fetchone()
|
||||||
@@ -2024,13 +2059,14 @@ def edit_license(license_id):
|
|||||||
valid_from = request.form["valid_from"]
|
valid_from = request.form["valid_from"]
|
||||||
valid_until = request.form["valid_until"]
|
valid_until = request.form["valid_until"]
|
||||||
is_active = request.form.get("is_active") == "on"
|
is_active = request.form.get("is_active") == "on"
|
||||||
|
is_test = request.form.get("is_test") == "on"
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE licenses
|
UPDATE licenses
|
||||||
SET license_key = %s, license_type = %s, valid_from = %s,
|
SET license_key = %s, license_type = %s, valid_from = %s,
|
||||||
valid_until = %s, is_active = %s
|
valid_until = %s, is_active = %s, is_test = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (license_key, license_type, valid_from, valid_until, is_active, license_id))
|
""", (license_key, license_type, valid_from, valid_until, is_active, is_test, license_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -2041,14 +2077,16 @@ def edit_license(license_id):
|
|||||||
'license_type': old_license[1],
|
'license_type': old_license[1],
|
||||||
'valid_from': str(old_license[2]),
|
'valid_from': str(old_license[2]),
|
||||||
'valid_until': str(old_license[3]),
|
'valid_until': str(old_license[3]),
|
||||||
'is_active': old_license[4]
|
'is_active': old_license[4],
|
||||||
|
'is_test': old_license[5]
|
||||||
},
|
},
|
||||||
new_values={
|
new_values={
|
||||||
'license_key': license_key,
|
'license_key': license_key,
|
||||||
'license_type': license_type,
|
'license_type': license_type,
|
||||||
'valid_from': valid_from,
|
'valid_from': valid_from,
|
||||||
'valid_until': valid_until,
|
'valid_until': valid_until,
|
||||||
'is_active': is_active
|
'is_active': is_active,
|
||||||
|
'is_test': is_test
|
||||||
})
|
})
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -2059,7 +2097,7 @@ def edit_license(license_id):
|
|||||||
# Get license data
|
# Get license data
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
||||||
l.valid_from, l.valid_until, l.is_active, c.id
|
l.valid_from, l.valid_until, l.is_active, c.id, l.is_test
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
WHERE l.id = %s
|
WHERE l.id = %s
|
||||||
@@ -2140,7 +2178,7 @@ def customers():
|
|||||||
|
|
||||||
# SQL Query mit optionaler Suche
|
# SQL Query mit optionaler Suche
|
||||||
base_query = """
|
base_query = """
|
||||||
SELECT c.id, c.name, c.email, c.created_at,
|
SELECT c.id, c.name, c.email, c.created_at, c.is_test,
|
||||||
COUNT(l.id) as license_count,
|
COUNT(l.id) as license_count,
|
||||||
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses
|
||||||
FROM customers c
|
FROM customers c
|
||||||
@@ -2173,7 +2211,7 @@ def customers():
|
|||||||
# Pagination
|
# Pagination
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
query = base_query + f"""
|
query = base_query + f"""
|
||||||
GROUP BY c.id, c.name, c.email, c.created_at
|
GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
|
||||||
ORDER BY {sort_field} {order.upper()}
|
ORDER BY {sort_field} {order.upper()}
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
"""
|
"""
|
||||||
@@ -2206,18 +2244,19 @@ def edit_customer(customer_id):
|
|||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# Alte Werte für Audit-Log abrufen
|
# Alte Werte für Audit-Log abrufen
|
||||||
cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,))
|
cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,))
|
||||||
old_customer = cur.fetchone()
|
old_customer = cur.fetchone()
|
||||||
|
|
||||||
# Update customer
|
# Update customer
|
||||||
name = request.form["name"]
|
name = request.form["name"]
|
||||||
email = request.form["email"]
|
email = request.form["email"]
|
||||||
|
is_test = request.form.get("is_test") == "on"
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE customers
|
UPDATE customers
|
||||||
SET name = %s, email = %s
|
SET name = %s, email = %s, is_test = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (name, email, customer_id))
|
""", (name, email, is_test, customer_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -2225,11 +2264,13 @@ def edit_customer(customer_id):
|
|||||||
log_audit('UPDATE', 'customer', customer_id,
|
log_audit('UPDATE', 'customer', customer_id,
|
||||||
old_values={
|
old_values={
|
||||||
'name': old_customer[0],
|
'name': old_customer[0],
|
||||||
'email': old_customer[1]
|
'email': old_customer[1],
|
||||||
|
'is_test': old_customer[2]
|
||||||
},
|
},
|
||||||
new_values={
|
new_values={
|
||||||
'name': name,
|
'name': name,
|
||||||
'email': email
|
'email': email,
|
||||||
|
'is_test': is_test
|
||||||
})
|
})
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -2239,12 +2280,15 @@ def edit_customer(customer_id):
|
|||||||
|
|
||||||
# Get customer data with licenses
|
# Get customer data with licenses
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT id, name, email, created_at
|
SELECT id, name, email, is_test FROM customers WHERE id = %s
|
||||||
FROM customers
|
|
||||||
WHERE id = %s
|
|
||||||
""", (customer_id,))
|
""", (customer_id,))
|
||||||
|
|
||||||
customer = cur.fetchone()
|
customer = cur.fetchone()
|
||||||
|
if not customer:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return "Kunde nicht gefunden", 404
|
||||||
|
|
||||||
|
|
||||||
# Get customer's licenses
|
# Get customer's licenses
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
@@ -2409,10 +2453,12 @@ def export_licenses():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Alle Lizenzen mit Kundeninformationen abrufen
|
# Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
|
||||||
cur.execute("""
|
include_test = request.args.get('include_test', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
query = """
|
||||||
SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email,
|
SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email,
|
||||||
l.license_type, l.valid_from, l.valid_until, l.is_active,
|
l.license_type, l.valid_from, l.valid_until, l.is_active, l.is_test,
|
||||||
CASE
|
CASE
|
||||||
WHEN l.is_active = FALSE THEN 'Deaktiviert'
|
WHEN l.is_active = FALSE THEN 'Deaktiviert'
|
||||||
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
|
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
|
||||||
@@ -2421,12 +2467,18 @@ def export_licenses():
|
|||||||
END as status
|
END as status
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
ORDER BY l.id
|
"""
|
||||||
""")
|
|
||||||
|
if not include_test:
|
||||||
|
query += " WHERE l.is_test = FALSE"
|
||||||
|
|
||||||
|
query += " ORDER BY l.id"
|
||||||
|
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
# Spaltennamen
|
# Spaltennamen
|
||||||
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ',
|
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ',
|
||||||
'Gültig von', 'Gültig bis', 'Aktiv', 'Status']
|
'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status']
|
||||||
|
|
||||||
# Daten in DataFrame
|
# Daten in DataFrame
|
||||||
data = cur.fetchall()
|
data = cur.fetchall()
|
||||||
@@ -2439,6 +2491,7 @@ def export_licenses():
|
|||||||
# Typ und Aktiv Status anpassen
|
# Typ und Aktiv Status anpassen
|
||||||
df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
|
df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'})
|
||||||
df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
|
df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'})
|
||||||
|
df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -2498,12 +2551,12 @@ def export_customers():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Alle Kunden mit Lizenzstatistiken
|
# Alle Kunden mit Lizenzstatistiken (ohne Testdaten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT c.id, c.name, c.email, c.created_at,
|
SELECT c.id, c.name, c.email, c.created_at,
|
||||||
COUNT(l.id) as total_licenses,
|
COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
|
||||||
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
|
||||||
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
|
COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
|
||||||
FROM customers c
|
FROM customers c
|
||||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||||
GROUP BY c.id, c.name, c.email, c.created_at
|
GROUP BY c.id, c.name, c.email, c.created_at
|
||||||
@@ -2900,11 +2953,11 @@ def bulk_activate_licenses():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Update all selected licenses
|
# Update all selected licenses (nur Live-Daten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE licenses
|
UPDATE licenses
|
||||||
SET is_active = TRUE
|
SET is_active = TRUE
|
||||||
WHERE id = ANY(%s)
|
WHERE id = ANY(%s) AND is_test = FALSE
|
||||||
""", (license_ids,))
|
""", (license_ids,))
|
||||||
|
|
||||||
affected_rows = cur.rowcount
|
affected_rows = cur.rowcount
|
||||||
@@ -2936,11 +2989,11 @@ def bulk_deactivate_licenses():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Update all selected licenses
|
# Update all selected licenses (nur Live-Daten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE licenses
|
UPDATE licenses
|
||||||
SET is_active = FALSE
|
SET is_active = FALSE
|
||||||
WHERE id = ANY(%s)
|
WHERE id = ANY(%s) AND is_test = FALSE
|
||||||
""", (license_ids,))
|
""", (license_ids,))
|
||||||
|
|
||||||
affected_rows = cur.rowcount
|
affected_rows = cur.rowcount
|
||||||
@@ -2972,18 +3025,18 @@ def bulk_delete_licenses():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Get license info for audit log
|
# Get license info for audit log (nur Live-Daten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT license_key
|
SELECT license_key
|
||||||
FROM licenses
|
FROM licenses
|
||||||
WHERE id = ANY(%s)
|
WHERE id = ANY(%s) AND is_test = FALSE
|
||||||
""", (license_ids,))
|
""", (license_ids,))
|
||||||
license_keys = [row[0] for row in cur.fetchall()]
|
license_keys = [row[0] for row in cur.fetchall()]
|
||||||
|
|
||||||
# Delete all selected licenses
|
# Delete all selected licenses (nur Live-Daten)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
DELETE FROM licenses
|
DELETE FROM licenses
|
||||||
WHERE id = ANY(%s)
|
WHERE id = ANY(%s) AND is_test = FALSE
|
||||||
""", (license_ids,))
|
""", (license_ids,))
|
||||||
|
|
||||||
affected_rows = cur.rowcount
|
affected_rows = cur.rowcount
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS customers (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT,
|
email TEXT,
|
||||||
|
is_test BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT unique_email UNIQUE (email)
|
CONSTRAINT unique_email UNIQUE (email)
|
||||||
);
|
);
|
||||||
@@ -20,6 +21,7 @@ CREATE TABLE IF NOT EXISTS licenses (
|
|||||||
valid_from DATE NOT NULL,
|
valid_from DATE NOT NULL,
|
||||||
valid_until DATE NOT NULL,
|
valid_until DATE NOT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
is_test BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,6 +120,7 @@ CREATE TABLE IF NOT EXISTS resource_pools (
|
|||||||
quarantine_until TIMESTAMP WITH TIME ZONE,
|
quarantine_until TIMESTAMP WITH TIME ZONE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
|
is_test BOOLEAN DEFAULT FALSE,
|
||||||
UNIQUE(resource_type, resource_value)
|
UNIQUE(resource_type, resource_value)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -200,3 +203,48 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
-- Index for faster login lookups
|
-- Index for faster login lookups
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;
|
CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(password_reset_token) WHERE password_reset_token IS NOT NULL;
|
||||||
|
|
||||||
|
-- Migration: Add is_test column to licenses if it doesn't exist
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'licenses' AND column_name = 'is_test') THEN
|
||||||
|
ALTER TABLE licenses ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Mark all existing licenses as test data
|
||||||
|
UPDATE licenses SET is_test = TRUE;
|
||||||
|
|
||||||
|
-- Add index for better performance when filtering test data
|
||||||
|
CREATE INDEX idx_licenses_is_test ON licenses(is_test);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Migration: Add is_test column to customers if it doesn't exist
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'customers' AND column_name = 'is_test') THEN
|
||||||
|
ALTER TABLE customers ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Mark all existing customers as test data
|
||||||
|
UPDATE customers SET is_test = TRUE;
|
||||||
|
|
||||||
|
-- Add index for better performance
|
||||||
|
CREATE INDEX idx_customers_is_test ON customers(is_test);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Migration: Add is_test column to resource_pools if it doesn't exist
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'resource_pools' AND column_name = 'is_test') THEN
|
||||||
|
ALTER TABLE resource_pools ADD COLUMN is_test BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Mark all existing resources as test data
|
||||||
|
UPDATE resource_pools SET is_test = TRUE;
|
||||||
|
|
||||||
|
-- Add index for better performance
|
||||||
|
CREATE INDEX idx_resource_pools_is_test ON resource_pools(is_test);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|||||||
@@ -152,6 +152,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Data Checkbox -->
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||||
|
<label class="form-check-label" for="isTest">
|
||||||
|
<i class="fas fa-flask"></i> Als Testdaten markieren
|
||||||
|
<small class="text-muted">(wird von der Software ignoriert)</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 d-flex gap-2">
|
<div class="mt-4 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
🔑 Batch generieren
|
🔑 Batch generieren
|
||||||
|
|||||||
@@ -84,16 +84,21 @@
|
|||||||
{% for customer in customers %}
|
{% for customer in customers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ customer[0] }}</td>
|
<td>{{ customer[0] }}</td>
|
||||||
<td>{{ customer[1] }}</td>
|
<td>
|
||||||
|
{{ customer[1] }}
|
||||||
|
{% if customer[4] %}
|
||||||
|
<span class="badge bg-secondary ms-1" title="Testdaten">🧪</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ customer[2] or '-' }}</td>
|
<td>{{ customer[2] or '-' }}</td>
|
||||||
<td>{{ customer[3].strftime('%d.%m.%Y %H:%M') }}</td>
|
<td>{{ customer[3].strftime('%d.%m.%Y %H:%M') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-info">{{ customer[5] }}/{{ customer[4] }}</span>
|
<span class="badge bg-info">{{ customer[6] }}/{{ customer[5] }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
<a href="/customer/edit/{{ customer[0] }}" class="btn btn-outline-primary">✏️ Bearbeiten</a>
|
<a href="/customer/edit/{{ customer[0] }}" class="btn btn-outline-primary">✏️ Bearbeiten</a>
|
||||||
{% if customer[4] == 0 %}
|
{% if customer[5] == 0 %}
|
||||||
<form method="post" action="/customer/delete/{{ customer[0] }}" style="display: inline;" onsubmit="return confirm('Kunde wirklich löschen?');">
|
<form method="post" action="/customer/delete/{{ customer[0] }}" style="display: inline;" onsubmit="return confirm('Kunde wirklich löschen?');">
|
||||||
<button type="submit" class="btn btn-outline-danger">🗑️ Löschen</button>
|
<button type="submit" class="btn btn-outline-danger">🗑️ Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -127,6 +127,16 @@
|
|||||||
<p class="text-muted">Testversionen</p>
|
<p class="text-muted">Testversionen</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if stats.test_data_count > 0 or stats.test_customers_count > 0 or stats.test_resources_count > 0 %}
|
||||||
|
<div class="alert alert-info mt-3 mb-0">
|
||||||
|
<small>
|
||||||
|
<i class="fas fa-flask"></i> Testdaten:
|
||||||
|
{{ stats.test_data_count }} Lizenzen,
|
||||||
|
{{ stats.test_customers_count }} Kunden,
|
||||||
|
{{ stats.test_resources_count }} Ressourcen
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if customer[3] %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="isTest">
|
||||||
|
<i class="fas fa-flask"></i> Als Testdaten markieren
|
||||||
|
<small class="text-muted">(Kunde und seine Lizenzen werden von der Software ignoriert)</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
||||||
<a href="/customers" class="btn btn-secondary">Abbrechen</a>
|
<a href="/customers" class="btn btn-secondary">Abbrechen</a>
|
||||||
|
|||||||
@@ -54,6 +54,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test" {% if license[9] %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="isTest">
|
||||||
|
<i class="fas fa-flask"></i> Als Testdaten markieren
|
||||||
|
<small class="text-muted">(wird von der Software ignoriert)</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
<button type="submit" class="btn btn-primary">💾 Änderungen speichern</button>
|
||||||
<a href="/licenses" class="btn btn-secondary">Abbrechen</a>
|
<a href="/licenses" class="btn btn-secondary">Abbrechen</a>
|
||||||
|
|||||||
@@ -133,6 +133,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Data Checkbox -->
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||||
|
<label class="form-check-label" for="isTest">
|
||||||
|
<i class="fas fa-flask"></i> Als Testdaten markieren
|
||||||
|
<small class="text-muted">(wird von der Software ignoriert)</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
|
<button type="submit" class="btn btn-primary">➕ Lizenz erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
<option value="">Alle Typen</option>
|
<option value="">Alle Typen</option>
|
||||||
<option value="full" {% if filter_type == 'full' %}selected{% endif %}>Vollversion</option>
|
<option value="full" {% if filter_type == 'full' %}selected{% endif %}>Vollversion</option>
|
||||||
<option value="test" {% if filter_type == 'test' %}selected{% endif %}>Testversion</option>
|
<option value="test" {% if filter_type == 'test' %}selected{% endif %}>Testversion</option>
|
||||||
|
<option value="test_data" {% if filter_type == 'test_data' %}selected{% endif %}>🧪 Testdaten</option>
|
||||||
|
<option value="live_data" {% if filter_type == 'live_data' %}selected{% endif %}>🚀 Live-Daten</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@@ -129,7 +131,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ license[2] }}</td>
|
<td>
|
||||||
|
{{ license[2] }}
|
||||||
|
{% if license[8] %}
|
||||||
|
<span class="badge bg-secondary ms-1" title="Testdaten">🧪</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ license[3] or '-' }}</td>
|
<td>{{ license[3] or '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if license[4] == 'full' %}
|
{% if license[4] == 'full' %}
|
||||||
@@ -141,10 +148,12 @@
|
|||||||
<td>{{ license[5].strftime('%d.%m.%Y') }}</td>
|
<td>{{ license[5].strftime('%d.%m.%Y') }}</td>
|
||||||
<td>{{ license[6].strftime('%d.%m.%Y') }}</td>
|
<td>{{ license[6].strftime('%d.%m.%Y') }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if license[8] == 'abgelaufen' %}
|
{% if license[9] == 'abgelaufen' %}
|
||||||
<span class="status-abgelaufen">⚠️ Abgelaufen</span>
|
<span class="status-abgelaufen">⚠️ Abgelaufen</span>
|
||||||
{% elif license[8] == 'läuft bald ab' %}
|
{% elif license[9] == 'läuft bald ab' %}
|
||||||
<span class="status-ablaufend">⏰ Läuft bald ab</span>
|
<span class="status-ablaufend">⏰ Läuft bald ab</span>
|
||||||
|
{% elif license[9] == 'deaktiviert' %}
|
||||||
|
<span class="status-deaktiviert">❌ Deaktiviert</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="status-aktiv">✅ Aktiv</span>
|
<span class="status-aktiv">✅ Aktiv</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren