📝 "Refactor backend
This commit is contained in:
parent
f5a39a450a
commit
831caec87a
308
backend/app.py
308
backend/app.py
@ -4828,34 +4828,32 @@ def session_status():
|
|||||||
@app.route('/api/session/extend', methods=['POST'])
|
@app.route('/api/session/extend', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def extend_session():
|
def extend_session():
|
||||||
"""
|
"""Verlängert die aktuelle Session um die Standard-Lebensdauer"""
|
||||||
Verlängert die aktuelle Session um weitere Zeit.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
data = request.get_json() or {}
|
# Session-Lebensdauer zurücksetzen
|
||||||
extend_minutes = data.get('extend_minutes', 30)
|
session.permanent = True
|
||||||
|
|
||||||
# Begrenzen der Verlängerung (max 2 Stunden)
|
# Aktivität für Rate Limiting aktualisieren
|
||||||
extend_minutes = min(extend_minutes, 120)
|
current_user.update_last_activity()
|
||||||
|
|
||||||
now = datetime.now()
|
# Optional: Session-Statistiken für Admin
|
||||||
session['last_activity'] = now.isoformat()
|
user_agent = request.headers.get('User-Agent', 'Unknown')
|
||||||
session['session_extended'] = now.isoformat()
|
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
||||||
session['extended_by_minutes'] = extend_minutes
|
|
||||||
|
|
||||||
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({
|
return jsonify({
|
||||||
"success": True,
|
'success': True,
|
||||||
"message": f"Session um {extend_minutes} Minuten verlängert",
|
'message': 'Session erfolgreich verlängert',
|
||||||
"extended_until": (now + timedelta(minutes=extend_minutes)).isoformat(),
|
'expires_at': (datetime.now() + SESSION_LIFETIME).isoformat()
|
||||||
"extended_minutes": extend_minutes
|
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
auth_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
|
app_logger.error(f"Fehler beim Verlängern der Session: {str(e)}")
|
||||||
return jsonify({"error": "Session-Verlängerung fehlgeschlagen"}), 500
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Fehler beim Verlängern der Session'
|
||||||
|
}), 500
|
||||||
|
|
||||||
# ===== GASTANTRÄGE API-ROUTEN =====
|
# ===== GASTANTRÄGE API-ROUTEN =====
|
||||||
|
|
||||||
@ -5669,3 +5667,273 @@ if __name__ == "__main__":
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
sys.exit(1)
|
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
BIN
backend/database/myp.db-shm
Normal file
Binary file not shown.
BIN
backend/database/myp.db-wal
Normal file
BIN
backend/database/myp.db-wal
Normal file
Binary file not shown.
290
backend/docs/AUTO_OPTIMIERUNG_MODAL_VERBESSERUNGEN.md
Normal file
290
backend/docs/AUTO_OPTIMIERUNG_MODAL_VERBESSERUNGEN.md
Normal 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
|
@ -2508,3 +2508,9 @@
|
|||||||
2025-05-31 23:29:42 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
|
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 von Benutzer Administrator (ID: 1)
|
||||||
2025-05-31 23:31:14 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
|
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)
|
||||||
|
@ -2683,3 +2683,6 @@
|
|||||||
2025-05-31 23:32:11 - myp.scheduler - INFO - Scheduler gestartet
|
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-Thread beendet
|
||||||
2025-05-31 23:32:16 - myp.scheduler - INFO - Scheduler gestoppt
|
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
|
||||||
|
341
backend/static/css/optimization-animations.css
Normal file
341
backend/static/css/optimization-animations.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -210,6 +210,9 @@ class OptimizationManager {
|
|||||||
*/
|
*/
|
||||||
async performAutoOptimization() {
|
async performAutoOptimization() {
|
||||||
try {
|
try {
|
||||||
|
// Ladeanimation anzeigen
|
||||||
|
this.showOptimizationLoading();
|
||||||
|
|
||||||
const response = await fetch('/api/optimization/auto-optimize', {
|
const response = await fetch('/api/optimization/auto-optimize', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -224,18 +227,249 @@ class OptimizationManager {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Ladeanimation ausblenden
|
||||||
|
this.hideOptimizationLoading();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.showSuccessMessage(`Optimierung erfolgreich: ${data.optimized_jobs} Jobs optimiert`);
|
// Belohnendes animiertes Modal anzeigen
|
||||||
|
this.showRewardModal(data);
|
||||||
this.refreshCurrentView();
|
this.refreshCurrentView();
|
||||||
} else {
|
} else {
|
||||||
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
|
this.showErrorMessage(`Optimierung fehlgeschlagen: ${data.error}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.hideOptimizationLoading();
|
||||||
console.error('Auto-Optimierung Fehler:', error);
|
console.error('Auto-Optimierung Fehler:', error);
|
||||||
this.showErrorMessage('Netzwerkfehler bei der Auto-Optimierung');
|
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
|
* Batch-Operationen durchführen
|
||||||
*/
|
*/
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<link href="{{ url_for('static', filename='css/tailwind.min.css') }}" rel="stylesheet">
|
<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/components.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', filename='css/professional-theme.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 -->
|
<!-- Preload critical resources -->
|
||||||
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
|
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
|
||||||
|
663
backend/utils/form_validation.py
Normal file
663
backend/utils/form_validation.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user