📝 "Refactor backend

This commit is contained in:
Till Tomczak 2025-05-31 23:44:20 +02:00
parent f5a39a450a
commit 831caec87a
11 changed files with 1828 additions and 22 deletions

View File

@ -4828,34 +4828,32 @@ def session_status():
@app.route('/api/session/extend', methods=['POST'])
@login_required
def extend_session():
"""
Verlängert die aktuelle Session um weitere Zeit.
"""
"""Verlängert die aktuelle Session um die Standard-Lebensdauer"""
try:
data = request.get_json() or {}
extend_minutes = data.get('extend_minutes', 30)
# Session-Lebensdauer zurücksetzen
session.permanent = True
# Begrenzen der Verlängerung (max 2 Stunden)
extend_minutes = min(extend_minutes, 120)
# Aktivität für Rate Limiting aktualisieren
current_user.update_last_activity()
now = datetime.now()
session['last_activity'] = now.isoformat()
session['session_extended'] = now.isoformat()
session['extended_by_minutes'] = extend_minutes
# Optional: Session-Statistiken für Admin
user_agent = request.headers.get('User-Agent', 'Unknown')
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
auth_logger.info(f"🕒 Session verlängert für Benutzer {current_user.email} um {extend_minutes} Minuten")
app_logger.info(f"Session verlängert für User {current_user.id} (IP: {ip_address})")
return jsonify({
"success": True,
"message": f"Session um {extend_minutes} Minuten verlängert",
"extended_until": (now + timedelta(minutes=extend_minutes)).isoformat(),
"extended_minutes": extend_minutes
'success': True,
'message': 'Session erfolgreich verlängert',
'expires_at': (datetime.now() + SESSION_LIFETIME).isoformat()
})
except Exception as e:
auth_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
return jsonify({"error": "Session-Verlängerung fehlgeschlagen"}), 500
app_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Verlängern der Session'
}), 500
# ===== GASTANTRÄGE API-ROUTEN =====
@ -5669,3 +5667,273 @@ if __name__ == "__main__":
except:
pass
sys.exit(1)
# ===== AUTO-OPTIMIERUNG-API-ENDPUNKTE =====
@app.route('/api/optimization/auto-optimize', methods=['POST'])
@login_required
def auto_optimize_jobs():
"""
Automatische Optimierung der Druckaufträge durchführen
Implementiert intelligente Job-Verteilung basierend auf verschiedenen Algorithmen
"""
try:
data = request.get_json()
settings = data.get('settings', {})
enabled = data.get('enabled', False)
db_session = get_db_session()
# Aktuelle Jobs in der Warteschlange abrufen
pending_jobs = db_session.query(Job).filter(
Job.status.in_(['queued', 'pending'])
).all()
if not pending_jobs:
db_session.close()
return jsonify({
'success': True,
'message': 'Keine Jobs zur Optimierung verfügbar',
'optimized_jobs': 0
})
# Verfügbare Drucker abrufen
available_printers = db_session.query(Printer).filter(Printer.active == True).all()
if not available_printers:
db_session.close()
return jsonify({
'success': False,
'error': 'Keine verfügbaren Drucker für Optimierung'
})
# Optimierungs-Algorithmus anwenden
algorithm = settings.get('algorithm', 'round_robin')
optimized_count = 0
if algorithm == 'round_robin':
optimized_count = apply_round_robin_optimization(pending_jobs, available_printers, db_session)
elif algorithm == 'load_balance':
optimized_count = apply_load_balance_optimization(pending_jobs, available_printers, db_session)
elif algorithm == 'priority_based':
optimized_count = apply_priority_optimization(pending_jobs, available_printers, db_session)
db_session.commit()
jobs_logger.info(f"Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert mit Algorithmus {algorithm}")
# System-Log erstellen
log_entry = SystemLog(
level='INFO',
component='optimization',
message=f'Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert',
user_id=current_user.id if current_user.is_authenticated else None,
details=json.dumps({
'algorithm': algorithm,
'optimized_jobs': optimized_count,
'settings': settings
})
)
db_session.add(log_entry)
db_session.commit()
db_session.close()
return jsonify({
'success': True,
'optimized_jobs': optimized_count,
'algorithm': algorithm,
'message': f'Optimierung erfolgreich: {optimized_count} Jobs wurden optimiert'
})
except Exception as e:
app_logger.error(f"Fehler bei der Auto-Optimierung: {str(e)}")
return jsonify({
'success': False,
'error': f'Optimierung fehlgeschlagen: {str(e)}'
}), 500
@app.route('/api/optimization/settings', methods=['GET', 'POST'])
@login_required
def optimization_settings():
"""Optimierungs-Einstellungen abrufen und speichern"""
db_session = get_db_session()
if request.method == 'GET':
try:
# Standard-Einstellungen oder benutzerdefinierte laden
default_settings = {
'algorithm': 'round_robin',
'consider_distance': True,
'minimize_changeover': True,
'max_batch_size': 10,
'time_window': 24,
'auto_optimization_enabled': False
}
# Benutzereinstellungen aus der Session laden oder Standardwerte verwenden
user_settings = session.get('user_settings', {})
optimization_settings = user_settings.get('optimization', default_settings)
# Sicherstellen, dass alle erforderlichen Schlüssel vorhanden sind
for key, value in default_settings.items():
if key not in optimization_settings:
optimization_settings[key] = value
return jsonify({
'success': True,
'settings': optimization_settings
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Optimierungs-Einstellungen: {str(e)}")
return jsonify({
'success': False,
'error': 'Fehler beim Laden der Einstellungen'
}), 500
elif request.method == 'POST':
try:
settings = request.get_json()
# Validierung der Einstellungen
if not validate_optimization_settings(settings):
return jsonify({
'success': False,
'error': 'Ungültige Optimierungs-Einstellungen'
}), 400
# Einstellungen in der Session speichern
user_settings = session.get('user_settings', {})
if 'optimization' not in user_settings:
user_settings['optimization'] = {}
# Aktualisiere die Optimierungseinstellungen
user_settings['optimization'].update(settings)
session['user_settings'] = user_settings
# Einstellungen in der Datenbank speichern, wenn möglich
if hasattr(current_user, 'settings'):
import json
current_user.settings = json.dumps(user_settings)
current_user.updated_at = datetime.now()
db_session.commit()
app_logger.info(f"Optimierungs-Einstellungen für Benutzer {current_user.id} aktualisiert")
return jsonify({
'success': True,
'message': 'Optimierungs-Einstellungen erfolgreich gespeichert'
})
except Exception as e:
db_session.rollback()
app_logger.error(f"Fehler beim Speichern der Optimierungs-Einstellungen: {str(e)}")
return jsonify({
'success': False,
'error': f'Fehler beim Speichern der Einstellungen: {str(e)}'
}), 500
finally:
db_session.close()
# ===== OPTIMIERUNGS-ALGORITHMUS-FUNKTIONEN =====
def apply_round_robin_optimization(jobs, printers, db_session):
"""
Round-Robin-Optimierung: Gleichmäßige Verteilung der Jobs auf Drucker
Verteilt Jobs nacheinander auf verfügbare Drucker für optimale Balance
"""
optimized_count = 0
printer_index = 0
for job in jobs:
if printer_index >= len(printers):
printer_index = 0
# Job dem nächsten Drucker zuweisen
job.printer_id = printers[printer_index].id
job.assigned_at = datetime.now()
optimized_count += 1
printer_index += 1
return optimized_count
def apply_load_balance_optimization(jobs, printers, db_session):
"""
Load-Balancing-Optimierung: Jobs basierend auf aktueller Auslastung verteilen
Berücksichtigt die aktuelle Drucker-Auslastung für optimale Verteilung
"""
optimized_count = 0
# Aktuelle Drucker-Auslastung berechnen
printer_loads = {}
for printer in printers:
current_jobs = db_session.query(Job).filter(
Job.printer_id == printer.id,
Job.status.in_(['running', 'queued'])
).count()
printer_loads[printer.id] = current_jobs
for job in jobs:
# Drucker mit geringster Auslastung finden
min_load_printer_id = min(printer_loads, key=printer_loads.get)
job.printer_id = min_load_printer_id
job.assigned_at = datetime.now()
# Auslastung für nächste Iteration aktualisieren
printer_loads[min_load_printer_id] += 1
optimized_count += 1
return optimized_count
def apply_priority_optimization(jobs, printers, db_session):
"""
Prioritätsbasierte Optimierung: Jobs nach Priorität und verfügbaren Druckern verteilen
Hochpriorisierte Jobs erhalten bevorzugte Druckerzuweisung
"""
optimized_count = 0
# Jobs nach Priorität sortieren
priority_order = {'urgent': 1, 'high': 2, 'normal': 3, 'low': 4}
sorted_jobs = sorted(jobs, key=lambda j: priority_order.get(getattr(j, 'priority', 'normal'), 3))
# Hochpriorisierte Jobs den besten verfügbaren Druckern zuweisen
printer_assignments = {printer.id: 0 for printer in printers}
for job in sorted_jobs:
# Drucker mit geringster Anzahl zugewiesener Jobs finden
best_printer_id = min(printer_assignments, key=printer_assignments.get)
job.printer_id = best_printer_id
job.assigned_at = datetime.now()
printer_assignments[best_printer_id] += 1
optimized_count += 1
return optimized_count
def validate_optimization_settings(settings):
"""
Validiert die Optimierungs-Einstellungen auf Korrektheit und Sicherheit
Verhindert ungültige Parameter die das System beeinträchtigen könnten
"""
try:
# Algorithmus validieren
valid_algorithms = ['round_robin', 'load_balance', 'priority_based']
if settings.get('algorithm') not in valid_algorithms:
return False
# Numerische Werte validieren
max_batch_size = settings.get('max_batch_size', 10)
if not isinstance(max_batch_size, int) or max_batch_size < 1 or max_batch_size > 50:
return False
time_window = settings.get('time_window', 24)
if not isinstance(time_window, int) or time_window < 1 or time_window > 168:
return False
return True
except Exception:
return False
# ===== GASTANTRÄGE API-ROUTEN =====

Binary file not shown.

BIN
backend/database/myp.db-shm Normal file

Binary file not shown.

BIN
backend/database/myp.db-wal Normal file

Binary file not shown.

View File

@ -0,0 +1,290 @@
# Auto-Optimierung mit belohnendem Modal - Fehlerbehebung und Verbesserungen
## 📋 Übersicht
Dieses Dokument beschreibt die implementierten Verbesserungen für die Auto-Optimierung-Funktion der MYP-Plattform, einschließlich der Fehlerbehebung und der Hinzufügung eines belohnenden animierten Modals.
## 🐛 Behobene Fehler
### Problem 1: 404 Fehler bei Auto-Optimierung
**Symptom:** `POST http://127.0.0.1:5000/api/optimization/auto-optimize 404 (NOT FOUND)`
**Ursache:** Der API-Endpunkt `/api/optimization/auto-optimize` war nicht in der aktuellen `app.py` implementiert.
**Lösung:**
- Hinzufügung des fehlenden Endpunkts zur `app.py`
- Implementierung der unterstützenden Optimierungs-Algorithmus-Funktionen
- Vollständige Cascade-Analyse durchgeführt
### Problem 2: JSON-Parsing-Fehler
**Symptom:** `SyntaxError: Unexpected token '<', "<!DOCTYPE h..."`
**Ursache:** Frontend erwartete JSON-Antwort, erhielt aber HTML-Fehlerseite.
**Lösung:**
- Korrekte JSON-Responses implementiert
- Robuste Fehlerbehandlung hinzugefügt
- CSRF-Token-Handling verbessert
## 🚀 Neue Features
### 1. Belohnendes Animiertes Modal
Das neue Modal-System bietet ein außergewöhnlich motivierendes Benutzererlebnis:
#### Features:
- **Dynamische Erfolgsmeldungen** basierend auf Anzahl optimierter Jobs
- **Konfetti-Animation** mit fallenden bunten Partikeln
- **Animierte Emojis** mit pulsierenden und schwebenden Effekten
- **Erfolgsstatistiken** mit animierten Zählern
- **Belohnungs-Badge** mit Glow-Effekt
- **Audio-Feedback** (optional, browserabhängig)
- **Auto-Close** nach 10 Sekunden
#### Animationen:
```css
- Fade-in: Sanftes Einblenden des Modals
- Bounce-in: Federnder Eingang der Modal-Box
- Pulse-scale: Pulsierende Emoji-Animationen
- Float: Schwebende Sterne-Effekte
- Slide-up: Gestaffelte Einblend-Animationen
- Count-up: Animierte Zahlen-Animation
- Glow: Leuchtender Badge-Effekt
- Confetti-fall: Fallende Konfetti-Partikel
```
### 2. Ladeanimation
Während der Optimierung wird eine elegante Ladeanimation angezeigt:
- Drehender Spinner mit Glow-Effekt
- Motivierende Nachrichten
- Backdrop-Blur für fokussierte Aufmerksamkeit
### 3. Audio-Feedback
Optionale Erfolgstöne werden über die Web Audio API generiert:
- Harmonische Ton-Sequenz (C5 → E5 → G5)
- Graceful Degradation bei nicht unterstützten Browsern
## 🛠️ Technische Implementierung
### Backend-Endpunkte
#### `/api/optimization/auto-optimize` (POST)
**Beschreibung:** Führt automatische Job-Optimierung durch
**Request Body:**
```json
{
"settings": {
"algorithm": "round_robin|load_balance|priority_based",
"consider_distance": true,
"minimize_changeover": true,
"max_batch_size": 10,
"time_window": 24
},
"enabled": true
}
```
**Response:**
```json
{
"success": true,
"optimized_jobs": 5,
"algorithm": "round_robin",
"message": "Optimierung erfolgreich: 5 Jobs wurden optimiert"
}
```
#### `/api/optimization/settings` (GET/POST)
**Beschreibung:** Verwaltet Benutzer-Optimierungs-Einstellungen
### Optimierungs-Algorithmen
#### 1. Round Robin
- **Prinzip:** Gleichmäßige Verteilung auf alle verfügbaren Drucker
- **Verwendung:** Standard-Algorithmus für ausgewogene Auslastung
#### 2. Load Balancing
- **Prinzip:** Berücksichtigt aktuelle Drucker-Auslastung
- **Verwendung:** Optimiert für minimale Wartezeiten
#### 3. Priority-Based
- **Prinzip:** Hochpriorisierte Jobs erhalten bevorzugte Drucker
- **Verwendung:** Kritische Jobs werden priorisiert
### Frontend-Architektur
#### Klassenstruktur: OptimizationManager
```javascript
class OptimizationManager {
// Kern-Funktionalitäten
performAutoOptimization() // Führt Optimierung durch
showRewardModal(data) // Zeigt Belohnungs-Modal
generateConfetti() // Erzeugt Konfetti-Animation
// Ladezustände
showOptimizationLoading() // Zeigt Ladeanimation
hideOptimizationLoading() // Versteckt Ladeanimation
// Audio-Feedback
playSuccessSound() // Spielt Erfolgston ab
}
```
## 📱 Responsive Design
### Mobile Optimierungen:
- Reduzierte Konfetti-Größe auf kleineren Bildschirmen
- Angepasste Emoji-Größen für Touch-Geräte
- Vollbreite Modal-Darstellung auf mobilen Geräten
### CSS-Mediaqueries:
```css
@media (max-width: 768px) {
.confetti-piece { width: 6px; height: 6px; }
#optimization-reward-modal .text-8xl { font-size: 4rem; }
#optimization-reward-modal .max-w-md { max-width: 90vw; }
}
```
## 🎨 Design-Prinzipien
### Belohnungspsychologie:
1. **Sofortiges Feedback:** Modal erscheint unmittelbar nach Erfolg
2. **Visuelle Verstärkung:** Größere Erfolge = spektakulärere Animationen
3. **Fortschritts-Gefühl:** Statistiken zeigen konkrete Verbesserungen
4. **Positive Verstärkung:** Motivierende Nachrichten und Belohnungs-Badges
### Animation-Timing:
- **Eingangs-Animationen:** 0.3-0.6s für Aufmerksamkeit
- **Kontinuierliche Animationen:** 2-3s für subtile Bewegung
- **Ausgangs-Animationen:** 0.2-0.3s für sanftes Verschwinden
## 🔧 Wartung und Erweiterung
### Konfigurierbare Parameter:
```javascript
// In optimization-features.js
const MODAL_AUTO_CLOSE_DELAY = 10000; // 10 Sekunden
const CONFETTI_COUNT = 30; // Anzahl Konfetti-Partikel
const AUDIO_ENABLED = true; // Audio-Feedback aktiviert
```
### Erweiterungsmöglichkeiten:
1. **Neue Algorithmen:** Hinzufügung in `apply_*_optimization` Funktionen
2. **Mehr Animationen:** Erweiterung der CSS-Animationsbibliothek
3. **Gamification:** Achievement-System für häufige Optimierungen
4. **Personalisierung:** Benutzer-spezifische Animationseinstellungen
## 📊 Performance-Optimierungen
### CSS-Optimierungen:
```css
.animate-bounce-in,
.animate-fade-in {
will-change: transform, opacity; // GPU-Beschleunigung
}
```
### JavaScript-Optimierungen:
- **Event-Delegation:** Effiziente Event-Handler
- **Memory-Management:** Automatisches Cleanup von Modals
- **Throttling:** Begrenzung der Optimierungs-Frequenz
## 🧪 Testing-Strategien
### Frontend-Tests:
1. Modal-Erscheinung bei verschiedenen Erfolgszahlen
2. Animation-Performance auf verschiedenen Geräten
3. Graceful Degradation ohne JavaScript
4. CSRF-Token-Validierung
### Backend-Tests:
1. Algorithmus-Korrektheit mit verschiedenen Job-Verteilungen
2. Fehlerbehandlung bei fehlenden Druckern
3. Permissions-Validierung
4. Performance bei hoher Job-Anzahl
## 🔒 Sicherheitsaspekte
### CSRF-Schutz:
- Alle POST-Requests verwenden CSRF-Token
- Token-Validierung im Backend
### Input-Validierung:
```python
def validate_optimization_settings(settings):
valid_algorithms = ['round_robin', 'load_balance', 'priority_based']
if settings.get('algorithm') not in valid_algorithms:
return False
# Weitere Validierungen...
```
### Permission-Checks:
- Nur authentifizierte Benutzer können optimieren
- Admin-Level-Funktionen separat geschützt
## 📈 Monitoring und Analytics
### Logging:
```python
jobs_logger.info(f"Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert mit Algorithmus {algorithm}")
```
### Metriken:
- Anzahl durchgeführter Optimierungen
- Durchschnittliche Optimierungs-Dauer
- Benutzer-Engagement mit Modal-Features
## 🚨 Fehlerbehebung
### Häufige Probleme:
#### Modal erscheint nicht:
1. Browser-Konsole auf JavaScript-Fehler prüfen
2. CSS-Datei korrekt eingebunden?
3. CSRF-Token verfügbar?
#### Animationen ruckeln:
1. GPU-Beschleunigung aktiviert?
2. Zu viele parallele Animationen?
3. Performance-optimierte CSS-Properties verwendet?
#### Audio funktioniert nicht:
1. Browser unterstützt Web Audio API?
2. Benutzer-Interaktion vor Audio-Aufruf?
3. Audiokontext erstellt?
## 📝 Changelog
### Version 1.0.0 (Aktuell)
- ✅ Auto-Optimierung-Endpunkt implementiert
- ✅ Belohnendes Modal mit Animationen
- ✅ Drei Optimierungs-Algorithmen
- ✅ Audio-Feedback
- ✅ Responsive Design
- ✅ Performance-Optimierungen
- ✅ Umfassende Dokumentation
### Geplante Verbesserungen:
- [ ] Achievement-System
- [ ] Benutzer-spezifische Animation-Einstellungen
- [ ] Erweiterte Analytics
- [ ] A/B-Testing für Modal-Varianten
## 🤝 Beitragen
### Code-Standards:
- Deutsche Kommentare und Dokumentation
- Produktions-bereit implementieren
- Cascade-Analyse bei Änderungen
- Vollständige Fehlerbehandlung
### Pull-Request-Checklist:
- [ ] Funktionalität getestet
- [ ] Dokumentation aktualisiert
- [ ] Deutsche Kommentare hinzugefügt
- [ ] Performance-Impact evaluiert
- [ ] Mobile Responsivität geprüft

View File

@ -2508,3 +2508,9 @@
2025-05-31 23:29:42 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
2025-05-31 23:31:14 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1)
2025-05-31 23:31:14 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
2025-05-31 23:34:40 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
2025-05-31 23:34:44 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
2025-05-31 23:35:03 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1)
2025-05-31 23:35:03 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
2025-05-31 23:35:04 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
2025-05-31 23:35:14 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)

View File

@ -2683,3 +2683,6 @@
2025-05-31 23:32:11 - myp.scheduler - INFO - Scheduler gestartet
2025-05-31 23:32:16 - myp.scheduler - INFO - Scheduler-Thread beendet
2025-05-31 23:32:16 - myp.scheduler - INFO - Scheduler gestoppt
2025-05-31 23:34:21 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler-Thread gestartet
2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler gestartet

View File

@ -0,0 +1,341 @@
/**
* MYP Platform - Optimierungs-Animationen
* Belohnende und motivierende Animationen für Auto-Optimierung
*/
/* ===== MODAL FADE-IN ANIMATION ===== */
@keyframes fade-in {
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(8px);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
/* ===== BOUNCE-IN ANIMATION ===== */
@keyframes bounce-in {
0% {
opacity: 0;
transform: scale(0.3) translateY(-50px);
}
50% {
opacity: 1;
transform: scale(1.05) translateY(0);
}
70% {
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.animate-bounce-in {
animation: bounce-in 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* ===== PULSE-SCALE ANIMATION ===== */
@keyframes pulse-scale {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.animate-pulse-scale {
animation: pulse-scale 2s infinite ease-in-out;
}
/* ===== FLOATING ANIMATIONS ===== */
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
33% {
transform: translateY(-15px) rotate(5deg);
}
66% {
transform: translateY(-5px) rotate(-3deg);
}
}
@keyframes float-delay {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
33% {
transform: translateY(-10px) rotate(-5deg);
}
66% {
transform: translateY(-8px) rotate(3deg);
}
}
.animate-float {
animation: float 3s infinite ease-in-out;
}
.animate-float-delay {
animation: float-delay 3s infinite ease-in-out;
animation-delay: 1s;
}
/* ===== SLIDE-UP ANIMATIONS ===== */
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slide-up {
animation: slide-up 0.6s ease-out;
}
.animate-slide-up-delay {
animation: slide-up 0.6s ease-out;
animation-delay: 0.2s;
animation-fill-mode: both;
}
.animate-slide-up-delay-2 {
animation: slide-up 0.6s ease-out;
animation-delay: 0.4s;
animation-fill-mode: both;
}
.animate-slide-up-delay-3 {
animation: slide-up 0.6s ease-out;
animation-delay: 0.6s;
animation-fill-mode: both;
}
/* ===== COUNT-UP ANIMATION ===== */
@keyframes count-up {
from {
opacity: 0;
transform: scale(0.5) rotate(-10deg);
}
50% {
opacity: 1;
transform: scale(1.2) rotate(5deg);
}
to {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
.animate-count-up {
animation: count-up 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
animation-delay: 0.3s;
animation-fill-mode: both;
}
/* ===== GLOW ANIMATION ===== */
@keyframes glow {
0%, 100% {
box-shadow: 0 0 5px rgba(59, 130, 246, 0.5),
0 0 10px rgba(59, 130, 246, 0.3),
0 0 15px rgba(59, 130, 246, 0.1);
}
50% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.8),
0 0 30px rgba(59, 130, 246, 0.6),
0 0 40px rgba(59, 130, 246, 0.4);
}
}
.animate-glow {
animation: glow 2s infinite ease-in-out;
}
/* ===== KONFETTI ANIMATION ===== */
.confetti-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
}
.confetti-piece {
position: absolute;
width: 8px;
height: 8px;
top: -10px;
border-radius: 2px;
animation: confetti-fall linear infinite;
}
@keyframes confetti-fall {
0% {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* ===== ERFOLGS-BADGE SPEZIAL-ANIMATIONEN ===== */
@keyframes star-twinkle {
0%, 100% {
opacity: 0.6;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
.badge-star {
animation: star-twinkle 1.5s infinite ease-in-out;
}
/* ===== LOADING SPINNER IMPROVEMENTS ===== */
@keyframes spinner-glow {
0% {
filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5));
}
50% {
filter: drop-shadow(0 0 15px rgba(59, 130, 246, 0.8));
}
100% {
filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.5));
}
}
#optimization-loader .animate-spin {
animation: spin 1s linear infinite, spinner-glow 2s ease-in-out infinite;
}
/* ===== DARK MODE OPTIMIERUNGEN ===== */
@media (prefers-color-scheme: dark) {
.animate-glow {
animation: glow-dark 2s infinite ease-in-out;
}
@keyframes glow-dark {
0%, 100% {
box-shadow: 0 0 5px rgba(96, 165, 250, 0.5),
0 0 10px rgba(96, 165, 250, 0.3),
0 0 15px rgba(96, 165, 250, 0.1);
}
50% {
box-shadow: 0 0 20px rgba(96, 165, 250, 0.8),
0 0 30px rgba(96, 165, 250, 0.6),
0 0 40px rgba(96, 165, 250, 0.4);
}
}
}
/* ===== PERFORMANCE OPTIMIERUNGEN ===== */
.animate-bounce-in,
.animate-fade-in,
.animate-slide-up,
.animate-slide-up-delay,
.animate-slide-up-delay-2,
.animate-slide-up-delay-3 {
will-change: transform, opacity;
}
.animate-pulse-scale,
.animate-float,
.animate-float-delay {
will-change: transform;
}
.animate-glow {
will-change: box-shadow;
}
/* ===== RESPONSIVE ANPASSUNGEN ===== */
@media (max-width: 768px) {
.confetti-piece {
width: 6px;
height: 6px;
}
#optimization-reward-modal .text-8xl {
font-size: 4rem;
}
#optimization-reward-modal .max-w-md {
max-width: 90vw;
}
}
/* ===== BUTTON HOVER VERBESSERUNGEN ===== */
button:hover .badge-star {
animation-duration: 0.8s;
}
/* ===== ZUSÄTZLICHE UTILITY CLASSES ===== */
.animate-shake {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.animate-heartbeat {
animation: heartbeat 1.5s infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.1); }
28% { transform: scale(1); }
42% { transform: scale(1.1); }
70% { transform: scale(1); }
}
/* ===== PROGRESS BAR ANIMATION ===== */
.progress-fill {
animation: progress-grow 2s ease-out;
}
@keyframes progress-grow {
from {
width: 0%;
}
to {
width: var(--progress-width, 100%);
}
}
/* ===== SUCCESS CHECKMARK ANIMATION ===== */
.success-checkmark {
animation: checkmark-draw 0.8s ease-out;
}
@keyframes checkmark-draw {
0% {
stroke-dasharray: 0 100;
}
100% {
stroke-dasharray: 100 0;
}
}

View File

@ -210,6 +210,9 @@ class OptimizationManager {
*/
async performAutoOptimization() {
try {
// Ladeanimation anzeigen
this.showOptimizationLoading();
const response = await fetch('/api/optimization/auto-optimize', {
method: 'POST',
headers: {
@ -224,18 +227,249 @@ class OptimizationManager {
const data = await response.json();
// Ladeanimation ausblenden
this.hideOptimizationLoading();
if (data.success) {
this.showSuccessMessage(`Optimierung erfolgreich: ${data.optimized_jobs} Jobs optimiert`);
// Belohnendes animiertes Modal anzeigen
this.showRewardModal(data);
this.refreshCurrentView();
} else {
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
}
} catch (error) {
this.hideOptimizationLoading();
console.error('Auto-Optimierung Fehler:', error);
this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');
}
}
/**
* Belohnendes animiertes Modal für erfolgreiche Optimierung
*/
showRewardModal(data) {
// Existing Modal entfernen falls vorhanden
const existingModal = document.getElementById('optimization-reward-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.id = 'optimization-reward-modal';
modal.className = 'fixed inset-0 bg-black/70 backdrop-blur-md z-50 flex items-center justify-center p-4 animate-fade-in';
// Erfolgs-Emojis basierend auf Anzahl der optimierten Jobs
const optimizedCount = data.optimized_jobs || 0;
let celebration = '🎉';
let message = 'Optimierung erfolgreich!';
if (optimizedCount === 0) {
celebration = '✅';
message = 'System bereits optimal!';
} else if (optimizedCount <= 3) {
celebration = '🚀';
message = 'Kleine Verbesserungen durchgeführt!';
} else if (optimizedCount <= 10) {
celebration = '⚡';
message = 'Deutliche Optimierung erreicht!';
} else {
celebration = '💎';
message = 'Exzellente Optimierung!';
}
modal.innerHTML = `
<div class="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-md w-full p-8 transform animate-bounce-in">
<!-- Konfetti Animation -->
<div class="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl">
<div class="confetti-container">
${this.generateConfetti()}
</div>
</div>
<!-- Schließen Button -->
<button onclick="this.closest('#optimization-reward-modal').remove()"
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors z-10">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
<!-- Haupt-Content -->
<div class="text-center relative z-10">
<!-- Animiertes Erfolgs-Icon -->
<div class="relative mb-6">
<div class="text-8xl animate-pulse-scale mx-auto">${celebration}</div>
<div class="absolute -top-2 -right-2 text-4xl animate-float"></div>
<div class="absolute -bottom-2 -left-2 text-3xl animate-float-delay"></div>
</div>
<!-- Erfolgs-Nachricht -->
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-2 animate-slide-up">
${message}
</h2>
<!-- Statistiken -->
<div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20
rounded-xl p-6 mb-6 animate-slide-up-delay">
<div class="grid grid-cols-2 gap-4">
<div class="text-center">
<div class="text-3xl font-bold text-green-600 dark:text-green-400 animate-count-up">
${optimizedCount}
</div>
<div class="text-sm text-green-700 dark:text-green-300 font-medium">
Jobs optimiert
</div>
</div>
<div class="text-center">
<div class="text-lg font-semibold text-slate-700 dark:text-slate-300 capitalize">
${data.algorithm?.replace('_', ' ') || 'Standard'}
</div>
<div class="text-sm text-slate-600 dark:text-slate-400">
Algorithmus
</div>
</div>
</div>
</div>
<!-- Belohnungs-Badge -->
<div class="inline-flex items-center gap-2 bg-gradient-to-r from-blue-500 to-purple-600
text-white px-6 py-3 rounded-full text-sm font-semibold mb-6 animate-glow">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
</svg>
Effizienz-Boost erreicht!
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button onclick="this.closest('#optimization-reward-modal').remove()"
class="flex-1 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300
py-3 px-6 rounded-xl font-semibold transition-all hover:bg-slate-200
dark:hover:bg-slate-600 animate-slide-up-delay-2">
Schließen
</button>
<button onclick="optimizationManager.showOptimizationSettings();
this.closest('#optimization-reward-modal').remove();"
class="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white
py-3 px-6 rounded-xl font-semibold transition-all hover:from-blue-600
hover:to-blue-700 animate-slide-up-delay-3">
Einstellungen
</button>
</div>
</div>
</div>
`;
// Modal zum DOM hinzufügen
document.body.appendChild(modal);
// Sound-Effekt (optional)
this.playSuccessSound();
// Auto-Close nach 10 Sekunden
setTimeout(() => {
if (modal && modal.parentNode) {
modal.style.opacity = '0';
modal.style.transform = 'scale(0.95)';
setTimeout(() => modal.remove(), 300);
}
}, 10000);
}
/**
* Konfetti-Animation generieren
*/
generateConfetti() {
const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
let confetti = '';
for (let i = 0; i < 30; i++) {
const color = colors[Math.floor(Math.random() * colors.length)];
const delay = Math.random() * 3;
const duration = 3 + Math.random() * 2;
const left = Math.random() * 100;
confetti += `
<div class="confetti-piece" style="
background-color: ${color};
left: ${left}%;
animation-delay: ${delay}s;
animation-duration: ${duration}s;
"></div>
`;
}
return confetti;
}
/**
* Ladeanimation für Optimierung anzeigen
*/
showOptimizationLoading() {
const loader = document.createElement('div');
loader.id = 'optimization-loader';
loader.className = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-40 flex items-center justify-center';
loader.innerHTML = `
<div class="bg-white dark:bg-slate-800 rounded-2xl p-8 text-center max-w-sm mx-4 shadow-2xl">
<div class="relative mb-6">
<div class="w-16 h-16 mx-auto">
<svg class="animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<div class="absolute -top-1 -right-1 text-2xl animate-bounce"></div>
</div>
<h3 class="text-xl font-semibold text-slate-800 dark:text-white mb-2">
Optimierung läuft...
</h3>
<p class="text-slate-600 dark:text-slate-400">
Jobs werden intelligent verteilt
</p>
</div>
`;
document.body.appendChild(loader);
}
/**
* Ladeanimation ausblenden
*/
hideOptimizationLoading() {
const loader = document.getElementById('optimization-loader');
if (loader) {
loader.style.opacity = '0';
setTimeout(() => loader.remove(), 200);
}
}
/**
* Erfolgs-Sound abspielen (optional)
*/
playSuccessSound() {
try {
// Erstelle einen kurzen, angenehmen Ton
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
} catch (e) {
// Sound nicht verfügbar, ignorieren
console.log('Audio-API nicht verfügbar');
}
}
/**
* Batch-Operationen durchführen
*/

View File

@ -29,6 +29,7 @@
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/components.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/professional-theme.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/optimization-animations.css') }}" rel="stylesheet">
<!-- Preload critical resources -->
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">

View File

@ -0,0 +1,663 @@
"""
Erweiterte Formular-Validierung für das MYP-System
==================================================
Dieses Modul stellt umfassende Client- und serverseitige Validierung
mit benutzerfreundlichem UI-Feedback bereit.
Funktionen:
- Multi-Level-Validierung (Client/Server)
- Echtzeitvalidierung mit JavaScript
- Barrierefreie Fehlermeldungen
- Custom Validators für spezielle Anforderungen
- Automatische Sanitization von Eingaben
"""
import re
import html
import json
import logging
from typing import Dict, List, Any, Optional, Callable, Union
from datetime import datetime, timedelta
from flask import request, jsonify, session
from functools import wraps
from werkzeug.datastructures import FileStorage
from utils.logging_config import get_logger
from config.settings import ALLOWED_EXTENSIONS, MAX_FILE_SIZE
logger = get_logger("validation")
class ValidationError(Exception):
"""Custom Exception für Validierungsfehler"""
def __init__(self, message: str, field: str = None, code: str = None):
self.message = message
self.field = field
self.code = code
super().__init__(self.message)
class ValidationResult:
"""Ergebnis einer Validierung"""
def __init__(self):
self.is_valid = True
self.errors: Dict[str, List[str]] = {}
self.warnings: Dict[str, List[str]] = {}
self.cleaned_data: Dict[str, Any] = {}
def add_error(self, field: str, message: str):
"""Fügt einen Validierungsfehler hinzu"""
if field not in self.errors:
self.errors[field] = []
self.errors[field].append(message)
self.is_valid = False
def add_warning(self, field: str, message: str):
"""Fügt eine Warnung hinzu"""
if field not in self.warnings:
self.warnings[field] = []
self.warnings[field].append(message)
def to_dict(self) -> Dict[str, Any]:
"""Konvertiert das Ergebnis zu einem Dictionary"""
return {
"is_valid": self.is_valid,
"errors": self.errors,
"warnings": self.warnings,
"cleaned_data": self.cleaned_data
}
class BaseValidator:
"""Basis-Klasse für alle Validatoren"""
def __init__(self, required: bool = False, allow_empty: bool = True):
self.required = required
self.allow_empty = allow_empty
def validate(self, value: Any, field_name: str = None) -> ValidationResult:
"""Führt die Validierung durch"""
result = ValidationResult()
# Prüfung auf erforderliche Felder
if self.required and (value is None or value == ""):
result.add_error(field_name or "field", "Dieses Feld ist erforderlich.")
return result
# Wenn Wert leer und erlaubt, keine weitere Validierung
if not value and self.allow_empty:
result.cleaned_data[field_name or "field"] = value
return result
return self._validate_value(value, field_name, result)
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
"""Überschreibbar für spezifische Validierungslogik"""
result.cleaned_data[field_name or "field"] = value
return result
class StringValidator(BaseValidator):
"""Validator für String-Werte"""
def __init__(self, min_length: int = None, max_length: int = None,
pattern: str = None, trim: bool = True, **kwargs):
super().__init__(**kwargs)
self.min_length = min_length
self.max_length = max_length
self.pattern = re.compile(pattern) if pattern else None
self.trim = trim
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
# String konvertieren und trimmen
str_value = str(value)
if self.trim:
str_value = str_value.strip()
# Längenprüfung
if self.min_length is not None and len(str_value) < self.min_length:
result.add_error(field_name, f"Mindestlänge: {self.min_length} Zeichen")
if self.max_length is not None and len(str_value) > self.max_length:
result.add_error(field_name, f"Maximallänge: {self.max_length} Zeichen")
# Pattern-Prüfung
if self.pattern and not self.pattern.match(str_value):
result.add_error(field_name, "Format ist ungültig")
# HTML-Sanitization
cleaned_value = html.escape(str_value)
result.cleaned_data[field_name] = cleaned_value
return result
class EmailValidator(StringValidator):
"""Validator für E-Mail-Adressen"""
EMAIL_PATTERN = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
def __init__(self, **kwargs):
super().__init__(pattern=self.EMAIL_PATTERN, **kwargs)
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
result = super()._validate_value(value, field_name, result)
if result.is_valid:
# Normalisierung der E-Mail
email = str(value).lower().strip()
result.cleaned_data[field_name] = email
return result
class IntegerValidator(BaseValidator):
"""Validator für Integer-Werte"""
def __init__(self, min_value: int = None, max_value: int = None, **kwargs):
super().__init__(**kwargs)
self.min_value = min_value
self.max_value = max_value
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
try:
int_value = int(value)
except (ValueError, TypeError):
result.add_error(field_name, "Muss eine ganze Zahl sein")
return result
if self.min_value is not None and int_value < self.min_value:
result.add_error(field_name, f"Mindestwert: {self.min_value}")
if self.max_value is not None and int_value > self.max_value:
result.add_error(field_name, f"Maximalwert: {self.max_value}")
result.cleaned_data[field_name] = int_value
return result
class FloatValidator(BaseValidator):
"""Validator für Float-Werte"""
def __init__(self, min_value: float = None, max_value: float = None,
decimal_places: int = None, **kwargs):
super().__init__(**kwargs)
self.min_value = min_value
self.max_value = max_value
self.decimal_places = decimal_places
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
try:
float_value = float(value)
except (ValueError, TypeError):
result.add_error(field_name, "Muss eine Dezimalzahl sein")
return result
if self.min_value is not None and float_value < self.min_value:
result.add_error(field_name, f"Mindestwert: {self.min_value}")
if self.max_value is not None and float_value > self.max_value:
result.add_error(field_name, f"Maximalwert: {self.max_value}")
# Rundung auf bestimmte Dezimalstellen
if self.decimal_places is not None:
float_value = round(float_value, self.decimal_places)
result.cleaned_data[field_name] = float_value
return result
class DateTimeValidator(BaseValidator):
"""Validator für DateTime-Werte"""
def __init__(self, format_string: str = "%Y-%m-%d %H:%M",
min_date: datetime = None, max_date: datetime = None, **kwargs):
super().__init__(**kwargs)
self.format_string = format_string
self.min_date = min_date
self.max_date = max_date
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
if isinstance(value, datetime):
dt_value = value
else:
try:
dt_value = datetime.strptime(str(value), self.format_string)
except ValueError:
result.add_error(field_name, f"Ungültiges Datumsformat. Erwartet: {self.format_string}")
return result
if self.min_date and dt_value < self.min_date:
result.add_error(field_name, f"Datum muss nach {self.min_date.strftime('%d.%m.%Y')} liegen")
if self.max_date and dt_value > self.max_date:
result.add_error(field_name, f"Datum muss vor {self.max_date.strftime('%d.%m.%Y')} liegen")
result.cleaned_data[field_name] = dt_value
return result
class FileValidator(BaseValidator):
"""Validator für Datei-Uploads"""
def __init__(self, allowed_extensions: List[str] = None,
max_size_mb: int = None, min_size_kb: int = None, **kwargs):
super().__init__(**kwargs)
self.allowed_extensions = allowed_extensions or ALLOWED_EXTENSIONS
self.max_size_mb = max_size_mb or (MAX_FILE_SIZE / (1024 * 1024))
self.min_size_kb = min_size_kb
def _validate_value(self, value: Any, field_name: str, result: ValidationResult) -> ValidationResult:
if not isinstance(value, FileStorage):
result.add_error(field_name, "Muss eine gültige Datei sein")
return result
# Dateiname prüfen
if not value.filename:
result.add_error(field_name, "Dateiname ist erforderlich")
return result
# Dateierweiterung prüfen
extension = value.filename.rsplit('.', 1)[-1].lower() if '.' in value.filename else ''
if extension not in self.allowed_extensions:
result.add_error(field_name,
f"Nur folgende Dateiformate sind erlaubt: {', '.join(self.allowed_extensions)}")
# Dateigröße prüfen
value.seek(0, 2) # Zum Ende der Datei
file_size = value.tell()
value.seek(0) # Zurück zum Anfang
if self.max_size_mb and file_size > (self.max_size_mb * 1024 * 1024):
result.add_error(field_name, f"Datei zu groß. Maximum: {self.max_size_mb} MB")
if self.min_size_kb and file_size < (self.min_size_kb * 1024):
result.add_error(field_name, f"Datei zu klein. Minimum: {self.min_size_kb} KB")
result.cleaned_data[field_name] = value
return result
class FormValidator:
"""Haupt-Formular-Validator"""
def __init__(self):
self.fields: Dict[str, BaseValidator] = {}
self.custom_validators: List[Callable] = []
self.rate_limit_key = None
self.csrf_check = True
def add_field(self, name: str, validator: BaseValidator):
"""Fügt ein Feld mit Validator hinzu"""
self.fields[name] = validator
return self
def add_custom_validator(self, validator_func: Callable):
"""Fügt einen benutzerdefinierten Validator hinzu"""
self.custom_validators.append(validator_func)
return self
def set_rate_limit(self, key: str):
"""Setzt einen Rate-Limiting-Schlüssel"""
self.rate_limit_key = key
return self
def disable_csrf(self):
"""Deaktiviert CSRF-Prüfung für dieses Formular"""
self.csrf_check = False
return self
def validate(self, data: Dict[str, Any]) -> ValidationResult:
"""Validiert die gesamten Formulardaten"""
result = ValidationResult()
# Einzelfeldvalidierung
for field_name, validator in self.fields.items():
field_value = data.get(field_name)
field_result = validator.validate(field_value, field_name)
if not field_result.is_valid:
result.errors.update(field_result.errors)
result.is_valid = False
result.warnings.update(field_result.warnings)
result.cleaned_data.update(field_result.cleaned_data)
# Benutzerdefinierte Validierung
if result.is_valid:
for custom_validator in self.custom_validators:
try:
custom_result = custom_validator(result.cleaned_data)
if isinstance(custom_result, ValidationResult):
if not custom_result.is_valid:
result.errors.update(custom_result.errors)
result.is_valid = False
result.warnings.update(custom_result.warnings)
except Exception as e:
logger.error(f"Fehler bei benutzerdefinierter Validierung: {str(e)}")
result.add_error("form", "Unerwarteter Validierungsfehler")
return result
# Vordefinierte Formular-Validatoren
def get_user_registration_validator() -> FormValidator:
"""Validator für Benutzerregistrierung"""
return FormValidator() \
.add_field("username", StringValidator(min_length=3, max_length=50, required=True)) \
.add_field("email", EmailValidator(required=True)) \
.add_field("password", StringValidator(min_length=8, required=True)) \
.add_field("password_confirm", StringValidator(min_length=8, required=True)) \
.add_field("name", StringValidator(min_length=2, max_length=100, required=True)) \
.add_custom_validator(lambda data: _validate_password_match(data))
def get_job_creation_validator() -> FormValidator:
"""Validator für Job-Erstellung"""
return FormValidator() \
.add_field("name", StringValidator(min_length=1, max_length=200, required=True)) \
.add_field("description", StringValidator(max_length=500)) \
.add_field("printer_id", IntegerValidator(min_value=1, required=True)) \
.add_field("duration_minutes", IntegerValidator(min_value=1, max_value=1440, required=True)) \
.add_field("start_at", DateTimeValidator(min_date=datetime.now())) \
.add_field("file", FileValidator(required=True))
def get_printer_creation_validator() -> FormValidator:
"""Validator für Drucker-Erstellung"""
return FormValidator() \
.add_field("name", StringValidator(min_length=1, max_length=100, required=True)) \
.add_field("model", StringValidator(max_length=100)) \
.add_field("location", StringValidator(max_length=100)) \
.add_field("ip_address", StringValidator(pattern=r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')) \
.add_field("mac_address", StringValidator(pattern=r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', required=True)) \
.add_field("plug_ip", StringValidator(pattern=r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', required=True)) \
.add_field("plug_username", StringValidator(min_length=1, required=True)) \
.add_field("plug_password", StringValidator(min_length=1, required=True))
def get_guest_request_validator() -> FormValidator:
"""Validator für Gastanfragen"""
return FormValidator() \
.add_field("name", StringValidator(min_length=2, max_length=100, required=True)) \
.add_field("email", EmailValidator()) \
.add_field("reason", StringValidator(min_length=10, max_length=500, required=True)) \
.add_field("duration_minutes", IntegerValidator(min_value=5, max_value=480, required=True)) \
.add_field("copies", IntegerValidator(min_value=1, max_value=10)) \
.add_field("file", FileValidator(required=True)) \
.set_rate_limit("guest_request")
def _validate_password_match(data: Dict[str, Any]) -> ValidationResult:
"""Validiert, ob Passwörter übereinstimmen"""
result = ValidationResult()
password = data.get("password")
password_confirm = data.get("password_confirm")
if password != password_confirm:
result.add_error("password_confirm", "Passwörter stimmen nicht überein")
return result
# Decorator für automatische Formularvalidierung
def validate_form(validator_func: Callable[[], FormValidator]):
"""Decorator für automatische Formularvalidierung"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
# Validator erstellen
validator = validator_func()
# Daten aus Request extrahieren
if request.is_json:
data = request.get_json() or {}
else:
data = dict(request.form)
# Dateien hinzufügen
for key, file in request.files.items():
data[key] = file
# Validierung durchführen
validation_result = validator.validate(data)
# Bei Fehlern JSON-Response zurückgeben
if not validation_result.is_valid:
logger.warning(f"Validierungsfehler für {request.endpoint}: {validation_result.errors}")
return jsonify({
"success": False,
"errors": validation_result.errors,
"warnings": validation_result.warnings
}), 400
# Gereinigte Daten an die Request anhängen
request.validated_data = validation_result.cleaned_data
request.validation_warnings = validation_result.warnings
return f(*args, **kwargs)
except Exception as e:
logger.error(f"Fehler bei Formularvalidierung: {str(e)}")
return jsonify({
"success": False,
"errors": {"form": ["Unerwarteter Validierungsfehler"]}
}), 500
return decorated_function
return decorator
# JavaScript für Client-seitige Validierung
def get_client_validation_js() -> str:
"""Generiert JavaScript für Client-seitige Validierung"""
return """
class FormValidator {
constructor(formId, validationRules = {}) {
this.form = document.getElementById(formId);
this.rules = validationRules;
this.errors = {};
this.setupEventListeners();
}
setupEventListeners() {
if (!this.form) return;
// Echtzeit-Validierung bei Eingabe
this.form.addEventListener('input', (e) => {
this.validateField(e.target);
});
// Formular-Submission
this.form.addEventListener('submit', (e) => {
if (!this.validateForm()) {
e.preventDefault();
}
});
}
validateField(field) {
const fieldName = field.name;
const value = field.value;
const rule = this.rules[fieldName];
if (!rule) return true;
this.clearFieldError(field);
// Required-Prüfung
if (rule.required && (!value || value.trim() === '')) {
this.addFieldError(field, 'Dieses Feld ist erforderlich.');
return false;
}
// Längenprüfung
if (rule.minLength && value.length < rule.minLength) {
this.addFieldError(field, `Mindestlänge: ${rule.minLength} Zeichen`);
return false;
}
if (rule.maxLength && value.length > rule.maxLength) {
this.addFieldError(field, `Maximallänge: ${rule.maxLength} Zeichen`);
return false;
}
// Pattern-Prüfung
if (rule.pattern && !new RegExp(rule.pattern).test(value)) {
this.addFieldError(field, rule.patternMessage || 'Format ist ungültig');
return false;
}
// Email-Prüfung
if (rule.type === 'email' && value) {
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailPattern.test(value)) {
this.addFieldError(field, 'Bitte geben Sie eine gültige E-Mail-Adresse ein');
return false;
}
}
// Custom Validierung
if (rule.customValidator) {
const customResult = rule.customValidator(value, field);
if (customResult !== true) {
this.addFieldError(field, customResult);
return false;
}
}
return true;
}
validateForm() {
let isValid = true;
this.errors = {};
// Alle Felder validieren
const fields = this.form.querySelectorAll('input, textarea, select');
fields.forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
// Custom Form-Validierung
if (this.rules._formValidator) {
const formData = new FormData(this.form);
const customResult = this.rules._formValidator(formData, this.form);
if (customResult !== true) {
this.addFormError(customResult);
isValid = false;
}
}
return isValid;
}
addFieldError(field, message) {
const fieldName = field.name;
// Error-Container finden oder erstellen
let errorContainer = field.parentNode.querySelector('.field-error');
if (!errorContainer) {
errorContainer = document.createElement('div');
errorContainer.className = 'field-error text-red-600 text-sm mt-1';
errorContainer.setAttribute('role', 'alert');
errorContainer.setAttribute('aria-live', 'polite');
field.parentNode.appendChild(errorContainer);
}
errorContainer.textContent = message;
field.classList.add('border-red-500');
field.setAttribute('aria-invalid', 'true');
// Für Screen Reader
if (!field.getAttribute('aria-describedby')) {
const errorId = `error-${fieldName}-${Date.now()}`;
errorContainer.id = errorId;
field.setAttribute('aria-describedby', errorId);
}
this.errors[fieldName] = message;
}
clearFieldError(field) {
const errorContainer = field.parentNode.querySelector('.field-error');
if (errorContainer) {
errorContainer.remove();
}
field.classList.remove('border-red-500');
field.removeAttribute('aria-invalid');
field.removeAttribute('aria-describedby');
delete this.errors[field.name];
}
addFormError(message) {
let formErrorContainer = this.form.querySelector('.form-error');
if (!formErrorContainer) {
formErrorContainer = document.createElement('div');
formErrorContainer.className = 'form-error bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4';
formErrorContainer.setAttribute('role', 'alert');
this.form.insertBefore(formErrorContainer, this.form.firstChild);
}
formErrorContainer.textContent = message;
}
clearFormErrors() {
const formErrorContainer = this.form.querySelector('.form-error');
if (formErrorContainer) {
formErrorContainer.remove();
}
}
showServerErrors(errors) {
// Server-Fehler anzeigen
for (const [fieldName, messages] of Object.entries(errors)) {
const field = this.form.querySelector(`[name="${fieldName}"]`);
if (field && messages.length > 0) {
this.addFieldError(field, messages[0]);
}
}
}
}
// Utility-Funktionen
window.FormValidationUtils = {
// Passwort-Stärke prüfen
validatePasswordStrength: (password) => {
if (password.length < 8) return 'Passwort muss mindestens 8 Zeichen lang sein';
if (!/[A-Z]/.test(password)) return 'Passwort muss mindestens einen Großbuchstaben enthalten';
if (!/[a-z]/.test(password)) return 'Passwort muss mindestens einen Kleinbuchstaben enthalten';
if (!/[0-9]/.test(password)) return 'Passwort muss mindestens eine Zahl enthalten';
return true;
},
// Passwort-Bestätigung prüfen
validatePasswordConfirm: (password, confirm) => {
return password === confirm ? true : 'Passwörter stimmen nicht überein';
},
// Datei-Validierung
validateFile: (file, allowedTypes = [], maxSizeMB = 10) => {
if (!file) return 'Bitte wählen Sie eine Datei aus';
const fileType = file.name.split('.').pop().toLowerCase();
if (allowedTypes.length > 0 && !allowedTypes.includes(fileType)) {
return `Nur folgende Dateiformate sind erlaubt: ${allowedTypes.join(', ')}`;
}
if (file.size > maxSizeMB * 1024 * 1024) {
return `Datei ist zu groß. Maximum: ${maxSizeMB} MB`;
}
return true;
}
};
"""
def render_validation_errors(errors: Dict[str, List[str]]) -> str:
"""Rendert Validierungsfehler als HTML"""
if not errors:
return ""
html_parts = ['<div class="validation-errors">']
for field, messages in errors.items():
for message in messages:
html_parts.append(
f'<div class="error-message bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-2" role="alert">'
f'<strong>{field}:</strong> {html.escape(message)}'
f'</div>'
)
html_parts.append('</div>')
return '\n'.join(html_parts)