diff --git a/backend/app.py b/backend/app.py index 90a89a71..965aab55 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5439,237 +5439,9 @@ def export_guest_requests(): }), 500 -# ===== STARTUP UND MAIN ===== -if __name__ == "__main__": - import sys - import signal - import os - - # Debug-Modus prüfen - debug_mode = len(sys.argv) > 1 and sys.argv[1] == "--debug" - - # Windows-spezifische Umgebungsvariablen setzen für bessere Flask-Kompatibilität - if os.name == 'nt' and debug_mode: - # Entferne problematische Werkzeug-Variablen - os.environ.pop('WERKZEUG_SERVER_FD', None) - os.environ.pop('WERKZEUG_RUN_MAIN', None) - - # Setze saubere Umgebung - os.environ['FLASK_ENV'] = 'development' - os.environ['PYTHONIOENCODING'] = 'utf-8' - os.environ['PYTHONUTF8'] = '1' - - # Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown - def signal_handler(sig, frame): - """Signal-Handler für ordnungsgemäßes Shutdown.""" - app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...") - try: - # Queue Manager stoppen - app_logger.info("🔄 Beende Queue Manager...") - stop_queue_manager() - - # Scheduler stoppen falls aktiviert - if SCHEDULER_ENABLED and scheduler: - try: - scheduler.stop() - app_logger.info("Job-Scheduler gestoppt") - except Exception as e: - app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}") - - # ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN ===== - app_logger.info("💾 Führe Datenbank-Cleanup durch...") - try: - from models import get_db_session, create_optimized_engine - from sqlalchemy import text - - # WAL-Checkpoint ausführen um .shm und .wal Dateien zu bereinigen - engine = create_optimized_engine() - - with engine.connect() as conn: - # Vollständiger WAL-Checkpoint (TRUNCATE-Modus) - app_logger.info("📝 Führe WAL-Checkpoint durch...") - result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() - - if result: - app_logger.info(f"WAL-Checkpoint abgeschlossen: {result[1]} Seiten übertragen, {result[2]} Seiten zurückgesetzt") - - # Alle pending Transaktionen committen - conn.commit() - - # Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien) - app_logger.info("📁 Schalte Journal-Mode um...") - conn.execute(text("PRAGMA journal_mode=DELETE")) - - # Optimize und Vacuum für sauberen Zustand - conn.execute(text("PRAGMA optimize")) - conn.execute(text("VACUUM")) - - conn.commit() - - # Engine-Connection-Pool schließen - engine.dispose() - - app_logger.info("✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein") - - except Exception as db_error: - app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}") - - app_logger.info("✅ Shutdown abgeschlossen") - sys.exit(0) - except Exception as e: - app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}") - sys.exit(1) - - # Signal-Handler registrieren (Windows-kompatibel) - if os.name == 'nt': # Windows - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - # Zusätzlich für Flask-Development-Server - signal.signal(signal.SIGBREAK, signal_handler) - else: # Unix/Linux - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) - - try: - # Datenbank initialisieren - init_database() - create_initial_admin() - - # Template-Hilfsfunktionen registrieren - register_template_helpers(app) - - # Drucker-Monitor Steckdosen-Initialisierung beim Start - try: - app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...") - initialization_results = printer_monitor.initialize_all_outlets_on_startup() - - if initialization_results: - success_count = sum(1 for success in initialization_results.values() if success) - total_count = len(initialization_results) - app_logger.info(f"✅ Steckdosen-Initialisierung: {success_count}/{total_count} Drucker erfolgreich") - - if success_count < total_count: - app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden") - else: - app_logger.info("ℹ️ Keine Drucker zur Initialisierung gefunden") - - except Exception as e: - app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}") - - # Queue-Manager für automatische Drucker-Überwachung starten - # Nur im Produktionsmodus starten (nicht im Debug-Modus) - if not debug_mode: - try: - queue_manager = start_queue_manager() - app_logger.info("✅ Printer Queue Manager erfolgreich gestartet") - - # Verbesserte Shutdown-Handler registrieren - def cleanup_queue_manager(): - try: - app_logger.info("🔄 Beende Queue Manager...") - stop_queue_manager() - except Exception as e: - app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}") - - atexit.register(cleanup_queue_manager) - - # ===== DATENBANK-CLEANUP BEIM PROGRAMMENDE ===== - def cleanup_database(): - """Führt Datenbank-Cleanup beim normalen Programmende aus.""" - try: - app_logger.info("💾 Führe finales Datenbank-Cleanup durch...") - from models import create_optimized_engine - from sqlalchemy import text - - engine = create_optimized_engine() - - with engine.connect() as conn: - # WAL-Checkpoint für sauberes Beenden - result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() - if result and result[1] > 0: - app_logger.info(f"Final WAL-Checkpoint: {result[1]} Seiten übertragen") - - # Journal-Mode umschalten um .wal/.shm Dateien zu entfernen - conn.execute(text("PRAGMA journal_mode=DELETE")) - conn.commit() - - # Connection-Pool ordnungsgemäß schließen - engine.dispose() - app_logger.info("✅ Finales Datenbank-Cleanup abgeschlossen") - - except Exception as e: - app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}") - - atexit.register(cleanup_database) - - except Exception as e: - app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") - else: - app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung") - - # Scheduler starten (falls aktiviert) - if SCHEDULER_ENABLED: - try: - scheduler.start() - app_logger.info("Job-Scheduler gestartet") - except Exception as e: - app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}") - - if debug_mode: - # Debug-Modus: HTTP auf Port 5000 - app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)") - - # Windows-spezifische Flask-Konfiguration - run_kwargs = { - "host": "0.0.0.0", - "port": 5000, - "debug": True, - "threaded": True - } - - if os.name == 'nt': - # Windows: Deaktiviere Auto-Reload um WERKZEUG_SERVER_FD Fehler zu vermeiden - run_kwargs["use_reloader"] = False - run_kwargs["passthrough_errors"] = False - app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert") - - app.run(**run_kwargs) - else: - # Produktions-Modus: HTTPS auf Port 443 - ssl_context = get_ssl_context() - - if ssl_context: - app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443") - app.run( - host="0.0.0.0", - port=443, - debug=False, - ssl_context=ssl_context, - threaded=True - ) - else: - app_logger.info("Starte HTTP-Server auf 0.0.0.0:80") - app.run( - host="0.0.0.0", - port=80, - debug=False, - threaded=True - ) - except KeyboardInterrupt: - app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...") - signal_handler(signal.SIGINT, None) - except Exception as e: - app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}") - # Cleanup bei Fehler - try: - stop_queue_manager() - except: - pass - sys.exit(1) - # ===== AUTO-OPTIMIERUNG-API-ENDPUNKTE ===== + @app.route('/api/optimization/auto-optimize', methods=['POST']) @login_required def auto_optimize_jobs(): @@ -5936,4 +5708,233 @@ def validate_optimization_settings(settings): except Exception: return False -# ===== GASTANTRÄGE API-ROUTEN ===== \ No newline at end of file +# ===== GASTANTRÄGE API-ROUTEN ===== + +# ===== STARTUP UND MAIN ===== +if __name__ == "__main__": + import sys + import signal + import os + + # Debug-Modus prüfen + debug_mode = len(sys.argv) > 1 and sys.argv[1] == "--debug" + + # Windows-spezifische Umgebungsvariablen setzen für bessere Flask-Kompatibilität + if os.name == 'nt' and debug_mode: + # Entferne problematische Werkzeug-Variablen + os.environ.pop('WERKZEUG_SERVER_FD', None) + os.environ.pop('WERKZEUG_RUN_MAIN', None) + + # Setze saubere Umgebung + os.environ['FLASK_ENV'] = 'development' + os.environ['PYTHONIOENCODING'] = 'utf-8' + os.environ['PYTHONUTF8'] = '1' + + # Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown + def signal_handler(sig, frame): + """Signal-Handler für ordnungsgemäßes Shutdown.""" + app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...") + try: + # Queue Manager stoppen + app_logger.info("🔄 Beende Queue Manager...") + stop_queue_manager() + + # Scheduler stoppen falls aktiviert + if SCHEDULER_ENABLED and scheduler: + try: + scheduler.stop() + app_logger.info("Job-Scheduler gestoppt") + except Exception as e: + app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}") + + # ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN ===== + app_logger.info("💾 Führe Datenbank-Cleanup durch...") + try: + from models import get_db_session, create_optimized_engine + from sqlalchemy import text + + # WAL-Checkpoint ausführen um .shm und .wal Dateien zu bereinigen + engine = create_optimized_engine() + + with engine.connect() as conn: + # Vollständiger WAL-Checkpoint (TRUNCATE-Modus) + app_logger.info("📝 Führe WAL-Checkpoint durch...") + result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() + + if result: + app_logger.info(f"WAL-Checkpoint abgeschlossen: {result[1]} Seiten übertragen, {result[2]} Seiten zurückgesetzt") + + # Alle pending Transaktionen committen + conn.commit() + + # Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien) + app_logger.info("📁 Schalte Journal-Mode um...") + conn.execute(text("PRAGMA journal_mode=DELETE")) + + # Optimize und Vacuum für sauberen Zustand + conn.execute(text("PRAGMA optimize")) + conn.execute(text("VACUUM")) + + conn.commit() + + # Engine-Connection-Pool schließen + engine.dispose() + + app_logger.info("✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein") + + except Exception as db_error: + app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}") + + app_logger.info("✅ Shutdown abgeschlossen") + sys.exit(0) + except Exception as e: + app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}") + sys.exit(1) + + # Signal-Handler registrieren (Windows-kompatibel) + if os.name == 'nt': # Windows + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + # Zusätzlich für Flask-Development-Server + signal.signal(signal.SIGBREAK, signal_handler) + else: # Unix/Linux + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) + + try: + # Datenbank initialisieren + init_database() + create_initial_admin() + + # Template-Hilfsfunktionen registrieren + register_template_helpers(app) + + # Drucker-Monitor Steckdosen-Initialisierung beim Start + try: + app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...") + initialization_results = printer_monitor.initialize_all_outlets_on_startup() + + if initialization_results: + success_count = sum(1 for success in initialization_results.values() if success) + total_count = len(initialization_results) + app_logger.info(f"✅ Steckdosen-Initialisierung: {success_count}/{total_count} Drucker erfolgreich") + + if success_count < total_count: + app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden") + else: + app_logger.info("ℹ️ Keine Drucker zur Initialisierung gefunden") + + except Exception as e: + app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}") + + # Queue-Manager für automatische Drucker-Überwachung starten + # Nur im Produktionsmodus starten (nicht im Debug-Modus) + if not debug_mode: + try: + queue_manager = start_queue_manager() + app_logger.info("✅ Printer Queue Manager erfolgreich gestartet") + + # Verbesserte Shutdown-Handler registrieren + def cleanup_queue_manager(): + try: + app_logger.info("🔄 Beende Queue Manager...") + stop_queue_manager() + except Exception as e: + app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}") + + atexit.register(cleanup_queue_manager) + + # ===== DATENBANK-CLEANUP BEIM PROGRAMMENDE ===== + def cleanup_database(): + """Führt Datenbank-Cleanup beim normalen Programmende aus.""" + try: + app_logger.info("💾 Führe finales Datenbank-Cleanup durch...") + from models import create_optimized_engine + from sqlalchemy import text + + engine = create_optimized_engine() + + with engine.connect() as conn: + # WAL-Checkpoint für sauberes Beenden + result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() + if result and result[1] > 0: + app_logger.info(f"Final WAL-Checkpoint: {result[1]} Seiten übertragen") + + # Journal-Mode umschalten um .wal/.shm Dateien zu entfernen + conn.execute(text("PRAGMA journal_mode=DELETE")) + conn.commit() + + # Connection-Pool ordnungsgemäß schließen + engine.dispose() + app_logger.info("✅ Finales Datenbank-Cleanup abgeschlossen") + + except Exception as e: + app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}") + + atexit.register(cleanup_database) + + except Exception as e: + app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") + else: + app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung") + + # Scheduler starten (falls aktiviert) + if SCHEDULER_ENABLED: + try: + scheduler.start() + app_logger.info("Job-Scheduler gestartet") + except Exception as e: + app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}") + + if debug_mode: + # Debug-Modus: HTTP auf Port 5000 + app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)") + + # Windows-spezifische Flask-Konfiguration + run_kwargs = { + "host": "0.0.0.0", + "port": 5000, + "debug": True, + "threaded": True + } + + if os.name == 'nt': + # Windows: Deaktiviere Auto-Reload um WERKZEUG_SERVER_FD Fehler zu vermeiden + run_kwargs["use_reloader"] = False + run_kwargs["passthrough_errors"] = False + app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert") + + app.run(**run_kwargs) + else: + # Produktions-Modus: HTTPS auf Port 443 + ssl_context = get_ssl_context() + + if ssl_context: + app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443") + app.run( + host="0.0.0.0", + port=443, + debug=False, + ssl_context=ssl_context, + threaded=True + ) + else: + app_logger.info("Starte HTTP-Server auf 0.0.0.0:80") + app.run( + host="0.0.0.0", + port=80, + debug=False, + threaded=True + ) + except KeyboardInterrupt: + app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...") + signal_handler(signal.SIGINT, None) + except Exception as e: + app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}") + # Cleanup bei Fehler + try: + stop_queue_manager() + except: + pass + sys.exit(1) \ No newline at end of file diff --git a/backend/blueprints/printers.py b/backend/blueprints/printers.py index 15f4cb1e..1ce48a84 100644 --- a/backend/blueprints/printers.py +++ b/backend/blueprints/printers.py @@ -175,6 +175,441 @@ def control_printer_power(printer_id): except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}") + return jsonify({ + "success": False, + "error": f"Allgemeiner Fehler: {str(e)}" + }), 500 + +@printers_blueprint.route("/test/socket/", methods=["GET"]) +@login_required +@require_permission(Permission.ADMIN) +@measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Status") +def test_socket_status(printer_id): + """ + Prüft den aktuellen Status einer Steckdose für Testzwecke (nur für Ausbilder/Administratoren). + + Args: + printer_id: ID des Druckers dessen Steckdose getestet werden soll + + Returns: + JSON mit detailliertem Status der Steckdose und Warnungen + """ + printers_logger.info(f"🔍 Steckdosen-Test-Status für Drucker {printer_id} von Admin {current_user.name}") + + try: + # Drucker aus Datenbank holen + db_session = get_db_session() + printer = db_session.query(Printer).filter(Printer.id == printer_id).first() + + if not printer: + db_session.close() + return jsonify({ + "success": False, + "error": f"Drucker mit ID {printer_id} nicht gefunden" + }), 404 + + # Prüfen, ob Drucker eine Steckdose konfiguriert hat + if not printer.plug_ip or not printer.plug_username or not printer.plug_password: + db_session.close() + return jsonify({ + "success": False, + "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert", + "warning": "Steckdose kann nicht getestet werden - Konfiguration fehlt" + }), 400 + + # Prüfen, ob der Drucker gerade aktive Jobs hat + active_jobs = db_session.query(Job).filter( + Job.printer_id == printer_id, + Job.status.in_(["running", "printing", "active"]) + ).all() + + db_session.close() + + # Steckdosen-Status prüfen + from PyP100 import PyP110 + socket_status = None + socket_info = None + error_message = None + + try: + # TP-Link Tapo P110 Verbindung herstellen + p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) + p110.handshake() # Authentifizierung + p110.login() # Login + + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() + socket_status = "online" if device_info["result"]["device_on"] else "offline" + + # Energieverbrauch abrufen (falls verfügbar) + try: + energy_info = p110.getEnergyUsage() + current_power = energy_info.get("result", {}).get("current_power", 0) + except: + current_power = None + + socket_info = { + "device_on": device_info["result"]["device_on"], + "signal_level": device_info["result"].get("signal_level", 0), + "current_power": current_power, + "device_id": device_info["result"].get("device_id", "Unbekannt"), + "model": device_info["result"].get("model", "Unbekannt"), + "hw_ver": device_info["result"].get("hw_ver", "Unbekannt"), + "fw_ver": device_info["result"].get("fw_ver", "Unbekannt") + } + + except Exception as e: + printers_logger.warning(f"⚠️ Fehler bei Steckdosen-Status-Abfrage für {printer.name}: {str(e)}") + socket_status = "error" + error_message = str(e) + + # Warnungen und Empfehlungen zusammenstellen + warnings = [] + recommendations = [] + risk_level = "low" + + if active_jobs: + warnings.append(f"ACHTUNG: Drucker hat {len(active_jobs)} aktive(n) Job(s)!") + risk_level = "high" + recommendations.append("Warten Sie bis alle Jobs abgeschlossen sind bevor Sie die Steckdose ausschalten") + + if socket_status == "online" and socket_info and socket_info.get("device_on"): + if socket_info.get("current_power", 0) > 10: # Mehr als 10W Verbrauch + warnings.append(f"Drucker verbraucht aktuell {socket_info['current_power']}W - vermutlich aktiv") + risk_level = "medium" if risk_level == "low" else risk_level + recommendations.append("Prüfen Sie den Druckerstatus bevor Sie die Steckdose ausschalten") + else: + recommendations.append("Drucker scheint im Standby-Modus zu sein - Test sollte sicher möglich sein") + + if socket_status == "error": + warnings.append("Steckdose nicht erreichbar - Netzwerk oder Konfigurationsproblem") + recommendations.append("Prüfen Sie die Netzwerkverbindung und Steckdosen-Konfiguration") + + if not warnings and socket_status == "offline": + recommendations.append("Steckdose ist ausgeschaltet - Test kann sicher durchgeführt werden") + + printers_logger.info(f"✅ Steckdosen-Test-Status erfolgreich abgerufen für {printer.name}") + + return jsonify({ + "success": True, + "printer": { + "id": printer.id, + "name": printer.name, + "model": printer.model, + "location": printer.location, + "status": printer.status + }, + "socket": { + "status": socket_status, + "info": socket_info, + "error": error_message, + "ip_address": printer.plug_ip + }, + "safety": { + "risk_level": risk_level, + "warnings": warnings, + "recommendations": recommendations, + "active_jobs_count": len(active_jobs), + "safe_to_test": len(warnings) == 0 + }, + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + printers_logger.error(f"❌ Allgemeiner Fehler bei Steckdosen-Test-Status: {str(e)}") + return jsonify({ + "success": False, + "error": f"Allgemeiner Fehler: {str(e)}" + }), 500 + +@printers_blueprint.route("/test/socket//control", methods=["POST"]) +@login_required +@require_permission(Permission.ADMIN) +@measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Steuerung") +def test_socket_control(printer_id): + """ + Steuert eine Steckdose für Testzwecke (nur für Ausbilder/Administratoren). + Diese Funktion zeigt Warnungen an, erlaubt aber trotzdem die Steuerung für Tests. + + Args: + printer_id: ID des Druckers dessen Steckdose gesteuert werden soll + + JSON-Parameter: + - action: "on" oder "off" + - force: boolean - überschreibt Sicherheitswarnungen (default: false) + - test_reason: string - Grund für den Test (optional) + + Returns: + JSON mit Ergebnis der Steuerungsaktion und Warnungen + """ + printers_logger.info(f"🧪 Steckdosen-Test-Steuerung für Drucker {printer_id} von Admin {current_user.name}") + + # Parameter validieren + data = request.get_json() + if not data or "action" not in data: + return jsonify({ + "success": False, + "error": "Parameter 'action' fehlt" + }), 400 + + action = data["action"] + if action not in ["on", "off"]: + return jsonify({ + "success": False, + "error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'." + }), 400 + + force = data.get("force", False) + test_reason = data.get("test_reason", "Routinetest") + + try: + # Drucker aus Datenbank holen + db_session = get_db_session() + printer = db_session.query(Printer).filter(Printer.id == printer_id).first() + + if not printer: + db_session.close() + return jsonify({ + "success": False, + "error": f"Drucker mit ID {printer_id} nicht gefunden" + }), 404 + + # Prüfen, ob Drucker eine Steckdose konfiguriert hat + if not printer.plug_ip or not printer.plug_username or not printer.plug_password: + db_session.close() + return jsonify({ + "success": False, + "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert" + }), 400 + + # Aktive Jobs prüfen + active_jobs = db_session.query(Job).filter( + Job.printer_id == printer_id, + Job.status.in_(["running", "printing", "active"]) + ).all() + + # Sicherheitsprüfungen + warnings = [] + should_block = False + + if active_jobs and action == "off": + warnings.append(f"WARNUNG: {len(active_jobs)} aktive Job(s) würden abgebrochen!") + if not force: + should_block = True + + if should_block: + db_session.close() + return jsonify({ + "success": False, + "error": "Aktion blockiert aufgrund von Sicherheitsbedenken", + "warnings": warnings, + "hint": "Verwenden Sie 'force': true um die Aktion trotzdem auszuführen", + "requires_force": True + }), 409 # Conflict + + # Steckdose steuern + from PyP100 import PyP110 + try: + # TP-Link Tapo P110 Verbindung herstellen + p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) + p110.handshake() # Authentifizierung + p110.login() # Login + + # Aktuellen Status vor der Änderung abrufen + device_info_before = p110.getDeviceInfo() + status_before = device_info_before["result"]["device_on"] + + # Steckdose ein- oder ausschalten + if action == "on": + p110.turnOn() + success = True + message = "Steckdose für Test erfolgreich eingeschaltet" + new_printer_status = "starting" + else: + p110.turnOff() + success = True + message = "Steckdose für Test erfolgreich ausgeschaltet" + new_printer_status = "offline" + + # Kurz warten und neuen Status prüfen + time.sleep(2) + device_info_after = p110.getDeviceInfo() + status_after = device_info_after["result"]["device_on"] + + # Drucker-Status aktualisieren + printer.status = new_printer_status + printer.last_checked = datetime.now() + db_session.commit() + + # Cache leeren, damit neue Status-Abfragen aktuell sind + printer_monitor.clear_all_caches() + + # Test-Eintrag für Audit-Log + printers_logger.info(f"🧪 TEST DURCHGEFÜHRT: {action.upper()} für {printer.name} | " + f"Admin: {current_user.name} | Grund: {test_reason} | " + f"Force: {force} | Status: {status_before} → {status_after}") + + except Exception as e: + printers_logger.error(f"❌ Fehler bei Test-Steckdosensteuerung für {printer.name}: {str(e)}") + db_session.close() + return jsonify({ + "success": False, + "error": f"Fehler bei Steckdosensteuerung: {str(e)}" + }), 500 + + db_session.close() + return jsonify({ + "success": True, + "message": message, + "test_info": { + "admin": current_user.name, + "reason": test_reason, + "forced": force, + "status_before": status_before, + "status_after": status_after + }, + "printer": { + "id": printer_id, + "name": printer.name, + "status": new_printer_status + }, + "action": action, + "warnings": warnings, + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + printers_logger.error(f"❌ Allgemeiner Fehler bei Test-Steckdosensteuerung: {str(e)}") + return jsonify({ + "success": False, + "error": f"Allgemeiner Fehler: {str(e)}" + }), 500 + +@printers_blueprint.route("/test/all-sockets", methods=["GET"]) +@login_required +@require_permission(Permission.ADMIN) +@measure_execution_time(logger=printers_logger, task_name="API-Alle-Steckdosen-Test-Status") +def test_all_sockets_status(): + """ + Liefert den Test-Status aller konfigurierten Steckdosen (nur für Ausbilder/Administratoren). + + Returns: + JSON mit Status aller Steckdosen und Gesamtübersicht + """ + printers_logger.info(f"🔍 Alle-Steckdosen-Test-Status von Admin {current_user.name}") + + try: + # Alle Drucker mit Steckdosen-Konfiguration holen + db_session = get_db_session() + printers = db_session.query(Printer).filter( + Printer.plug_ip.isnot(None), + Printer.plug_username.isnot(None), + Printer.plug_password.isnot(None) + ).all() + + results = [] + total_online = 0 + total_offline = 0 + total_error = 0 + total_warnings = 0 + + from PyP100 import PyP110 + + for printer in printers: + # Aktive Jobs für diesen Drucker prüfen + active_jobs = db_session.query(Job).filter( + Job.printer_id == printer.id, + Job.status.in_(["running", "printing", "active"]) + ).count() + + # Steckdosen-Status prüfen + socket_status = "unknown" + device_on = False + current_power = None + error_message = None + warnings = [] + + try: + p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) + p110.handshake() + p110.login() + + device_info = p110.getDeviceInfo() + device_on = device_info["result"]["device_on"] + socket_status = "online" if device_on else "offline" + + # Energieverbrauch abrufen + try: + energy_info = p110.getEnergyUsage() + current_power = energy_info.get("result", {}).get("current_power", 0) + except: + current_power = None + + # Warnungen generieren + if active_jobs > 0: + warnings.append(f"{active_jobs} aktive Job(s)") + + if device_on and current_power and current_power > 10: + warnings.append(f"Hoher Verbrauch: {current_power}W") + + except Exception as e: + socket_status = "error" + error_message = str(e) + warnings.append(f"Verbindungsfehler: {str(e)[:50]}") + + # Statistiken aktualisieren + if socket_status == "online": + total_online += 1 + elif socket_status == "offline": + total_offline += 1 + else: + total_error += 1 + + if warnings: + total_warnings += 1 + + results.append({ + "printer": { + "id": printer.id, + "name": printer.name, + "model": printer.model, + "location": printer.location + }, + "socket": { + "status": socket_status, + "device_on": device_on, + "current_power": current_power, + "ip_address": printer.plug_ip, + "error": error_message + }, + "warnings": warnings, + "active_jobs": active_jobs, + "safe_to_test": len(warnings) == 0 + }) + + db_session.close() + + # Gesamtübersicht erstellen + summary = { + "total_sockets": len(results), + "online": total_online, + "offline": total_offline, + "error": total_error, + "with_warnings": total_warnings, + "safe_to_test": len(results) - total_warnings + } + + printers_logger.info(f"✅ Alle-Steckdosen-Status erfolgreich abgerufen: {len(results)} Steckdosen") + + return jsonify({ + "success": True, + "sockets": results, + "summary": summary, + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + printers_logger.error(f"❌ Fehler bei Alle-Steckdosen-Test-Status: {str(e)}") return jsonify({ "success": False, "error": f"Allgemeiner Fehler: {str(e)}" diff --git a/backend/database/myp.db b/backend/database/myp.db index 2a753946..39886f6e 100644 Binary files a/backend/database/myp.db and b/backend/database/myp.db differ diff --git a/backend/database/myp.db-shm b/backend/database/myp.db-shm deleted file mode 100644 index ba938876..00000000 Binary files a/backend/database/myp.db-shm and /dev/null differ diff --git a/backend/database/myp.db-wal b/backend/database/myp.db-wal deleted file mode 100644 index 9e87a894..00000000 Binary files a/backend/database/myp.db-wal and /dev/null differ diff --git a/backend/logs/app/app.log b/backend/logs/app/app.log index 4695e158..d38facf7 100644 --- a/backend/logs/app/app.log +++ b/backend/logs/app/app.log @@ -75189,3 +75189,434 @@ WHERE users.id = ? 2025-05-31 23:44:39 - myp.app - INFO - [WEB] Hostname: C040L0079726760 2025-05-31 23:44:39 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:44:39 2025-05-31 23:44:39 - myp.app - INFO - ================================================== +2025-05-31 23:44:45 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:44:45] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:44:46 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:44:46] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:02 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-05-31 23:45:02 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-05-31 23:45:02 - myp.app - INFO - Job-Scheduler gestoppt +2025-05-31 23:45:02 - myp.app - INFO - 💾 Führe Datenbank-Cleanup durch... +2025-05-31 23:45:02 - myp.app - INFO - 📝 Führe WAL-Checkpoint durch... +2025-05-31 23:45:02 - myp.app - INFO - WAL-Checkpoint abgeschlossen: 0 Seiten übertragen, 0 Seiten zurückgesetzt +2025-05-31 23:45:02 - myp.app - INFO - 📁 Schalte Journal-Mode um... +2025-05-31 23:45:02 - myp.app - INFO - ✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein +2025-05-31 23:45:02 - myp.app - INFO - ✅ Shutdown abgeschlossen +2025-05-31 23:45:22 - myp.windows_fixes - INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Globaler subprocess-Patch angewendet +2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-05-31 23:45:22 - myp.app - INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-05-31 23:45:22 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-05-31 23:45:22 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-05-31 23:45:22 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-05-31 23:45:22 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-05-31 23:45:22 - myp.security - INFO - 🔒 Security System initialisiert +2025-05-31 23:45:22 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-05-31 23:45:22 - myp.app - INFO - ================================================== +2025-05-31 23:45:22 - myp.app - INFO - [START] MYP (Manage Your Printers) wird gestartet... +2025-05-31 23:45:22 - myp.app - INFO - [FOLDER] Log-Verzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\logs +2025-05-31 23:45:22 - myp.app - INFO - [CHART] Log-Level: INFO +2025-05-31 23:45:22 - myp.app - INFO - [PC] Betriebssystem: Windows 11 +2025-05-31 23:45:22 - myp.app - INFO - [WEB] Hostname: C040L0079726760 +2025-05-31 23:45:22 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:45:22 +2025-05-31 23:45:22 - myp.app - INFO - ================================================== +2025-05-31 23:45:22 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen) +2025-05-31 23:45:22 - myp.app - INFO - Datenbank mit Optimierungen initialisiert +2025-05-31 23:45:23 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-05-31 23:45:23 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-05-31 23:45:23 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-05-31 23:45:23 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden +2025-05-31 23:45:23 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden +2025-05-31 23:45:23 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-05-31 23:45:23 - myp.app - INFO - Job-Scheduler gestartet +2025-05-31 23:45:23 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-05-31 23:45:23 - myp.app - INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-05-31 23:45:23 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.178.111:5000 +2025-05-31 23:45:23 - werkzeug - INFO - Press CTRL+C to quit +2025-05-31 23:45:24 - myp.printer_monitor - INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-05-31 23:45:24 - myp.printer_monitor - INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-05-31 23:45:24 - myp.printer_monitor - INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-05-31 23:45:24 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:24] "GET /dashboard HTTP/1.1" 200 - +2025-05-31 23:45:24 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:24] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/optimization-features.js HTTP/1.1" 200 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/css/optimization-animations.css HTTP/1.1" 200 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:45:25 - myp.printer_monitor - INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:25 - myp.printer_monitor - INFO - ℹ️ Keine aktiven Drucker gefunden +2025-05-31 23:45:25 - myp.printer_monitor - INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:45:25 - myp.printer_monitor - INFO - ℹ️ Keine aktiven Drucker gefunden +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /api/printers/monitor/live-status?use_cache=true HTTP/1.1" 200 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:45:26 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:26] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /printers HTTP/1.1" 200 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/css/optimization-animations.css HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/optimization-features.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /api/printers HTTP/1.1" 200 - +2025-05-31 23:45:28 - myp.printer_monitor - INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-05-31 23:45:28 - myp.printer_monitor - INFO - ℹ️ Keine aktiven Drucker gefunden +2025-05-31 23:45:28 - myp.printer_monitor - INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:45:28 - myp.printer_monitor - INFO - ℹ️ Keine aktiven Drucker gefunden +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "GET /api/printers/monitor/live-status?use_cache=false HTTP/1.1" 200 - +2025-05-31 23:45:29 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:29] "GET /api/printers HTTP/1.1" 200 - +2025-05-31 23:45:29 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:29] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:45:29 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:29] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:45:30 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:30] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:45:30 - myp.printer_monitor - INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /jobs HTTP/1.1" 200 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/css/optimization-animations.css HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/optimization-features.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:32 - myp.app - WARNING - Schema-Problem beim User-Load für ID 1: tuple index out of range +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:45:32 - myp.app - INFO - User 1 erfolgreich über manuelle Abfrage geladen +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /api/jobs HTTP/1.1" 200 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /api/printers HTTP/1.1" 200 - +2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:45:33 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:33] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:45:33 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:33] "GET /stats HTTP/1.1" 200 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/optimization-animations.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/optimization-features.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /calendar HTTP/1.1" 200 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/optimization-animations.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/main.min.css HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/optimization-features.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/core.min.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/timegrid.min.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/daygrid.min.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/interaction.min.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/fullcalendar/list.min.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1" 404 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:45:36 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:36] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:45:36 - myp.printer_monitor - INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-05-31 23:45:38 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:38] "POST /api/optimization/auto-optimize HTTP/1.1" 404 - +2025-05-31 23:45:41 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:41] "POST /api/optimization/auto-optimize HTTP/1.1" 404 - +2025-05-31 23:45:42 - myp.printer_monitor - INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-05-31 23:45:48 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-05-31 23:45:54 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-05-31 23:46:00 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.2s +2025-05-31 23:46:04 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-05-31 23:46:04 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-05-31 23:46:05 - myp.app - INFO - Job-Scheduler gestoppt +2025-05-31 23:46:05 - myp.app - INFO - 💾 Führe Datenbank-Cleanup durch... +2025-05-31 23:46:05 - myp.app - INFO - 📝 Führe WAL-Checkpoint durch... +2025-05-31 23:46:05 - myp.app - INFO - WAL-Checkpoint abgeschlossen: 0 Seiten übertragen, 0 Seiten zurückgesetzt +2025-05-31 23:46:05 - myp.app - INFO - 📁 Schalte Journal-Mode um... +2025-05-31 23:46:05 - myp.app - INFO - ✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein +2025-05-31 23:46:05 - myp.app - INFO - ✅ Shutdown abgeschlossen +2025-05-31 23:46:11 - myp.windows_fixes - INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-05-31 23:46:11 - myp.windows_fixes - INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-05-31 23:46:11 - myp.windows_fixes - INFO - ✅ Globaler subprocess-Patch angewendet +2025-05-31 23:46:11 - myp.windows_fixes - INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-05-31 23:46:11 - myp.app - INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-05-31 23:46:12 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-05-31 23:46:12 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-05-31 23:46:12 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-05-31 23:46:12 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-05-31 23:46:12 - myp.security - INFO - 🔒 Security System initialisiert +2025-05-31 23:46:12 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-05-31 23:46:12 - myp.app - INFO - ================================================== +2025-05-31 23:46:12 - myp.app - INFO - [START] MYP (Manage Your Printers) wird gestartet... +2025-05-31 23:46:12 - myp.app - INFO - [FOLDER] Log-Verzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\logs +2025-05-31 23:46:12 - myp.app - INFO - [CHART] Log-Level: INFO +2025-05-31 23:46:12 - myp.app - INFO - [PC] Betriebssystem: Windows 11 +2025-05-31 23:46:12 - myp.app - INFO - [WEB] Hostname: C040L0079726760 +2025-05-31 23:46:12 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:46:12 +2025-05-31 23:46:12 - myp.app - INFO - ================================================== +2025-05-31 23:46:12 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen) +2025-05-31 23:46:12 - myp.app - INFO - Datenbank mit Optimierungen initialisiert +2025-05-31 23:46:12 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-05-31 23:46:12 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-05-31 23:46:12 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-05-31 23:46:12 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden +2025-05-31 23:46:12 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden +2025-05-31 23:46:12 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-05-31 23:46:12 - myp.app - INFO - Job-Scheduler gestartet +2025-05-31 23:46:12 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-05-31 23:46:12 - myp.app - INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-05-31 23:46:12 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.178.111:5000 +2025-05-31 23:46:12 - werkzeug - INFO - Press CTRL+C to quit +2025-05-31 23:46:14 - myp.printer_monitor - INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-05-31 23:46:14 - myp.printer_monitor - INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-05-31 23:46:14 - myp.printer_monitor - INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-05-31 23:46:20 - myp.printer_monitor - INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-05-31 23:46:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:46:21] "POST /api/optimization/auto-optimize HTTP/1.1" 404 - +2025-05-31 23:46:26 - myp.printer_monitor - INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-05-31 23:46:32 - myp.printer_monitor - INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-05-31 23:46:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:46:35] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:46:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:46:35] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:46:38 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-05-31 23:46:44 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-05-31 23:46:49 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:46:49] "POST /api/optimization/auto-optimize HTTP/1.1" 404 - +2025-05-31 23:46:50 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.1s +2025-05-31 23:47:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:47:05] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:47:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:47:05] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:47:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:47:35] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:47:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:47:35] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:48:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:48:05] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:48:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:48:05] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:48:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:48:35] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:48:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:48:35] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:49:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:05] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:49:05 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:05] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1" 404 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /calendar HTTP/1.1" 200 - +2025-05-31 23:49:21 - myp.app - WARNING - Schema-Problem beim User-Load für ID 1: tuple index out of range +2025-05-31 23:49:21 - myp.app - INFO - User 1 erfolgreich über manuelle Abfrage geladen +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/main.min.css HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/css/optimization-animations.css HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/optimization-features.js HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/daygrid.min.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/core.min.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/timegrid.min.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/interaction.min.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - myp.app - WARNING - Schema-Problem beim User-Load für ID 1: (sqlite3.InterfaceError) bad parameter or other API misuse +[SQL: SELECT users.id AS users_id, users.email AS users_email, users.username AS users_username, users.password_hash AS users_password_hash, users.name AS users_name, users.role AS users_role, users.active AS users_active, users.created_at AS users_created_at, users.last_login AS users_last_login, users.updated_at AS users_updated_at, users.settings AS users_settings, users.last_activity AS users_last_activity, users.department AS users_department, users.position AS users_position, users.phone AS users_phone, users.bio AS users_bio +FROM users +WHERE users.id = ? + LIMIT ? OFFSET ?] +[parameters: (1, 1, 0)] +(Background on this error at: https://sqlalche.me/e/20/rvf5) +2025-05-31 23:49:21 - myp.app - INFO - User 1 erfolgreich über manuelle Abfrage geladen +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/fullcalendar/list.min.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1" 404 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/favicon.svg HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:49:22 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:22] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:49:23 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:23] "POST /api/optimization/auto-optimize HTTP/1.1" 404 - +2025-05-31 23:49:27 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-05-31 23:49:27 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-05-31 23:49:27 - myp.app - INFO - Job-Scheduler gestoppt +2025-05-31 23:49:27 - myp.app - INFO - 💾 Führe Datenbank-Cleanup durch... +2025-05-31 23:49:27 - myp.app - INFO - 📝 Führe WAL-Checkpoint durch... +2025-05-31 23:49:27 - myp.app - INFO - WAL-Checkpoint abgeschlossen: 0 Seiten übertragen, 0 Seiten zurückgesetzt +2025-05-31 23:49:27 - myp.app - INFO - 📁 Schalte Journal-Mode um... +2025-05-31 23:49:27 - myp.app - INFO - ✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein +2025-05-31 23:49:27 - myp.app - INFO - ✅ Shutdown abgeschlossen +2025-05-31 23:49:29 - myp.windows_fixes - INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-05-31 23:49:29 - myp.windows_fixes - INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-05-31 23:49:29 - myp.windows_fixes - INFO - ✅ Globaler subprocess-Patch angewendet +2025-05-31 23:49:29 - myp.windows_fixes - INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-05-31 23:49:29 - myp.app - INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-05-31 23:49:29 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-05-31 23:49:29 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-05-31 23:49:29 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-05-31 23:49:30 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-05-31 23:49:30 - myp.security - INFO - 🔒 Security System initialisiert +2025-05-31 23:49:30 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-05-31 23:49:30 - myp.app - INFO - ================================================== +2025-05-31 23:49:30 - myp.app - INFO - [START] MYP (Manage Your Printers) wird gestartet... +2025-05-31 23:49:30 - myp.app - INFO - [FOLDER] Log-Verzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\logs +2025-05-31 23:49:30 - myp.app - INFO - [CHART] Log-Level: INFO +2025-05-31 23:49:30 - myp.app - INFO - [PC] Betriebssystem: Windows 11 +2025-05-31 23:49:30 - myp.app - INFO - [WEB] Hostname: C040L0079726760 +2025-05-31 23:49:30 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:49:30 +2025-05-31 23:49:30 - myp.app - INFO - ================================================== +2025-05-31 23:50:29 - myp.windows_fixes - INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-05-31 23:50:29 - myp.windows_fixes - INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-05-31 23:50:29 - myp.windows_fixes - INFO - ✅ Globaler subprocess-Patch angewendet +2025-05-31 23:50:29 - myp.windows_fixes - INFO - ✅ Alle Windows-Fixes erfolgreich angewendet +2025-05-31 23:50:29 - myp.app - INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db +2025-05-31 23:50:29 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-05-31 23:50:29 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-05-31 23:50:29 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-05-31 23:50:29 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-05-31 23:50:29 - myp.security - INFO - 🔒 Security System initialisiert +2025-05-31 23:50:29 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-05-31 23:50:29 - myp.app - INFO - ================================================== +2025-05-31 23:50:29 - myp.app - INFO - [START] MYP (Manage Your Printers) wird gestartet... +2025-05-31 23:50:29 - myp.app - INFO - [FOLDER] Log-Verzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\logs +2025-05-31 23:50:29 - myp.app - INFO - [CHART] Log-Level: INFO +2025-05-31 23:50:29 - myp.app - INFO - [PC] Betriebssystem: Windows 11 +2025-05-31 23:50:29 - myp.app - INFO - [WEB] Hostname: C040L0079726760 +2025-05-31 23:50:29 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:50:29 +2025-05-31 23:50:29 - myp.app - INFO - ================================================== +2025-05-31 23:50:30 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen) +2025-05-31 23:50:30 - myp.app - INFO - Datenbank mit Optimierungen initialisiert +2025-05-31 23:50:30 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-05-31 23:50:30 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-05-31 23:50:30 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-05-31 23:50:30 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden +2025-05-31 23:50:30 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden +2025-05-31 23:50:30 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-05-31 23:50:30 - myp.app - INFO - Job-Scheduler gestartet +2025-05-31 23:50:30 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-05-31 23:50:30 - myp.app - INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-05-31 23:50:30 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://192.168.178.111:5000 +2025-05-31 23:50:30 - werkzeug - INFO - Press CTRL+C to quit +2025-05-31 23:50:31 - myp.printer_monitor - INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-05-31 23:50:31 - myp.printer_monitor - INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-05-31 23:50:31 - myp.printer_monitor - INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /calendar HTTP/1.1" 200 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/css/professional-theme.css HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/offline-app.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/css/tailwind.min.css HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/css/components.css HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/optimization-features.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/core.min.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/daygrid.min.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/ui-components.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/css/optimization-animations.css HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/main.min.css HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/timegrid.min.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/list.min.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/fullcalendar/interaction.min.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/debug-fix.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/job-manager.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/dark-mode-fix.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/global-refresh-functions.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/event-handlers.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/csp-violation-handler.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/printer_monitor.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/notifications.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/session-manager.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/js/auto-logout.js HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1" 404 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /api/user/settings HTTP/1.1" 200 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/manifest.json HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/favicon.svg HTTP/1.1" 304 - +2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "GET /static/icons/icon-144x144.png HTTP/1.1" 304 - +2025-05-31 23:50:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:35] "POST /api/session/heartbeat HTTP/1.1" 200 - +2025-05-31 23:50:36 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:36] "POST /api/optimization/auto-optimize HTTP/1.1" 200 - +2025-05-31 23:50:36 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:36] "GET /api/jobs?page=1 HTTP/1.1" 200 - +2025-05-31 23:50:36 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:36] "GET /static/icons/apple-touch-icon.png HTTP/1.1" 200 - +2025-05-31 23:50:37 - myp.printer_monitor - INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-05-31 23:50:43 - myp.printer_monitor - INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-05-31 23:50:50 - myp.printer_monitor - INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-05-31 23:50:56 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-05-31 23:51:02 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-05-31 23:51:04 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:51:04] "GET /api/notifications HTTP/1.1" 200 - +2025-05-31 23:51:04 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:51:04] "GET /api/session/status HTTP/1.1" 200 - +2025-05-31 23:51:07 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-05-31 23:51:07 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-05-31 23:51:08 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.2s +2025-05-31 23:51:08 - myp.app - INFO - Job-Scheduler gestoppt +2025-05-31 23:51:08 - myp.app - INFO - 💾 Führe Datenbank-Cleanup durch... +2025-05-31 23:51:08 - myp.app - INFO - 📝 Führe WAL-Checkpoint durch... +2025-05-31 23:51:08 - myp.app - INFO - WAL-Checkpoint abgeschlossen: 0 Seiten übertragen, 0 Seiten zurückgesetzt +2025-05-31 23:51:08 - myp.app - INFO - 📁 Schalte Journal-Mode um... +2025-05-31 23:51:08 - myp.app - INFO - ✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein +2025-05-31 23:51:08 - myp.app - INFO - ✅ Shutdown abgeschlossen diff --git a/backend/logs/printers/printers.log b/backend/logs/printers/printers.log index 9ffc6575..2039c348 100644 --- a/backend/logs/printers/printers.log +++ b/backend/logs/printers/printers.log @@ -2514,3 +2514,10 @@ 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) +2025-05-31 23:45:25 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-05-31 23:45:25 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker +2025-05-31 23:45:28 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-05-31 23:45:28 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-05-31 23:45:28 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker +2025-05-31 23:45:29 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) +2025-05-31 23:45:32 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check) diff --git a/backend/logs/scheduler/scheduler.log b/backend/logs/scheduler/scheduler.log index f3309a4d..22957e80 100644 --- a/backend/logs/scheduler/scheduler.log +++ b/backend/logs/scheduler/scheduler.log @@ -2687,3 +2687,21 @@ 2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler-Thread gestartet 2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler gestartet 2025-05-31 23:44:38 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-05-31 23:45:02 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-05-31 23:45:02 - myp.scheduler - INFO - Scheduler gestoppt +2025-05-31 23:45:22 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-05-31 23:45:23 - myp.scheduler - INFO - Scheduler-Thread gestartet +2025-05-31 23:45:23 - myp.scheduler - INFO - Scheduler gestartet +2025-05-31 23:46:05 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-05-31 23:46:05 - myp.scheduler - INFO - Scheduler gestoppt +2025-05-31 23:46:12 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-05-31 23:46:12 - myp.scheduler - INFO - Scheduler-Thread gestartet +2025-05-31 23:46:12 - myp.scheduler - INFO - Scheduler gestartet +2025-05-31 23:49:27 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-05-31 23:49:27 - myp.scheduler - INFO - Scheduler gestoppt +2025-05-31 23:49:29 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-05-31 23:50:29 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-05-31 23:50:30 - myp.scheduler - INFO - Scheduler-Thread gestartet +2025-05-31 23:50:30 - myp.scheduler - INFO - Scheduler gestartet +2025-05-31 23:51:08 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-05-31 23:51:08 - myp.scheduler - INFO - Scheduler gestoppt diff --git a/backend/static/css/optimization-animations.css b/backend/static/css/optimization-animations.css index ad780a1b..917091ca 100644 --- a/backend/static/css/optimization-animations.css +++ b/backend/static/css/optimization-animations.css @@ -53,7 +53,7 @@ } .animate-pulse-scale { - animation: pulse-scale 2s infinite ease-in-out; + animation: pulse-scale 3s infinite ease-in-out; } /* ===== FLOATING ANIMATIONS ===== */ @@ -82,12 +82,12 @@ } .animate-float { - animation: float 3s infinite ease-in-out; + animation: float 4s infinite ease-in-out; } .animate-float-delay { - animation: float-delay 3s infinite ease-in-out; - animation-delay: 1s; + animation: float-delay 4s infinite ease-in-out; + animation-delay: 1.5s; } /* ===== SLIDE-UP ANIMATIONS ===== */ @@ -161,7 +161,7 @@ } .animate-glow { - animation: glow 2s infinite ease-in-out; + animation: glow 3s infinite ease-in-out; } /* ===== KONFETTI ANIMATION ===== */ @@ -190,7 +190,7 @@ opacity: 1; } 100% { - transform: translateY(100vh) rotate(720deg); + transform: translateY(120vh) rotate(720deg); opacity: 0; } } diff --git a/backend/static/js/optimization-features.js b/backend/static/js/optimization-features.js index 20b3d2aa..1d72e28f 100644 --- a/backend/static/js/optimization-features.js +++ b/backend/static/js/optimization-features.js @@ -366,14 +366,14 @@ class OptimizationManager { // Sound-Effekt (optional) this.playSuccessSound(); - // Auto-Close nach 10 Sekunden + // Auto-Close nach 20 Sekunden (verlängert für bessere Animation-Wirkung) setTimeout(() => { if (modal && modal.parentNode) { modal.style.opacity = '0'; modal.style.transform = 'scale(0.95)'; setTimeout(() => modal.remove(), 300); } - }, 10000); + }, 20000); } /** @@ -383,10 +383,10 @@ class OptimizationManager { const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']; let confetti = ''; - for (let i = 0; i < 30; i++) { + for (let i = 0; i < 50; i++) { const color = colors[Math.floor(Math.random() * colors.length)]; - const delay = Math.random() * 3; - const duration = 3 + Math.random() * 2; + const delay = Math.random() * 5; + const duration = 4 + Math.random() * 3; const left = Math.random() * 100; confetti += ` diff --git a/backend/utils/advanced_tables.py b/backend/utils/advanced_tables.py new file mode 100644 index 00000000..dc99a5b7 --- /dev/null +++ b/backend/utils/advanced_tables.py @@ -0,0 +1,936 @@ +""" +Erweitertes Tabellen-System für das MYP-System +============================================= + +Dieses Modul stellt erweiterte Tabellen-Funktionalität bereit: +- Sortierung nach allen Spalten +- Erweiterte Filter-Optionen +- Pagination mit anpassbaren Seitengrößen +- Spalten-Auswahl und -anpassung +- Export-Funktionen +- Responsive Design +""" + +import re +import json +import math +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Tuple, Union, Callable +from dataclasses import dataclass, asdict +from enum import Enum +from flask import request, jsonify +from sqlalchemy import func, text, or_, and_ +from sqlalchemy.orm import Query + +from utils.logging_config import get_logger +from models import Job, User, Printer, GuestRequest, get_db_session + +logger = get_logger("advanced_tables") + +class SortDirection(Enum): + ASC = "asc" + DESC = "desc" + +class FilterOperator(Enum): + EQUALS = "eq" + NOT_EQUALS = "ne" + CONTAINS = "contains" + NOT_CONTAINS = "not_contains" + STARTS_WITH = "starts_with" + ENDS_WITH = "ends_with" + GREATER_THAN = "gt" + LESS_THAN = "lt" + GREATER_EQUAL = "gte" + LESS_EQUAL = "lte" + BETWEEN = "between" + IN = "in" + NOT_IN = "not_in" + IS_NULL = "is_null" + IS_NOT_NULL = "is_not_null" + +@dataclass +class SortConfig: + """Sortierung-Konfiguration""" + column: str + direction: SortDirection = SortDirection.ASC + +@dataclass +class FilterConfig: + """Filter-Konfiguration""" + column: str + operator: FilterOperator + value: Any = None + values: List[Any] = None + +@dataclass +class PaginationConfig: + """Pagination-Konfiguration""" + page: int = 1 + page_size: int = 25 + max_page_size: int = 100 + +@dataclass +class ColumnConfig: + """Spalten-Konfiguration""" + key: str + label: str + sortable: bool = True + filterable: bool = True + searchable: bool = True + visible: bool = True + width: Optional[str] = None + align: str = "left" # left, center, right + format_type: str = "text" # text, number, date, datetime, boolean, currency + format_options: Dict[str, Any] = None + +@dataclass +class TableConfig: + """Gesamt-Tabellen-Konfiguration""" + table_id: str + columns: List[ColumnConfig] + default_sort: List[SortConfig] = None + default_filters: List[FilterConfig] = None + pagination: PaginationConfig = None + searchable: bool = True + exportable: bool = True + selectable: bool = False + row_actions: List[Dict[str, Any]] = None + +class AdvancedTableQuery: + """Builder für erweiterte Tabellen-Abfragen""" + + def __init__(self, base_query: Query, model_class): + self.base_query = base_query + self.model_class = model_class + self.filters = [] + self.sorts = [] + self.search_term = None + self.search_columns = [] + + def add_filter(self, filter_config: FilterConfig): + """Fügt einen Filter hinzu""" + self.filters.append(filter_config) + return self + + def add_sort(self, sort_config: SortConfig): + """Fügt eine Sortierung hinzu""" + self.sorts.append(sort_config) + return self + + def set_search(self, term: str, columns: List[str]): + """Setzt globale Suche""" + self.search_term = term + self.search_columns = columns + return self + + def build_query(self) -> Query: + """Erstellt die finale Query""" + query = self.base_query + + # Filter anwenden + for filter_config in self.filters: + query = self._apply_filter(query, filter_config) + + # Globale Suche anwenden + if self.search_term and self.search_columns: + query = self._apply_search(query) + + # Sortierung anwenden + for sort_config in self.sorts: + query = self._apply_sort(query, sort_config) + + return query + + def _apply_filter(self, query: Query, filter_config: FilterConfig) -> Query: + """Wendet einen Filter auf die Query an""" + column = getattr(self.model_class, filter_config.column, None) + if not column: + logger.warning(f"Spalte {filter_config.column} nicht gefunden in {self.model_class}") + return query + + op = filter_config.operator + value = filter_config.value + values = filter_config.values + + if op == FilterOperator.EQUALS: + return query.filter(column == value) + elif op == FilterOperator.NOT_EQUALS: + return query.filter(column != value) + elif op == FilterOperator.CONTAINS: + return query.filter(column.ilike(f"%{value}%")) + elif op == FilterOperator.NOT_CONTAINS: + return query.filter(~column.ilike(f"%{value}%")) + elif op == FilterOperator.STARTS_WITH: + return query.filter(column.ilike(f"{value}%")) + elif op == FilterOperator.ENDS_WITH: + return query.filter(column.ilike(f"%{value}")) + elif op == FilterOperator.GREATER_THAN: + return query.filter(column > value) + elif op == FilterOperator.LESS_THAN: + return query.filter(column < value) + elif op == FilterOperator.GREATER_EQUAL: + return query.filter(column >= value) + elif op == FilterOperator.LESS_EQUAL: + return query.filter(column <= value) + elif op == FilterOperator.BETWEEN and values and len(values) >= 2: + return query.filter(column.between(values[0], values[1])) + elif op == FilterOperator.IN and values: + return query.filter(column.in_(values)) + elif op == FilterOperator.NOT_IN and values: + return query.filter(~column.in_(values)) + elif op == FilterOperator.IS_NULL: + return query.filter(column.is_(None)) + elif op == FilterOperator.IS_NOT_NULL: + return query.filter(column.isnot(None)) + + return query + + def _apply_search(self, query: Query) -> Query: + """Wendet globale Suche an""" + if not self.search_term or not self.search_columns: + return query + + search_conditions = [] + for column_name in self.search_columns: + column = getattr(self.model_class, column_name, None) + if column: + # Konvertiere zu String für Suche in numerischen Spalten + search_conditions.append( + func.cast(column, sqlalchemy.String).ilike(f"%{self.search_term}%") + ) + + if search_conditions: + return query.filter(or_(*search_conditions)) + + return query + + def _apply_sort(self, query: Query, sort_config: SortConfig) -> Query: + """Wendet Sortierung an""" + column = getattr(self.model_class, sort_config.column, None) + if not column: + logger.warning(f"Spalte {sort_config.column} für Sortierung nicht gefunden") + return query + + if sort_config.direction == SortDirection.DESC: + return query.order_by(column.desc()) + else: + return query.order_by(column.asc()) + +class TableDataProcessor: + """Verarbeitet Tabellendaten für die Ausgabe""" + + def __init__(self, config: TableConfig): + self.config = config + + def process_data(self, data: List[Any]) -> List[Dict[str, Any]]: + """Verarbeitet rohe Daten für Tabellen-Ausgabe""" + processed_rows = [] + + for item in data: + row = {} + for column in self.config.columns: + if not column.visible: + continue + + # Wert extrahieren + value = self._extract_value(item, column.key) + + # Formatieren + formatted_value = self._format_value(value, column) + + row[column.key] = { + 'raw': value, + 'formatted': formatted_value, + 'sortable': column.sortable, + 'filterable': column.filterable + } + + # Row Actions hinzufügen + if self.config.row_actions: + row['_actions'] = self._get_row_actions(item) + + # Row Metadata + row['_id'] = getattr(item, 'id', None) + row['_type'] = item.__class__.__name__.lower() + + processed_rows.append(row) + + return processed_rows + + def _extract_value(self, item: Any, key: str) -> Any: + """Extrahiert Wert aus einem Objekt""" + try: + # Unterstützung für verschachtelte Attribute (z.B. "user.name") + if '.' in key: + obj = item + for part in key.split('.'): + obj = getattr(obj, part, None) + if obj is None: + break + return obj + else: + return getattr(item, key, None) + except AttributeError: + return None + + def _format_value(self, value: Any, column: ColumnConfig) -> str: + """Formatiert einen Wert basierend auf dem Spaltentyp""" + if value is None: + return "" + + format_type = column.format_type + options = column.format_options or {} + + if format_type == "date" and isinstance(value, datetime): + date_format = options.get('format', '%d.%m.%Y') + return value.strftime(date_format) + + elif format_type == "datetime" and isinstance(value, datetime): + datetime_format = options.get('format', '%d.%m.%Y %H:%M') + return value.strftime(datetime_format) + + elif format_type == "number" and isinstance(value, (int, float)): + decimals = options.get('decimals', 0) + return f"{value:.{decimals}f}" + + elif format_type == "currency" and isinstance(value, (int, float)): + currency = options.get('currency', '€') + decimals = options.get('decimals', 2) + return f"{value:.{decimals}f} {currency}" + + elif format_type == "boolean": + true_text = options.get('true_text', 'Ja') + false_text = options.get('false_text', 'Nein') + return true_text if value else false_text + + elif format_type == "truncate": + max_length = options.get('max_length', 50) + text = str(value) + if len(text) > max_length: + return text[:max_length-3] + "..." + return text + + return str(value) + + def _get_row_actions(self, item: Any) -> List[Dict[str, Any]]: + """Generiert verfügbare Aktionen für eine Zeile""" + actions = [] + + for action_config in self.config.row_actions: + # Prüfe Bedingungen für Aktion + if self._check_action_condition(item, action_config): + actions.append({ + 'type': action_config['type'], + 'label': action_config['label'], + 'icon': action_config.get('icon'), + 'url': self._build_action_url(item, action_config), + 'method': action_config.get('method', 'GET'), + 'confirm': action_config.get('confirm'), + 'class': action_config.get('class', '') + }) + + return actions + + def _check_action_condition(self, item: Any, action_config: Dict[str, Any]) -> bool: + """Prüft ob eine Aktion für ein Item verfügbar ist""" + condition = action_config.get('condition') + if not condition: + return True + + try: + # Einfache Bedingungsprüfung + if isinstance(condition, dict): + for key, expected_value in condition.items(): + actual_value = self._extract_value(item, key) + if actual_value != expected_value: + return False + return True + except Exception: + return False + + def _build_action_url(self, item: Any, action_config: Dict[str, Any]) -> str: + """Erstellt URL für eine Aktion""" + url_template = action_config.get('url', '') + + # Ersetze Platzhalter in URL + try: + return url_template.format(id=getattr(item, 'id', '')) + except Exception: + return url_template + +def parse_table_request(request_data: Dict[str, Any]) -> Tuple[List[SortConfig], List[FilterConfig], PaginationConfig, str]: + """Parst Tabellen-Request-Parameter""" + + # Sortierung parsen + sorts = [] + sort_data = request_data.get('sort', []) + if isinstance(sort_data, dict): + sort_data = [sort_data] + + for sort_item in sort_data: + if isinstance(sort_item, dict): + column = sort_item.get('column') + direction = SortDirection(sort_item.get('direction', 'asc')) + if column: + sorts.append(SortConfig(column=column, direction=direction)) + + # Filter parsen + filters = [] + filter_data = request_data.get('filters', []) + if isinstance(filter_data, dict): + filter_data = [filter_data] + + for filter_item in filter_data: + if isinstance(filter_item, dict): + column = filter_item.get('column') + operator = FilterOperator(filter_item.get('operator', 'eq')) + value = filter_item.get('value') + values = filter_item.get('values') + + if column: + filters.append(FilterConfig( + column=column, + operator=operator, + value=value, + values=values + )) + + # Pagination parsen + page = int(request_data.get('page', 1)) + page_size = min(int(request_data.get('page_size', 25)), 100) + pagination = PaginationConfig(page=page, page_size=page_size) + + # Suche parsen + search = request_data.get('search', '') + + return sorts, filters, pagination, search + +def get_advanced_table_javascript() -> str: + """JavaScript für erweiterte Tabellen""" + return """ + class AdvancedTable { + constructor(tableId, config = {}) { + this.tableId = tableId; + this.config = { + apiUrl: '/api/table-data', + pageSize: 25, + searchDelay: 500, + sortable: true, + filterable: true, + searchable: true, + ...config + }; + + this.currentSort = []; + this.currentFilters = []; + this.currentPage = 1; + this.currentSearch = ''; + this.totalPages = 1; + this.totalItems = 0; + + this.searchTimeout = null; + + this.init(); + } + + init() { + this.setupTable(); + this.setupEventListeners(); + this.loadData(); + } + + setupTable() { + const table = document.getElementById(this.tableId); + if (!table) return; + + table.classList.add('advanced-table'); + + // Add table wrapper + const wrapper = document.createElement('div'); + wrapper.className = 'table-wrapper'; + table.parentNode.insertBefore(wrapper, table); + wrapper.appendChild(table); + + // Add controls + this.createControls(wrapper); + } + + createControls(wrapper) { + const controls = document.createElement('div'); + controls.className = 'table-controls'; + controls.innerHTML = ` +
+ +
+ + +
+
+
+ + + +
+ `; + + wrapper.insertBefore(controls, wrapper.firstChild); + + // Add pagination + const pagination = document.createElement('div'); + pagination.className = 'table-pagination'; + pagination.id = `${this.tableId}-pagination`; + wrapper.appendChild(pagination); + } + + setupEventListeners() { + // Search + const searchInput = document.getElementById(`${this.tableId}-search`); + searchInput?.addEventListener('input', (e) => { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(() => { + this.currentSearch = e.target.value; + this.currentPage = 1; + this.loadData(); + }, this.config.searchDelay); + }); + + // Page size + const pageSizeSelect = document.getElementById(`${this.tableId}-page-size`); + pageSizeSelect?.addEventListener('change', (e) => { + this.config.pageSize = parseInt(e.target.value); + this.currentPage = 1; + this.loadData(); + }); + + // Refresh + const refreshBtn = document.getElementById(`${this.tableId}-refresh-btn`); + refreshBtn?.addEventListener('click', () => { + this.loadData(); + }); + + // Export + const exportBtn = document.getElementById(`${this.tableId}-export-btn`); + exportBtn?.addEventListener('click', () => { + this.exportData(); + }); + + // Table header clicks (sorting) + const table = document.getElementById(this.tableId); + table?.addEventListener('click', (e) => { + const th = e.target.closest('th[data-sortable="true"]'); + if (th) { + const column = th.dataset.column; + this.toggleSort(column); + } + }); + } + + toggleSort(column) { + const existingSort = this.currentSort.find(s => s.column === column); + + if (existingSort) { + if (existingSort.direction === 'asc') { + existingSort.direction = 'desc'; + } else { + // Remove sort + this.currentSort = this.currentSort.filter(s => s.column !== column); + } + } else { + this.currentSort.push({ column, direction: 'asc' }); + } + + this.updateSortHeaders(); + this.loadData(); + } + + updateSortHeaders() { + const table = document.getElementById(this.tableId); + const headers = table?.querySelectorAll('th[data-column]'); + + headers?.forEach(th => { + const column = th.dataset.column; + const sort = this.currentSort.find(s => s.column === column); + + th.classList.remove('sort-asc', 'sort-desc'); + + if (sort) { + th.classList.add(`sort-${sort.direction}`); + } + }); + } + + async loadData() { + try { + const params = { + page: this.currentPage, + page_size: this.config.pageSize, + search: this.currentSearch, + sort: this.currentSort, + filters: this.currentFilters + }; + + const response = await fetch(this.config.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params) + }); + + const data = await response.json(); + + if (data.success) { + this.renderTable(data.data); + this.updatePagination(data.pagination); + } else { + console.error('Table data loading failed:', data.error); + } + + } catch (error) { + console.error('Table data loading error:', error); + } + } + + renderTable(data) { + const table = document.getElementById(this.tableId); + const tbody = table?.querySelector('tbody'); + + if (!tbody) return; + + tbody.innerHTML = ''; + + data.forEach(row => { + const tr = document.createElement('tr'); + tr.dataset.id = row._id; + + // Render cells + Object.keys(row).forEach(key => { + if (key.startsWith('_')) return; // Skip metadata + + const td = document.createElement('td'); + const cellData = row[key]; + + if (typeof cellData === 'object' && cellData.formatted !== undefined) { + td.innerHTML = cellData.formatted; + td.dataset.raw = cellData.raw; + } else { + td.textContent = cellData; + } + + tr.appendChild(td); + }); + + // Add actions column if exists + if (row._actions && row._actions.length > 0) { + const actionsTd = document.createElement('td'); + actionsTd.className = 'actions-cell'; + actionsTd.innerHTML = this.renderActions(row._actions); + tr.appendChild(actionsTd); + } + + tbody.appendChild(tr); + }); + } + + renderActions(actions) { + return actions.map(action => { + const confirmAttr = action.confirm ? `onclick="return confirm('${action.confirm}')"` : ''; + const icon = action.icon ? `${action.icon}` : ''; + + return ` + ${icon}${action.label} + `; + }).join(' '); + } + + updatePagination(pagination) { + this.currentPage = pagination.page; + this.totalPages = pagination.total_pages; + this.totalItems = pagination.total_items; + + const paginationEl = document.getElementById(`${this.tableId}-pagination`); + if (!paginationEl) return; + + paginationEl.innerHTML = ` +
+ Zeige ${pagination.start_item}-${pagination.end_item} von ${pagination.total_items} Einträgen +
+
+ ${this.renderPaginationButtons()} +
+ `; + + // Event listeners für Pagination + paginationEl.querySelectorAll('.page-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + const page = parseInt(btn.dataset.page); + if (page !== this.currentPage) { + this.currentPage = page; + this.loadData(); + } + }); + }); + } + + renderPaginationButtons() { + const buttons = []; + const maxButtons = 7; + + // Previous button + buttons.push(` + + `); + + // Page number buttons + let startPage = Math.max(1, this.currentPage - Math.floor(maxButtons / 2)); + let endPage = Math.min(this.totalPages, startPage + maxButtons - 1); + + if (endPage - startPage + 1 < maxButtons) { + startPage = Math.max(1, endPage - maxButtons + 1); + } + + for (let i = startPage; i <= endPage; i++) { + buttons.push(` + + `); + } + + // Next button + buttons.push(` + + `); + + return buttons.join(''); + } + + exportData() { + const params = new URLSearchParams({ + search: this.currentSearch, + sort: JSON.stringify(this.currentSort), + filters: JSON.stringify(this.currentFilters), + format: 'csv' + }); + + window.open(`${this.config.apiUrl}/export?${params}`, '_blank'); + } + } + + // Auto-initialize tables with data-advanced-table attribute + document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[data-advanced-table]').forEach(table => { + const config = JSON.parse(table.dataset.advancedTable || '{}'); + new AdvancedTable(table.id, config); + }); + }); + """ + +def get_advanced_table_css() -> str: + """CSS für erweiterte Tabellen""" + return """ + .table-wrapper { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + } + + .table-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; + } + + .table-controls-left { + display: flex; + align-items: center; + gap: 1rem; + } + + .search-box { + position: relative; + } + + .search-input { + padding: 0.5rem 0.75rem; + padding-right: 2rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + } + + .search-icon { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + color: #6b7280; + } + + .page-size-selector { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + } + + .table-controls-right { + display: flex; + gap: 0.5rem; + } + + .advanced-table { + width: 100%; + border-collapse: collapse; + } + + .advanced-table th { + background: #f8f9fa; + padding: 0.75rem; + text-align: left; + font-weight: 600; + border-bottom: 2px solid #e9ecef; + position: relative; + } + + .advanced-table th[data-sortable="true"] { + cursor: pointer; + user-select: none; + } + + .advanced-table th[data-sortable="true"]:hover { + background: #e9ecef; + } + + .advanced-table th.sort-asc::after { + content: " ↑"; + color: #3b82f6; + } + + .advanced-table th.sort-desc::after { + content: " ↓"; + color: #3b82f6; + } + + .advanced-table td { + padding: 0.75rem; + border-bottom: 1px solid #e9ecef; + } + + .advanced-table tbody tr:hover { + background: #f8f9fa; + } + + .actions-cell { + white-space: nowrap; + } + + .action-btn { + display: inline-block; + padding: 0.25rem 0.5rem; + margin: 0 0.125rem; + font-size: 0.75rem; + text-decoration: none; + border-radius: 4px; + background: #e5e7eb; + color: #374151; + } + + .action-btn:hover { + background: #d1d5db; + } + + .action-btn.btn-primary { + background: #3b82f6; + color: white; + } + + .action-btn.btn-danger { + background: #ef4444; + color: white; + } + + .table-pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: #f8f9fa; + border-top: 1px solid #e9ecef; + } + + .pagination-controls { + display: flex; + gap: 0.25rem; + } + + .page-btn { + padding: 0.5rem 0.75rem; + border: 1px solid #d1d5db; + background: white; + cursor: pointer; + border-radius: 4px; + } + + .page-btn:hover:not(.disabled) { + background: #f3f4f6; + } + + .page-btn.active { + background: #3b82f6; + color: white; + border-color: #3b82f6; + } + + .page-btn.disabled { + opacity: 0.5; + cursor: not-allowed; + } + + @media (max-width: 768px) { + .table-controls { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .table-controls-left, + .table-controls-right { + justify-content: center; + } + + .advanced-table { + font-size: 0.875rem; + } + + .advanced-table th, + .advanced-table td { + padding: 0.5rem; + } + + .table-pagination { + flex-direction: column; + gap: 1rem; + } + } + """ \ No newline at end of file diff --git a/backend/utils/drag_drop_system.py b/backend/utils/drag_drop_system.py new file mode 100644 index 00000000..4b7a4c8c --- /dev/null +++ b/backend/utils/drag_drop_system.py @@ -0,0 +1,1231 @@ +""" +Drag & Drop System für das MYP-System +==================================== + +Dieses Modul stellt umfassende Drag & Drop Funktionalität bereit: +- Job-Reihenfolge per Drag & Drop ändern +- Multi-File-Upload mit Drag & Drop +- Visuelles Feedback und Validierung +- Progress-Tracking für Uploads +- Barrierefreie Alternative-Eingaben +- Touch-Support für mobile Geräte +""" + +import os +import json +import mimetypes +from datetime import datetime +from typing import Dict, List, Any, Optional, Tuple, Union +from dataclasses import dataclass, asdict +from flask import request, jsonify, current_app +from flask_login import current_user + +from utils.logging_config import get_logger +from models import Job, Printer, get_db_session +from utils.file_manager import save_job_file, save_temp_file +from config.settings import ALLOWED_EXTENSIONS, MAX_FILE_SIZE, UPLOAD_FOLDER + +logger = get_logger("drag_drop") + +@dataclass +class DragDropConfig: + """Konfiguration für Drag & Drop Bereiche""" + element_id: str + accepted_types: List[str] + max_files: int = 10 + max_file_size: int = MAX_FILE_SIZE + multiple: bool = True + auto_upload: bool = False + preview_mode: str = "thumbnail" # thumbnail, list, grid + validation_rules: Dict[str, Any] = None + +@dataclass +class FileUploadProgress: + """Upload-Progress für eine Datei""" + file_id: str + filename: str + size: int + uploaded_bytes: int = 0 + status: str = "pending" # pending, uploading, completed, error + error_message: str = None + upload_speed: float = 0.0 + eta_seconds: int = 0 + +class DragDropManager: + """Manager für Drag & Drop Operationen""" + + def __init__(self): + self.upload_sessions: Dict[str, Dict[str, FileUploadProgress]] = {} + self.job_order_cache: Dict[int, List[int]] = {} # printer_id -> job_ids order + + def create_upload_session(self, session_id: str) -> str: + """Erstellt eine neue Upload-Session""" + self.upload_sessions[session_id] = {} + logger.info(f"Upload-Session erstellt: {session_id}") + return session_id + + def add_file_to_session(self, session_id: str, file_progress: FileUploadProgress): + """Fügt eine Datei zur Upload-Session hinzu""" + if session_id not in self.upload_sessions: + self.upload_sessions[session_id] = {} + + self.upload_sessions[session_id][file_progress.file_id] = file_progress + + def update_file_progress(self, session_id: str, file_id: str, + uploaded_bytes: int, status: str = None, + error_message: str = None): + """Aktualisiert Upload-Progress""" + if session_id in self.upload_sessions and file_id in self.upload_sessions[session_id]: + progress = self.upload_sessions[session_id][file_id] + progress.uploaded_bytes = uploaded_bytes + + if status: + progress.status = status + if error_message: + progress.error_message = error_message + + # Berechne Upload-Geschwindigkeit und ETA + if progress.size > 0: + progress_percent = uploaded_bytes / progress.size + if progress_percent > 0: + progress.eta_seconds = int((progress.size - uploaded_bytes) / max(progress.upload_speed, 1)) + + def get_session_progress(self, session_id: str) -> Dict[str, Any]: + """Holt Progress-Informationen für eine Session""" + if session_id not in self.upload_sessions: + return {'files': [], 'total_progress': 0} + + files = list(self.upload_sessions[session_id].values()) + total_size = sum(f.size for f in files) + total_uploaded = sum(f.uploaded_bytes for f in files) + + total_progress = (total_uploaded / total_size * 100) if total_size > 0 else 0 + + return { + 'files': [asdict(f) for f in files], + 'total_progress': total_progress, + 'total_size': total_size, + 'total_uploaded': total_uploaded, + 'files_completed': len([f for f in files if f.status == 'completed']), + 'files_error': len([f for f in files if f.status == 'error']) + } + + def cleanup_session(self, session_id: str): + """Bereinigt eine Upload-Session""" + if session_id in self.upload_sessions: + del self.upload_sessions[session_id] + logger.info(f"Upload-Session bereinigt: {session_id}") + + def update_job_order(self, printer_id: int, job_ids: List[int]) -> bool: + """Aktualisiert die Job-Reihenfolge für einen Drucker""" + try: + with get_db_session() as db_session: + # Validiere dass alle Jobs existieren und zum Drucker gehören + jobs = db_session.query(Job).filter( + Job.id.in_(job_ids), + Job.printer_id == printer_id, + Job.status.in_(['scheduled', 'paused']) + ).all() + + if len(jobs) != len(job_ids): + logger.warning(f"Nicht alle Jobs gefunden oder gehören zu Drucker {printer_id}") + return False + + # Cache aktualisieren + self.job_order_cache[printer_id] = job_ids + + # Optional: In Datenbank speichern (erweiterte Implementierung) + # Hier könnte man ein separates Job-Order-Table verwenden + + logger.info(f"Job-Reihenfolge für Drucker {printer_id} aktualisiert: {job_ids}") + return True + + except Exception as e: + logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}") + return False + + def get_job_order(self, printer_id: int) -> List[int]: + """Holt die aktuelle Job-Reihenfolge für einen Drucker""" + return self.job_order_cache.get(printer_id, []) + +# Globale Instanz +drag_drop_manager = DragDropManager() + +def validate_file_upload(file_data: Dict[str, Any], config: DragDropConfig) -> Tuple[bool, str]: + """Validiert eine Datei für Upload""" + filename = file_data.get('name', '') + file_size = file_data.get('size', 0) + file_type = file_data.get('type', '') + + # Dateiname prüfen + if not filename: + return False, "Dateiname ist erforderlich" + + # Dateierweiterung prüfen + file_extension = filename.rsplit('.', 1)[-1].lower() if '.' in filename else '' + if file_extension not in config.accepted_types: + return False, f"Dateityp '{file_extension}' nicht erlaubt. Erlaubt: {', '.join(config.accepted_types)}" + + # Dateigröße prüfen + if file_size > config.max_file_size: + max_mb = config.max_file_size / (1024 * 1024) + return False, f"Datei zu groß. Maximum: {max_mb:.1f} MB" + + if file_size <= 0: + return False, "Datei ist leer" + + # MIME-Type validieren + expected_mime = mimetypes.guess_type(filename)[0] + if expected_mime and file_type and not file_type.startswith(expected_mime.split('/')[0]): + return False, "MIME-Type stimmt nicht mit Dateierweiterung überein" + + # Custom Validierung + if config.validation_rules: + for rule_name, rule_value in config.validation_rules.items(): + if rule_name == 'min_size' and file_size < rule_value: + return False, f"Datei zu klein. Minimum: {rule_value} Bytes" + elif rule_name == 'max_name_length' and len(filename) > rule_value: + return False, f"Dateiname zu lang. Maximum: {rule_value} Zeichen" + + return True, "" + +def get_drag_drop_javascript() -> str: + """Generiert JavaScript für Drag & Drop Funktionalität""" + return """ + class DragDropManager { + constructor() { + this.dropZones = new Map(); + this.uploadSessions = new Map(); + this.sortableContainers = new Map(); + + this.setupGlobalEventListeners(); + } + + setupGlobalEventListeners() { + // Verhindere Standard-Drag-Verhalten für das gesamte Dokument + document.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + document.addEventListener('drop', (e) => { + e.preventDefault(); + }); + } + + createDropZone(elementId, config = {}) { + const element = document.getElementById(elementId); + if (!element) { + console.error(`Drop-Zone Element nicht gefunden: ${elementId}`); + return null; + } + + const dropZone = new DropZone(element, config); + this.dropZones.set(elementId, dropZone); + + return dropZone; + } + + createSortableContainer(elementId, config = {}) { + const element = document.getElementById(elementId); + if (!element) { + console.error(`Sortable Container nicht gefunden: ${elementId}`); + return null; + } + + const sortable = new SortableContainer(element, config); + this.sortableContainers.set(elementId, sortable); + + return sortable; + } + + createUploadSession(sessionId) { + const session = new UploadSession(sessionId); + this.uploadSessions.set(sessionId, session); + return session; + } + + getUploadSession(sessionId) { + return this.uploadSessions.get(sessionId); + } + } + + class DropZone { + constructor(element, config = {}) { + this.element = element; + this.config = { + acceptedTypes: ['*'], + maxFiles: 10, + maxFileSize: 10 * 1024 * 1024, // 10MB + multiple: true, + autoUpload: false, + previewMode: 'thumbnail', + uploadUrl: '/api/upload/drag-drop', + onFilesAdded: null, + onUploadProgress: null, + onUploadComplete: null, + onError: null, + ...config + }; + + this.files = []; + this.dragCounter = 0; + + this.setupElement(); + this.setupEventListeners(); + } + + setupElement() { + this.element.classList.add('drag-drop-zone'); + + if (!this.element.innerHTML.trim()) { + this.element.innerHTML = ` +
+
📁
+
+ Dateien hierher ziehen oder + +
+
+ ${this.getAcceptedTypesText()} +
+
+
+ + `; + } + + this.fileInput = this.element.querySelector('.file-input'); + this.previewContainer = this.element.querySelector('.file-preview-container'); + this.selectButton = this.element.querySelector('.file-select-btn'); + } + + setupEventListeners() { + // Drag & Drop Events + this.element.addEventListener('dragenter', (e) => { + e.preventDefault(); + this.dragCounter++; + this.element.classList.add('drag-over'); + }); + + this.element.addEventListener('dragleave', (e) => { + e.preventDefault(); + this.dragCounter--; + if (this.dragCounter === 0) { + this.element.classList.remove('drag-over'); + } + }); + + this.element.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + this.element.addEventListener('drop', (e) => { + e.preventDefault(); + this.dragCounter = 0; + this.element.classList.remove('drag-over'); + + const files = Array.from(e.dataTransfer.files); + this.handleFiles(files); + }); + + // File Input Events + this.selectButton?.addEventListener('click', () => { + this.fileInput.click(); + }); + + this.fileInput.addEventListener('change', (e) => { + const files = Array.from(e.target.files); + this.handleFiles(files); + }); + + // Keyboard Support + this.selectButton?.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.fileInput.click(); + } + }); + } + + handleFiles(files) { + // Validierung + const validFiles = []; + const errors = []; + + for (const file of files) { + const validation = this.validateFile(file); + if (validation.valid) { + validFiles.push(file); + } else { + errors.push(`${file.name}: ${validation.error}`); + } + } + + // Fehler anzeigen + if (errors.length > 0) { + this.showErrors(errors); + } + + // Gültige Dateien hinzufügen + if (validFiles.length > 0) { + this.addFiles(validFiles); + } + } + + validateFile(file) { + // Anzahl Dateien prüfen + if (!this.config.multiple && this.files.length >= 1) { + return { valid: false, error: 'Nur eine Datei erlaubt' }; + } + + if (this.files.length >= this.config.maxFiles) { + return { valid: false, error: `Maximum ${this.config.maxFiles} Dateien erlaubt` }; + } + + // Dateigröße prüfen + if (file.size > this.config.maxFileSize) { + const maxMB = this.config.maxFileSize / (1024 * 1024); + return { valid: false, error: `Datei zu groß. Maximum: ${maxMB.toFixed(1)} MB` }; + } + + // Dateityp prüfen + if (!this.isFileTypeAccepted(file)) { + return { valid: false, error: `Dateityp nicht erlaubt: ${file.type}` }; + } + + return { valid: true }; + } + + isFileTypeAccepted(file) { + if (this.config.acceptedTypes.includes('*')) { + return true; + } + + const fileExtension = file.name.split('.').pop().toLowerCase(); + return this.config.acceptedTypes.includes(fileExtension) || + this.config.acceptedTypes.some(type => file.type.includes(type)); + } + + addFiles(files) { + files.forEach(file => { + const fileObj = { + id: this.generateFileId(), + file: file, + name: file.name, + size: file.size, + type: file.type, + preview: null, + uploadProgress: 0, + status: 'pending' // pending, uploading, completed, error + }; + + this.files.push(fileObj); + this.createFilePreview(fileObj); + }); + + this.updateUI(); + + // Callback aufrufen + if (this.config.onFilesAdded) { + this.config.onFilesAdded(files, this.files); + } + + // Auto-Upload + if (this.config.autoUpload) { + this.uploadFiles(); + } + } + + createFilePreview(fileObj) { + const preview = document.createElement('div'); + preview.className = 'file-preview'; + preview.dataset.fileId = fileObj.id; + + // Preview basierend auf Modus + if (this.config.previewMode === 'thumbnail') { + preview.innerHTML = this.createThumbnailPreview(fileObj); + } else { + preview.innerHTML = this.createListPreview(fileObj); + } + + this.previewContainer.appendChild(preview); + + // Remove Button + const removeBtn = preview.querySelector('.remove-file-btn'); + removeBtn?.addEventListener('click', () => { + this.removeFile(fileObj.id); + }); + + // Thumbnail generieren (für Bilder) + if (fileObj.file.type.startsWith('image/')) { + this.generateThumbnail(fileObj, preview); + } + } + + createThumbnailPreview(fileObj) { + return ` +
+
+ ${this.getFileIcon(fileObj.type)} +
+
+
${fileObj.name}
+
${this.formatFileSize(fileObj.size)}
+
+
+
+
+ ${fileObj.uploadProgress}% +
+
+ +
+ `; + } + + createListPreview(fileObj) { + return ` +
+ ${this.getFileIcon(fileObj.type)} + ${fileObj.name} + ${this.formatFileSize(fileObj.size)} +
+
+
+
+
+ +
+ `; + } + + generateThumbnail(fileObj, previewElement) { + const reader = new FileReader(); + reader.onload = (e) => { + const img = previewElement.querySelector('.file-icon'); + if (img) { + img.innerHTML = `${fileObj.name}`; + } + }; + reader.readAsDataURL(fileObj.file); + } + + getFileIcon(fileType) { + if (fileType.startsWith('image/')) return '🖼️'; + if (fileType.startsWith('video/')) return '🎬'; + if (fileType.startsWith('audio/')) return '🎵'; + if (fileType.includes('pdf')) return '📄'; + if (fileType.includes('text/') || fileType.includes('document')) return '📝'; + if (fileType.includes('zip') || fileType.includes('archive')) return '🗜️'; + return '📁'; + } + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + removeFile(fileId) { + this.files = this.files.filter(f => f.id !== fileId); + + const preview = this.previewContainer.querySelector(`[data-file-id="${fileId}"]`); + if (preview) { + preview.remove(); + } + + this.updateUI(); + } + + updateUI() { + // UI-Updates basierend auf Dateien-Status + if (this.files.length === 0) { + this.element.classList.remove('has-files'); + } else { + this.element.classList.add('has-files'); + } + } + + async uploadFiles() { + const sessionId = this.generateSessionId(); + + for (const fileObj of this.files) { + if (fileObj.status === 'pending') { + await this.uploadFile(fileObj, sessionId); + } + } + } + + async uploadFile(fileObj, sessionId) { + fileObj.status = 'uploading'; + + const formData = new FormData(); + formData.append('file', fileObj.file); + formData.append('session_id', sessionId); + formData.append('file_id', fileObj.id); + + try { + const response = await fetch(this.config.uploadUrl, { + method: 'POST', + body: formData, + // Progress tracking würde XMLHttpRequest benötigen + }); + + if (response.ok) { + const result = await response.json(); + fileObj.status = 'completed'; + fileObj.uploadProgress = 100; + fileObj.uploadResult = result; + + this.updateFilePreview(fileObj); + + if (this.config.onUploadComplete) { + this.config.onUploadComplete(fileObj, result); + } + } else { + throw new Error(`Upload fehlgeschlagen: ${response.status}`); + } + + } catch (error) { + fileObj.status = 'error'; + fileObj.error = error.message; + + this.updateFilePreview(fileObj); + + if (this.config.onError) { + this.config.onError(fileObj, error); + } + } + } + + updateFilePreview(fileObj) { + const preview = this.previewContainer.querySelector(`[data-file-id="${fileObj.id}"]`); + if (preview) { + const progressFill = preview.querySelector('.progress-fill'); + const progressText = preview.querySelector('.progress-text'); + + if (progressFill) { + progressFill.style.width = `${fileObj.uploadProgress}%`; + } + + if (progressText) { + progressText.textContent = `${fileObj.uploadProgress}%`; + } + + preview.classList.remove('status-pending', 'status-uploading', 'status-completed', 'status-error'); + preview.classList.add(`status-${fileObj.status}`); + } + } + + getAcceptedTypesText() { + if (this.config.acceptedTypes.includes('*')) { + return 'Alle Dateitypen erlaubt'; + } + return `Erlaubt: ${this.config.acceptedTypes.join(', ')}`; + } + + getAcceptAttribute() { + if (this.config.acceptedTypes.includes('*')) { + return '*/*'; + } + return this.config.acceptedTypes.map(type => `.${type}`).join(','); + } + + showErrors(errors) { + const errorContainer = document.createElement('div'); + errorContainer.className = 'upload-errors'; + errorContainer.innerHTML = ` +
Upload-Fehler:
+ + + `; + + this.element.appendChild(errorContainer); + + errorContainer.querySelector('.close-errors-btn').addEventListener('click', () => { + errorContainer.remove(); + }); + + // Auto-remove nach 5 Sekunden + setTimeout(() => { + if (errorContainer.parentNode) { + errorContainer.remove(); + } + }, 5000); + } + + generateFileId() { + return 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + + generateSessionId() { + return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + } + + class SortableContainer { + constructor(element, config = {}) { + this.element = element; + this.config = { + handle: null, + placeholder: 'sortable-placeholder', + ghostClass: 'sortable-ghost', + chosenClass: 'sortable-chosen', + dragClass: 'sortable-drag', + onUpdate: null, + onSort: null, + disabled: false, + animation: 150, + ...config + }; + + this.dragElement = null; + this.placeholder = null; + this.isDragging = false; + this.startIndex = -1; + this.endIndex = -1; + + this.setupSortable(); + } + + setupSortable() { + this.element.classList.add('sortable-container'); + + // Event Listeners für alle draggable Items + this.updateEventListeners(); + } + + updateEventListeners() { + const items = this.getSortableItems(); + + items.forEach((item, index) => { + item.draggable = true; + item.dataset.sortableIndex = index; + + // Remove existing listeners + item.removeEventListener('dragstart', this.handleDragStart); + item.removeEventListener('dragend', this.handleDragEnd); + item.removeEventListener('dragover', this.handleDragOver); + item.removeEventListener('drop', this.handleDrop); + + // Add new listeners + item.addEventListener('dragstart', this.handleDragStart.bind(this)); + item.addEventListener('dragend', this.handleDragEnd.bind(this)); + item.addEventListener('dragover', this.handleDragOver.bind(this)); + item.addEventListener('drop', this.handleDrop.bind(this)); + }); + } + + getSortableItems() { + return Array.from(this.element.children).filter(child => + !child.classList.contains('sortable-placeholder') && + !child.classList.contains('non-sortable') + ); + } + + handleDragStart(e) { + if (this.config.disabled) { + e.preventDefault(); + return; + } + + this.isDragging = true; + this.dragElement = e.target.closest('[draggable="true"]'); + this.startIndex = parseInt(this.dragElement.dataset.sortableIndex); + + this.dragElement.classList.add(this.config.chosenClass); + + // Create placeholder + this.createPlaceholder(); + + // Set drag data + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/html', this.dragElement.outerHTML); + + // Add ghost class after a delay to allow proper drag image + setTimeout(() => { + if (this.dragElement) { + this.dragElement.classList.add(this.config.ghostClass); + } + }, 0); + } + + handleDragEnd(e) { + if (!this.isDragging) return; + + this.isDragging = false; + + // Clean up classes + if (this.dragElement) { + this.dragElement.classList.remove( + this.config.chosenClass, + this.config.ghostClass, + this.config.dragClass + ); + } + + // Remove placeholder + this.removePlaceholder(); + + // Update positions + this.endIndex = this.findElementIndex(this.dragElement); + + if (this.startIndex !== this.endIndex) { + // Trigger callbacks + if (this.config.onUpdate) { + this.config.onUpdate({ + item: this.dragElement, + oldIndex: this.startIndex, + newIndex: this.endIndex, + from: this.element, + to: this.element + }); + } + + if (this.config.onSort) { + this.config.onSort(this.getSortOrder()); + } + } + + // Reset + this.dragElement = null; + this.startIndex = -1; + this.endIndex = -1; + + // Update event listeners for new order + this.updateEventListeners(); + } + + handleDragOver(e) { + if (!this.isDragging) return; + + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + + const afterElement = this.getDragAfterElement(e.clientY); + + if (afterElement == null) { + this.element.appendChild(this.placeholder); + } else { + this.element.insertBefore(this.placeholder, afterElement); + } + } + + handleDrop(e) { + if (!this.isDragging) return; + + e.preventDefault(); + + // Insert the dragged element at the placeholder position + if (this.placeholder && this.dragElement) { + this.element.insertBefore(this.dragElement, this.placeholder); + } + } + + createPlaceholder() { + this.placeholder = document.createElement('div'); + this.placeholder.className = this.config.placeholder; + this.placeholder.style.height = this.dragElement.offsetHeight + 'px'; + this.placeholder.innerHTML = '
Drop hier einfügen
'; + } + + removePlaceholder() { + if (this.placeholder && this.placeholder.parentNode) { + this.placeholder.parentNode.removeChild(this.placeholder); + } + this.placeholder = null; + } + + getDragAfterElement(y) { + const draggableElements = [...this.element.querySelectorAll('[draggable="true"]:not(.sortable-ghost)')]; + + return draggableElements.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + + findElementIndex(element) { + const items = this.getSortableItems(); + return items.indexOf(element); + } + + getSortOrder() { + return this.getSortableItems().map(item => ({ + element: item, + id: item.dataset.id || item.id, + index: this.findElementIndex(item) + })); + } + + disable() { + this.config.disabled = true; + this.element.classList.add('sortable-disabled'); + } + + enable() { + this.config.disabled = false; + this.element.classList.remove('sortable-disabled'); + } + } + + class UploadSession { + constructor(sessionId) { + this.sessionId = sessionId; + this.files = new Map(); + this.totalSize = 0; + this.uploadedSize = 0; + } + + addFile(fileId, fileSize) { + this.files.set(fileId, { + id: fileId, + size: fileSize, + uploaded: 0, + status: 'pending' + }); + this.totalSize += fileSize; + } + + updateFileProgress(fileId, uploadedBytes, status = null) { + const file = this.files.get(fileId); + if (file) { + const previousUploaded = file.uploaded; + file.uploaded = uploadedBytes; + + if (status) { + file.status = status; + } + + // Update total uploaded + this.uploadedSize += (uploadedBytes - previousUploaded); + } + } + + getProgress() { + return { + totalSize: this.totalSize, + uploadedSize: this.uploadedSize, + percentage: this.totalSize > 0 ? (this.uploadedSize / this.totalSize) * 100 : 0, + files: Array.from(this.files.values()) + }; + } + } + + // CSS für Drag & Drop (als String für Injection) + const dragDropCSS = ` + .drag-drop-zone { + border: 2px dashed #d1d5db; + border-radius: 8px; + padding: 2rem; + text-align: center; + transition: all 0.3s ease; + background-color: #f9fafb; + position: relative; + } + + .drag-drop-zone.drag-over { + border-color: #3b82f6; + background-color: #eff6ff; + transform: scale(1.02); + } + + .drag-drop-zone.has-files { + border-style: solid; + background-color: white; + } + + .drop-zone-content { + margin-bottom: 1rem; + } + + .drop-zone-icon { + font-size: 3rem; + margin-bottom: 1rem; + } + + .drop-zone-text { + font-size: 1.1rem; + color: #374151; + margin-bottom: 0.5rem; + } + + .file-select-btn { + color: #3b82f6; + text-decoration: underline; + background: none; + border: none; + cursor: pointer; + font-size: inherit; + } + + .file-select-btn:hover { + color: #1d4ed8; + } + + .drop-zone-info { + font-size: 0.875rem; + color: #6b7280; + } + + .file-preview-container { + margin-top: 1rem; + } + + .file-preview { + margin-bottom: 0.5rem; + animation: slideIn 0.3s ease; + } + + .file-thumbnail { + display: flex; + align-items: center; + background: white; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 0.75rem; + position: relative; + } + + .file-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + margin-right: 0.75rem; + border-radius: 4px; + background: #f3f4f6; + } + + .file-info { + flex: 1; + min-width: 0; + } + + .file-name { + font-weight: 500; + color: #111827; + truncate: true; + margin-bottom: 0.25rem; + } + + .file-size { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 0.5rem; + } + + .file-progress { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .progress-bar { + flex: 1; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + overflow: hidden; + } + + .progress-fill { + height: 100%; + background: #3b82f6; + transition: width 0.3s ease; + } + + .progress-text { + font-size: 0.75rem; + color: #6b7280; + min-width: 3rem; + text-align: right; + } + + .remove-file-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 1.5rem; + height: 1.5rem; + border: none; + background: #ef4444; + color: white; + border-radius: 50%; + cursor: pointer; + font-size: 1rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .remove-file-btn:hover { + background: #dc2626; + } + + .file-preview.status-completed .progress-fill { + background: #10b981; + } + + .file-preview.status-error .progress-fill { + background: #ef4444; + } + + .file-preview.status-error { + border-color: #fecaca; + background: #fef2f2; + } + + .upload-errors { + margin-top: 1rem; + padding: 1rem; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 6px; + color: #dc2626; + } + + .error-title { + font-weight: 600; + margin-bottom: 0.5rem; + } + + .close-errors-btn { + margin-top: 0.5rem; + padding: 0.25rem 0.5rem; + background: #dc2626; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + } + + /* Sortable Styles */ + .sortable-container { + min-height: 2rem; + } + + .sortable-container [draggable="true"] { + cursor: move; + transition: all 0.15s ease; + } + + .sortable-container [draggable="true"]:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + .sortable-placeholder { + background: #eff6ff; + border: 2px dashed #3b82f6; + border-radius: 6px; + margin: 0.25rem 0; + opacity: 0.8; + display: flex; + align-items: center; + justify-content: center; + color: #3b82f6; + font-weight: 500; + } + + .placeholder-content { + padding: 1rem; + } + + .sortable-ghost { + opacity: 0.4; + transform: scale(0.98); + } + + .sortable-chosen { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + .sortable-disabled [draggable="true"] { + cursor: not-allowed; + opacity: 0.6; + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Mobile Touch Support */ + @media (max-width: 768px) { + .drag-drop-zone { + padding: 1rem; + } + + .drop-zone-text { + font-size: 1rem; + } + + .file-thumbnail { + padding: 0.5rem; + } + + .file-icon { + width: 32px; + height: 32px; + font-size: 1.25rem; + } + } + `; + + // CSS automatisch injizieren + if (!document.getElementById('drag-drop-styles')) { + const style = document.createElement('style'); + style.id = 'drag-drop-styles'; + style.textContent = dragDropCSS; + document.head.appendChild(style); + } + + // Globale Instanz erstellen + window.dragDropManager = new DragDropManager(); + + // Auto-Initialize für vorhandene Drop-Zones + document.addEventListener('DOMContentLoaded', function() { + // Auto-init für Elemente mit data-drag-drop Attribut + document.querySelectorAll('[data-drag-drop]').forEach(element => { + const config = JSON.parse(element.dataset.dragDrop || '{}'); + window.dragDropManager.createDropZone(element.id, config); + }); + + // Auto-init für Sortable Container + document.querySelectorAll('[data-sortable]').forEach(element => { + const config = JSON.parse(element.dataset.sortable || '{}'); + window.dragDropManager.createSortableContainer(element.id, config); + }); + }); + """ + +def get_drag_drop_css() -> str: + """Gibt CSS für Drag & Drop zurück""" + return """ + /* Drag & Drop CSS ist bereits im JavaScript enthalten */ + """ \ No newline at end of file diff --git a/backend/utils/realtime_dashboard.py b/backend/utils/realtime_dashboard.py new file mode 100644 index 00000000..81debc8a --- /dev/null +++ b/backend/utils/realtime_dashboard.py @@ -0,0 +1,1137 @@ +""" +Echtzeit-Dashboard-System für das MYP-System +========================================== + +Dieses Modul stellt ein erweiterte Dashboard mit Echtzeit-Updates bereit: +- WebSocket-basierte Live-Updates +- Ereignis-gesteuerte Benachrichtigungen +- Modulare Dashboard-Widgets +- Performance-optimierte Datenabfragen +- Responsive Design mit automatischer Anpassung +""" + +import asyncio +import json +import logging +import time +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Callable, Set +from dataclasses import dataclass, asdict +from enum import Enum +from collections import defaultdict, deque +import threading +from concurrent.futures import ThreadPoolExecutor + +# WebSocket-Support +try: + from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect + from eventlet import wsgi, listen + import eventlet + WEBSOCKET_AVAILABLE = True +except ImportError: + WEBSOCKET_AVAILABLE = False + SocketIO = None + +from flask import request, session, current_app +from flask_login import current_user + +from utils.logging_config import get_logger +from models import Job, User, Printer, Stats, GuestRequest, get_db_session +from utils.analytics import track_event, get_dashboard_stats + +logger = get_logger("dashboard") + +class EventType(Enum): + """Typen von Dashboard-Ereignissen""" + JOB_CREATED = "job_created" + JOB_STARTED = "job_started" + JOB_FINISHED = "job_finished" + JOB_CANCELLED = "job_cancelled" + JOB_PAUSED = "job_paused" + JOB_RESUMED = "job_resumed" + + PRINTER_ONLINE = "printer_online" + PRINTER_OFFLINE = "printer_offline" + PRINTER_BUSY = "printer_busy" + PRINTER_IDLE = "printer_idle" + + USER_LOGIN = "user_login" + USER_LOGOUT = "user_logout" + USER_CREATED = "user_created" + + GUEST_REQUEST = "guest_request" + GUEST_APPROVED = "guest_approved" + GUEST_REJECTED = "guest_rejected" + + SYSTEM_ALERT = "system_alert" + SYSTEM_WARNING = "system_warning" + SYSTEM_INFO = "system_info" + +@dataclass +class DashboardEvent: + """Dashboard-Ereignis mit Metadaten""" + event_type: EventType + data: Dict[str, Any] + timestamp: datetime + user_id: Optional[int] = None + priority: str = "normal" # low, normal, high, critical + + def to_dict(self) -> Dict[str, Any]: + """Konvertiert das Ereignis zu einem Dictionary""" + return { + 'event_type': self.event_type.value, + 'data': self.data, + 'timestamp': self.timestamp.isoformat(), + 'user_id': self.user_id, + 'priority': self.priority + } + +@dataclass +class WidgetConfig: + """Konfiguration für Dashboard-Widgets""" + widget_id: str + title: str + widget_type: str # chart, table, metric, alert, custom + refresh_interval: int = 30 # Sekunden + data_source: str = "" + chart_type: Optional[str] = None # line, bar, pie, donut + filters: Dict[str, Any] = None + permissions: List[str] = None + size: str = "medium" # small, medium, large, full + +class DashboardManager: + """Zentraler Manager für Dashboard-Funktionalität""" + + def __init__(self): + self.socketio: Optional[SocketIO] = None + self.connected_clients: Set[str] = set() + self.user_rooms: Dict[int, Set[str]] = defaultdict(set) + self.event_queue: deque = deque(maxlen=1000) + self.event_handlers: Dict[EventType, List[Callable]] = defaultdict(list) + self.widgets: Dict[str, WidgetConfig] = {} + self.cached_data: Dict[str, Any] = {} + self.cache_timestamps: Dict[str, datetime] = {} + self.executor = ThreadPoolExecutor(max_workers=4) + + self._setup_default_widgets() + self._start_background_tasks() + + def init_socketio(self, app, cors_allowed_origins="*"): + """Initialisiert SocketIO für das Dashboard""" + if not WEBSOCKET_AVAILABLE: + logger.warning("WebSocket-Funktionalität nicht verfügbar. Flask-SocketIO installieren.") + return None + + self.socketio = SocketIO( + app, + cors_allowed_origins=cors_allowed_origins, + logger=False, + engineio_logger=False, + async_mode='eventlet' + ) + + self._setup_socket_handlers() + logger.info("Dashboard WebSocket-Server initialisiert") + return self.socketio + + def _setup_socket_handlers(self): + """Richtet WebSocket-Event-Handler ein""" + if not self.socketio: + return + + @self.socketio.on('connect') + def handle_connect(auth): + """Behandelt neue WebSocket-Verbindungen""" + if not current_user.is_authenticated: + logger.warning(f"Nicht authentifizierte WebSocket-Verbindung von {request.remote_addr}") + disconnect() + return False + + client_id = request.sid + user_id = current_user.id + + self.connected_clients.add(client_id) + self.user_rooms[user_id].add(client_id) + + # Benutzer zu seiner persönlichen Room hinzufügen + join_room(f"user_{user_id}") + + # Admin-Benutzer zu Admin-Room hinzufügen + if current_user.is_admin: + join_room("admin") + + logger.info(f"Dashboard-Verbindung: User {user_id} ({client_id})") + + # Initiale Dashboard-Daten senden + self._send_initial_data(client_id, user_id) + + return True + + @self.socketio.on('disconnect') + def handle_disconnect(): + """Behandelt getrennte WebSocket-Verbindungen""" + client_id = request.sid + user_id = current_user.id if current_user.is_authenticated else None + + self.connected_clients.discard(client_id) + + if user_id: + self.user_rooms[user_id].discard(client_id) + if not self.user_rooms[user_id]: + del self.user_rooms[user_id] + + logger.info(f"Dashboard-Trennung: User {user_id} ({client_id})") + + @self.socketio.on('subscribe_widget') + def handle_widget_subscription(data): + """Behandelt Widget-Abonnements""" + widget_id = data.get('widget_id') + refresh_interval = data.get('refresh_interval', 30) + + if widget_id in self.widgets: + join_room(f"widget_{widget_id}") + # Sofortige Datenaktualisierung + widget_data = self._get_widget_data(widget_id) + emit('widget_update', { + 'widget_id': widget_id, + 'data': widget_data, + 'timestamp': datetime.now().isoformat() + }) + + @self.socketio.on('unsubscribe_widget') + def handle_widget_unsubscription(data): + """Behandelt Widget-Abbestellungen""" + widget_id = data.get('widget_id') + leave_room(f"widget_{widget_id}") + + @self.socketio.on('request_data') + def handle_data_request(data): + """Behandelt manuelle Datenabfragen""" + data_type = data.get('type') + filters = data.get('filters', {}) + + try: + if data_type == 'jobs': + result = self._get_jobs_data(filters) + elif data_type == 'printers': + result = self._get_printers_data(filters) + elif data_type == 'users': + result = self._get_users_data(filters) + elif data_type == 'stats': + result = self._get_stats_data(filters) + else: + result = {'error': 'Unbekannter Datentyp'} + + emit('data_response', { + 'type': data_type, + 'data': result, + 'timestamp': datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"Fehler bei Datenabfrage {data_type}: {str(e)}") + emit('data_response', { + 'type': data_type, + 'error': str(e), + 'timestamp': datetime.now().isoformat() + }) + + def _send_initial_data(self, client_id: str, user_id: int): + """Sendet initiale Dashboard-Daten an einen neuen Client""" + try: + # Basis-Dashboard-Daten + dashboard_data = { + 'widgets': {wid: asdict(widget) for wid, widget in self.widgets.items()}, + 'user_permissions': self._get_user_permissions(user_id), + 'system_status': self._get_system_status(), + 'recent_events': [event.to_dict() for event in list(self.event_queue)[-10:]] + } + + self.socketio.emit('dashboard_init', dashboard_data, room=client_id) + + except Exception as e: + logger.error(f"Fehler beim Senden der initialen Daten: {str(e)}") + + def _setup_default_widgets(self): + """Richtet Standard-Dashboard-Widgets ein""" + self.widgets = { + 'active_jobs': WidgetConfig( + widget_id='active_jobs', + title='Aktive Jobs', + widget_type='metric', + refresh_interval=15, + data_source='jobs', + size='small' + ), + 'online_printers': WidgetConfig( + widget_id='online_printers', + title='Online Drucker', + widget_type='metric', + refresh_interval=30, + data_source='printers', + size='small' + ), + 'jobs_timeline': WidgetConfig( + widget_id='jobs_timeline', + title='Jobs Timeline', + widget_type='chart', + chart_type='line', + refresh_interval=60, + data_source='jobs', + size='large' + ), + 'printer_status': WidgetConfig( + widget_id='printer_status', + title='Drucker Status', + widget_type='chart', + chart_type='donut', + refresh_interval=30, + data_source='printers', + size='medium' + ), + 'recent_jobs': WidgetConfig( + widget_id='recent_jobs', + title='Letzte Jobs', + widget_type='table', + refresh_interval=30, + data_source='jobs', + size='large' + ), + 'system_alerts': WidgetConfig( + widget_id='system_alerts', + title='System-Meldungen', + widget_type='alert', + refresh_interval=10, + data_source='alerts', + size='full' + ), + 'user_activity': WidgetConfig( + widget_id='user_activity', + title='Benutzer-Aktivität', + widget_type='chart', + chart_type='bar', + refresh_interval=300, + data_source='users', + size='medium', + permissions=['admin'] + ), + 'guest_requests': WidgetConfig( + widget_id='guest_requests', + title='Gast-Anfragen', + widget_type='metric', + refresh_interval=60, + data_source='guest_requests', + size='small', + permissions=['admin'] + ) + } + + def _start_background_tasks(self): + """Startet Hintergrundaufgaben für das Dashboard""" + def background_worker(): + """Hintergrund-Worker für regelmäßige Updates""" + while True: + try: + self._update_cached_data() + self._broadcast_widget_updates() + self._cleanup_old_events() + time.sleep(10) # Alle 10 Sekunden prüfen + except Exception as e: + logger.error(f"Fehler im Dashboard-Background-Worker: {str(e)}") + time.sleep(30) # Längere Pause bei Fehlern + + # Background-Thread starten + background_thread = threading.Thread(target=background_worker, daemon=True) + background_thread.start() + logger.info("Dashboard-Background-Worker gestartet") + + def _update_cached_data(self): + """Aktualisiert gecachte Dashboard-Daten""" + current_time = datetime.now() + + for widget_id, widget in self.widgets.items(): + cache_key = f"widget_{widget_id}" + last_update = self.cache_timestamps.get(cache_key, datetime.min) + + if (current_time - last_update).total_seconds() >= widget.refresh_interval: + try: + new_data = self._get_widget_data(widget_id) + self.cached_data[cache_key] = new_data + self.cache_timestamps[cache_key] = current_time + except Exception as e: + logger.error(f"Fehler beim Cache-Update für Widget {widget_id}: {str(e)}") + + def _broadcast_widget_updates(self): + """Sendet Widget-Updates an alle verbundenen Clients""" + if not self.socketio: + return + + current_time = datetime.now() + + for widget_id, widget in self.widgets.items(): + cache_key = f"widget_{widget_id}" + + if cache_key in self.cached_data: + widget_data = self.cached_data[cache_key] + + self.socketio.emit('widget_update', { + 'widget_id': widget_id, + 'data': widget_data, + 'timestamp': current_time.isoformat() + }, room=f"widget_{widget_id}") + + def _get_widget_data(self, widget_id: str) -> Dict[str, Any]: + """Holt Daten für ein spezifisches Widget""" + widget = self.widgets.get(widget_id) + if not widget: + return {'error': 'Widget nicht gefunden'} + + try: + if widget.data_source == 'jobs': + return self._get_jobs_widget_data(widget) + elif widget.data_source == 'printers': + return self._get_printers_widget_data(widget) + elif widget.data_source == 'users': + return self._get_users_widget_data(widget) + elif widget.data_source == 'alerts': + return self._get_alerts_widget_data(widget) + elif widget.data_source == 'guest_requests': + return self._get_guest_requests_widget_data(widget) + else: + return {'error': 'Unbekannte Datenquelle'} + + except Exception as e: + logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}") + return {'error': str(e)} + + def _get_jobs_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]: + """Holt Job-Daten für Widgets""" + with get_db_session() as db_session: + if widget.widget_type == 'metric' and widget.widget_id == 'active_jobs': + # Anzahl aktiver Jobs + count = db_session.query(Job).filter( + Job.status.in_(['running', 'scheduled', 'paused']) + ).count() + return { + 'value': count, + 'label': 'Aktive Jobs', + 'trend': self._calculate_trend('active_jobs', count) + } + + elif widget.widget_type == 'chart' and widget.widget_id == 'jobs_timeline': + # Jobs-Timeline für die letzten 24 Stunden + since = datetime.now() - timedelta(hours=24) + jobs = db_session.query(Job).filter(Job.created_at >= since).all() + + # Gruppiere nach Stunden + hourly_data = defaultdict(int) + for job in jobs: + hour = job.created_at.replace(minute=0, second=0, microsecond=0) + hourly_data[hour] += 1 + + # Sortierte Timeline erstellen + timeline_data = [] + for i in range(24): + hour = (datetime.now() - timedelta(hours=23-i)).replace(minute=0, second=0, microsecond=0) + timeline_data.append({ + 'time': hour.isoformat(), + 'value': hourly_data.get(hour, 0) + }) + + return { + 'chart_type': 'line', + 'data': timeline_data, + 'title': 'Jobs der letzten 24 Stunden' + } + + elif widget.widget_type == 'table' and widget.widget_id == 'recent_jobs': + # Letzte 10 Jobs + jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(10).all() + + job_data = [] + for job in jobs: + job_data.append({ + 'id': job.id, + 'name': job.name, + 'user': job.user.name if job.user else 'Unbekannt', + 'printer': job.printer.name if job.printer else 'Unbekannt', + 'status': job.status, + 'created': job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '' + }) + + return { + 'headers': ['ID', 'Name', 'Benutzer', 'Drucker', 'Status', 'Erstellt'], + 'rows': job_data + } + + return {} + + def _get_printers_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]: + """Holt Drucker-Daten für Widgets""" + with get_db_session() as db_session: + if widget.widget_type == 'metric' and widget.widget_id == 'online_printers': + # Anzahl online Drucker + count = db_session.query(Printer).filter( + Printer.status.in_(['online', 'idle', 'available']) + ).count() + total = db_session.query(Printer).filter(Printer.active == True).count() + + return { + 'value': count, + 'total': total, + 'label': f'{count}/{total} Online', + 'percentage': (count / total * 100) if total > 0 else 0 + } + + elif widget.widget_type == 'chart' and widget.widget_id == 'printer_status': + # Drucker-Status-Verteilung + printers = db_session.query(Printer).filter(Printer.active == True).all() + + status_counts = defaultdict(int) + for printer in printers: + status_counts[printer.status] += 1 + + chart_data = [] + colors = { + 'online': '#10b981', + 'idle': '#3b82f6', + 'busy': '#f59e0b', + 'offline': '#ef4444', + 'maintenance': '#8b5cf6' + } + + for status, count in status_counts.items(): + chart_data.append({ + 'label': status.title(), + 'value': count, + 'color': colors.get(status, '#6b7280') + }) + + return { + 'chart_type': 'donut', + 'data': chart_data, + 'title': 'Drucker Status' + } + + return {} + + def _get_users_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]: + """Holt Benutzer-Daten für Widgets (nur für Admins)""" + with get_db_session() as db_session: + if widget.widget_type == 'chart' and widget.widget_id == 'user_activity': + # Benutzer-Aktivität der letzten 7 Tage + since = datetime.now() - timedelta(days=7) + users = db_session.query(User).filter( + User.last_activity >= since + ).all() + + # Gruppiere nach Tagen + daily_data = defaultdict(set) + for user in users: + if user.last_activity: + day = user.last_activity.date() + daily_data[day].add(user.id) + + # Chart-Daten erstellen + chart_data = [] + for i in range(7): + day = (datetime.now() - timedelta(days=6-i)).date() + chart_data.append({ + 'date': day.isoformat(), + 'active_users': len(daily_data.get(day, set())) + }) + + return { + 'chart_type': 'bar', + 'data': chart_data, + 'title': 'Aktive Benutzer (7 Tage)' + } + + return {} + + def _get_guest_requests_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]: + """Holt Gast-Anfragen-Daten für Widgets""" + with get_db_session() as db_session: + if widget.widget_type == 'metric' and widget.widget_id == 'guest_requests': + # Anzahl offener Gast-Anfragen + count = db_session.query(GuestRequest).filter( + GuestRequest.status == 'pending' + ).count() + + return { + 'value': count, + 'label': 'Offene Anfragen', + 'urgency': 'high' if count > 5 else 'normal' + } + + return {} + + def _get_alerts_widget_data(self, widget: WidgetConfig) -> Dict[str, Any]: + """Holt System-Meldungen für Widgets""" + # Sammle aktuelle System-Alerts + alerts = [] + + # Prüfe auf kritische Zustände + with get_db_session() as db_session: + # Offline Drucker + offline_printers = db_session.query(Printer).filter( + Printer.status == 'offline', + Printer.active == True + ).count() + + if offline_printers > 0: + alerts.append({ + 'type': 'warning', + 'message': f'{offline_printers} Drucker offline', + 'timestamp': datetime.now().isoformat(), + 'action': 'printers' + }) + + # Lange wartende Jobs + waiting_jobs = db_session.query(Job).filter( + Job.status == 'scheduled', + Job.created_at < datetime.now() - timedelta(hours=1) + ).count() + + if waiting_jobs > 0: + alerts.append({ + 'type': 'info', + 'message': f'{waiting_jobs} Jobs warten seit über 1 Stunde', + 'timestamp': datetime.now().isoformat(), + 'action': 'jobs' + }) + + # Gast-Anfragen + pending_requests = db_session.query(GuestRequest).filter( + GuestRequest.status == 'pending' + ).count() + + if pending_requests > 3: + alerts.append({ + 'type': 'warning', + 'message': f'{pending_requests} unbearbeitete Gast-Anfragen', + 'timestamp': datetime.now().isoformat(), + 'action': 'guest_requests' + }) + + return { + 'alerts': alerts[-10:], # Nur die neuesten 10 + 'count': len(alerts) + } + + def _calculate_trend(self, metric_key: str, current_value: int) -> str: + """Berechnet den Trend für eine Metrik""" + # Vereinfachte Trend-Berechnung + # In einer echten Implementierung würde man historische Daten verwenden + cache_key = f"trend_{metric_key}" + previous_value = self.cached_data.get(cache_key, current_value) + + if current_value > previous_value: + trend = "up" + elif current_value < previous_value: + trend = "down" + else: + trend = "stable" + + self.cached_data[cache_key] = current_value + return trend + + def _get_user_permissions(self, user_id: int) -> List[str]: + """Holt Benutzer-Berechtigungen für Dashboard-Features""" + with get_db_session() as db_session: + user = db_session.query(User).filter(User.id == user_id).first() + + if not user: + return [] + + permissions = ['view_dashboard'] + + if user.is_admin: + permissions.extend([ + 'view_all_jobs', + 'manage_users', + 'manage_printers', + 'view_system_stats', + 'view_guest_requests' + ]) + else: + permissions.extend([ + 'view_own_jobs', + 'create_jobs' + ]) + + return permissions + + def _get_system_status(self) -> Dict[str, Any]: + """Holt aktuellen System-Status""" + with get_db_session() as db_session: + # Basis-Statistiken + total_jobs = db_session.query(Job).count() + active_jobs = db_session.query(Job).filter( + Job.status.in_(['running', 'scheduled', 'paused']) + ).count() + total_printers = db_session.query(Printer).filter(Printer.active == True).count() + online_printers = db_session.query(Printer).filter( + Printer.status.in_(['online', 'idle', 'available']) + ).count() + total_users = db_session.query(User).filter(User.active == True).count() + + return { + 'jobs': { + 'total': total_jobs, + 'active': active_jobs + }, + 'printers': { + 'total': total_printers, + 'online': online_printers + }, + 'users': { + 'total': total_users + }, + 'last_updated': datetime.now().isoformat() + } + + def _cleanup_old_events(self): + """Bereinigt alte Ereignisse aus der Queue""" + cutoff_time = datetime.now() - timedelta(hours=24) + + # Entferne Ereignisse älter als 24 Stunden + while self.event_queue and self.event_queue[0].timestamp < cutoff_time: + self.event_queue.popleft() + + def emit_event(self, event: DashboardEvent): + """Sendet ein Ereignis an alle verbundenen Clients""" + if not self.socketio: + return + + # Ereignis zur Queue hinzufügen + self.event_queue.append(event) + + # Ereignis an relevante Clients senden + room = None + if event.user_id: + room = f"user_{event.user_id}" + elif event.priority in ['high', 'critical']: + room = "admin" # Kritische Ereignisse nur an Admins + + self.socketio.emit('dashboard_event', event.to_dict(), room=room) + + # Event-Handler ausführen + for handler in self.event_handlers[event.event_type]: + try: + handler(event) + except Exception as e: + logger.error(f"Fehler im Event-Handler: {str(e)}") + + def register_event_handler(self, event_type: EventType, handler: Callable): + """Registriert einen Event-Handler""" + self.event_handlers[event_type].append(handler) + + def add_custom_widget(self, widget: WidgetConfig): + """Fügt ein benutzerdefiniertes Widget hinzu""" + self.widgets[widget.widget_id] = widget + logger.info(f"Custom Widget hinzugefügt: {widget.widget_id}") + + def get_dashboard_config(self, user_id: int) -> Dict[str, Any]: + """Holt Dashboard-Konfiguration für einen Benutzer""" + user_permissions = self._get_user_permissions(user_id) + + # Filtere Widgets basierend auf Berechtigungen + available_widgets = {} + for widget_id, widget in self.widgets.items(): + if not widget.permissions or any(perm in user_permissions for perm in widget.permissions): + available_widgets[widget_id] = asdict(widget) + + return { + 'widgets': available_widgets, + 'permissions': user_permissions, + 'refresh_intervals': { + wid: widget.refresh_interval + for wid, widget in available_widgets.items() + } + } + +# Globale Dashboard-Manager-Instanz +dashboard_manager = DashboardManager() + +# Convenience-Funktionen für Events +def emit_job_event(event_type: EventType, job: Job, user_id: int = None): + """Sendet ein Job-Ereignis""" + event = DashboardEvent( + event_type=event_type, + data={ + 'job_id': job.id, + 'job_name': job.name, + 'status': job.status, + 'user_name': job.user.name if job.user else 'Unbekannt', + 'printer_name': job.printer.name if job.printer else 'Unbekannt' + }, + timestamp=datetime.now(), + user_id=user_id, + priority='normal' + ) + dashboard_manager.emit_event(event) + +def emit_printer_event(event_type: EventType, printer: Printer): + """Sendet ein Drucker-Ereignis""" + event = DashboardEvent( + event_type=event_type, + data={ + 'printer_id': printer.id, + 'printer_name': printer.name, + 'status': printer.status, + 'location': printer.location or 'Unbekannt' + }, + timestamp=datetime.now(), + priority='normal' if event_type in [EventType.PRINTER_ONLINE, EventType.PRINTER_IDLE] else 'high' + ) + dashboard_manager.emit_event(event) + +def emit_system_alert(message: str, alert_type: str = "info", priority: str = "normal"): + """Sendet eine System-Meldung""" + event_type_map = { + 'info': EventType.SYSTEM_INFO, + 'warning': EventType.SYSTEM_WARNING, + 'alert': EventType.SYSTEM_ALERT + } + + event = DashboardEvent( + event_type=event_type_map.get(alert_type, EventType.SYSTEM_INFO), + data={ + 'message': message, + 'alert_type': alert_type + }, + timestamp=datetime.now(), + priority=priority + ) + dashboard_manager.emit_event(event) + +# JavaScript für Frontend-Integration +def get_dashboard_client_js() -> str: + """Generiert JavaScript für Dashboard-Client""" + return """ + class DashboardClient { + constructor(socketUrl = '') { + this.socket = null; + this.widgets = {}; + this.eventHandlers = {}; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.reconnectDelay = 1000; + + this.connect(socketUrl); + } + + connect(socketUrl) { + if (typeof io === 'undefined') { + console.error('Socket.IO nicht verfügbar'); + return; + } + + this.socket = io(socketUrl); + + this.socket.on('connect', () => { + console.log('Dashboard verbunden'); + this.reconnectAttempts = 0; + this.onConnected(); + }); + + this.socket.on('disconnect', () => { + console.log('Dashboard getrennt'); + this.onDisconnected(); + this.attemptReconnect(); + }); + + this.socket.on('dashboard_init', (data) => { + this.handleDashboardInit(data); + }); + + this.socket.on('widget_update', (data) => { + this.handleWidgetUpdate(data); + }); + + this.socket.on('dashboard_event', (event) => { + this.handleDashboardEvent(event); + }); + + this.socket.on('data_response', (data) => { + this.handleDataResponse(data); + }); + } + + attemptReconnect() { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + setTimeout(() => { + console.log(`Reconnect-Versuch ${this.reconnectAttempts + 1}`); + this.reconnectAttempts++; + this.socket.connect(); + }, this.reconnectDelay * Math.pow(2, this.reconnectAttempts)); + } + } + + subscribeWidget(widgetId, refreshInterval = 30) { + if (this.socket) { + this.socket.emit('subscribe_widget', { + widget_id: widgetId, + refresh_interval: refreshInterval + }); + } + } + + unsubscribeWidget(widgetId) { + if (this.socket) { + this.socket.emit('unsubscribe_widget', { + widget_id: widgetId + }); + } + } + + requestData(type, filters = {}) { + if (this.socket) { + this.socket.emit('request_data', { + type: type, + filters: filters + }); + } + } + + handleDashboardInit(data) { + this.widgets = data.widgets || {}; + + // Ereignis-Handler aufrufen + this.fireEvent('dashboard_init', data); + + // Auto-Subscribe für alle Widgets + Object.keys(this.widgets).forEach(widgetId => { + this.subscribeWidget(widgetId, this.widgets[widgetId].refresh_interval); + }); + } + + handleWidgetUpdate(data) { + const widgetId = data.widget_id; + const widgetData = data.data; + + // Widget-Element suchen und aktualisieren + const widgetElement = document.querySelector(`[data-widget-id="${widgetId}"]`); + if (widgetElement) { + this.updateWidgetElement(widgetElement, widgetData); + } + + // Ereignis-Handler aufrufen + this.fireEvent('widget_update', data); + } + + handleDashboardEvent(event) { + // Event-Notification anzeigen + this.showNotification(event); + + // Ereignis-Handler aufrufen + this.fireEvent('dashboard_event', event); + } + + handleDataResponse(data) { + // Ereignis-Handler aufrufen + this.fireEvent('data_response', data); + } + + updateWidgetElement(element, data) { + const widgetType = element.dataset.widgetType; + + switch (widgetType) { + case 'metric': + this.updateMetricWidget(element, data); + break; + case 'chart': + this.updateChartWidget(element, data); + break; + case 'table': + this.updateTableWidget(element, data); + break; + case 'alert': + this.updateAlertWidget(element, data); + break; + } + } + + updateMetricWidget(element, data) { + const valueElement = element.querySelector('.metric-value'); + const labelElement = element.querySelector('.metric-label'); + const trendElement = element.querySelector('.metric-trend'); + + if (valueElement && data.value !== undefined) { + valueElement.textContent = data.value; + } + + if (labelElement && data.label) { + labelElement.textContent = data.label; + } + + if (trendElement && data.trend) { + trendElement.className = `metric-trend trend-${data.trend}`; + trendElement.innerHTML = this.getTrendIcon(data.trend); + } + } + + updateChartWidget(element, data) { + const chartContainer = element.querySelector('.chart-container'); + if (chartContainer && data.data) { + // Chart.js oder andere Chart-Library verwenden + this.renderChart(chartContainer, data); + } + } + + updateTableWidget(element, data) { + const tableBody = element.querySelector('tbody'); + if (tableBody && data.rows) { + tableBody.innerHTML = ''; + + data.rows.forEach(row => { + const tr = document.createElement('tr'); + Object.values(row).forEach(cellValue => { + const td = document.createElement('td'); + td.textContent = cellValue; + tr.appendChild(td); + }); + tableBody.appendChild(tr); + }); + } + } + + updateAlertWidget(element, data) { + const alertContainer = element.querySelector('.alerts-container'); + if (alertContainer && data.alerts) { + alertContainer.innerHTML = ''; + + data.alerts.forEach(alert => { + const alertElement = this.createAlertElement(alert); + alertContainer.appendChild(alertElement); + }); + } + } + + createAlertElement(alert) { + const div = document.createElement('div'); + div.className = `alert alert-${alert.type}`; + div.innerHTML = ` +
+ ${alert.message} + ${new Date(alert.timestamp).toLocaleTimeString()} +
+ `; + return div; + } + + showNotification(event) { + // Toast-Notification anzeigen + const notification = document.createElement('div'); + notification.className = `notification notification-${event.priority}`; + notification.innerHTML = ` +
+ ${event.event_type.replace('_', ' ').toUpperCase()} +

${this.formatEventMessage(event)}

+
+ + `; + + // Auto-dismiss nach 5 Sekunden + setTimeout(() => { + notification.remove(); + }, 5000); + + // Close-Button + notification.querySelector('.notification-close').onclick = () => { + notification.remove(); + }; + + // Notification-Container finden oder erstellen + let container = document.querySelector('.notifications-container'); + if (!container) { + container = document.createElement('div'); + container.className = 'notifications-container'; + document.body.appendChild(container); + } + + container.appendChild(notification); + } + + formatEventMessage(event) { + const data = event.data; + + switch (event.event_type) { + case 'job_created': + return `Neuer Job "${data.job_name}" von ${data.user_name}`; + case 'job_started': + return `Job "${data.job_name}" gestartet auf ${data.printer_name}`; + case 'job_finished': + return `Job "${data.job_name}" abgeschlossen`; + case 'printer_offline': + return `Drucker "${data.printer_name}" ist offline`; + case 'printer_online': + return `Drucker "${data.printer_name}" ist online`; + default: + return data.message || 'Ereignis aufgetreten'; + } + } + + getTrendIcon(trend) { + switch (trend) { + case 'up': + return '↗'; + case 'down': + return '↘'; + default: + return '→'; + } + } + + renderChart(container, data) { + // Placeholder für Chart-Rendering + // Hier würde Chart.js oder eine andere Chart-Library verwendet + container.innerHTML = `

Chart: ${data.title}

`; + } + + on(event, handler) { + if (!this.eventHandlers[event]) { + this.eventHandlers[event] = []; + } + this.eventHandlers[event].push(handler); + } + + fireEvent(event, data) { + if (this.eventHandlers[event]) { + this.eventHandlers[event].forEach(handler => { + try { + handler(data); + } catch (e) { + console.error('Fehler im Event-Handler:', e); + } + }); + } + } + + onConnected() { + // Connection-Status anzeigen + this.updateConnectionStatus('connected'); + } + + onDisconnected() { + // Connection-Status anzeigen + this.updateConnectionStatus('disconnected'); + } + + updateConnectionStatus(status) { + const statusElement = document.querySelector('.connection-status'); + if (statusElement) { + statusElement.className = `connection-status status-${status}`; + statusElement.textContent = status === 'connected' ? 'Verbunden' : 'Getrennt'; + } + } + } + + // Auto-Initialize + document.addEventListener('DOMContentLoaded', function() { + if (typeof window.dashboardClient === 'undefined') { + window.dashboardClient = new DashboardClient(); + } + }); + """ \ No newline at end of file diff --git a/backend/utils/report_generator.py b/backend/utils/report_generator.py new file mode 100644 index 00000000..c39efd1b --- /dev/null +++ b/backend/utils/report_generator.py @@ -0,0 +1,909 @@ +""" +Multi-Format-Report-Generator für das MYP-System +=============================================== + +Dieses Modul stellt umfassende Report-Generierung in verschiedenen Formaten bereit: +- PDF-Reports mit professionellem Layout +- Excel-Reports mit Diagrammen und Formatierungen +- CSV-Export für Datenanalyse +- JSON-Export für API-Integration +""" + +import os +import io +import json +import logging +from datetime import datetime, timedelta +from typing import Dict, List, Any, Optional, Union, BinaryIO +from dataclasses import dataclass, asdict +from abc import ABC, abstractmethod + +# PDF-Generation +try: + from reportlab.lib import colors + from reportlab.lib.pagesizes import A4, letter + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + from reportlab.lib.units import inch, cm + from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak + from reportlab.graphics.shapes import Drawing + from reportlab.graphics.charts.lineplots import LinePlot + from reportlab.graphics.charts.barcharts import VerticalBarChart + from reportlab.graphics.charts.piecharts import Pie + from reportlab.lib.validators import Auto + PDF_AVAILABLE = True +except ImportError: + PDF_AVAILABLE = False + +# Excel-Generation +try: + import xlsxwriter + from xlsxwriter.workbook import Workbook + from xlsxwriter.worksheet import Worksheet + EXCEL_AVAILABLE = True +except ImportError: + EXCEL_AVAILABLE = False + +import csv +from flask import make_response, jsonify + +from utils.logging_config import get_logger +from models import Job, User, Printer, Stats, GuestRequest, get_db_session + +logger = get_logger("reports") + +@dataclass +class ReportConfig: + """Konfiguration für Report-Generierung""" + title: str + subtitle: str = "" + author: str = "MYP System" + date_range: tuple = None + include_charts: bool = True + include_summary: bool = True + template: str = "standard" + logo_path: str = None + footer_text: str = "Generiert vom MYP-System" + +@dataclass +class ChartData: + """Daten für Diagramme""" + chart_type: str # 'line', 'bar', 'pie' + title: str + data: List[Dict[str, Any]] + labels: List[str] = None + colors: List[str] = None + +class BaseReportGenerator(ABC): + """Abstrakte Basis-Klasse für Report-Generatoren""" + + def __init__(self, config: ReportConfig): + self.config = config + self.data = {} + self.charts = [] + + @abstractmethod + def generate(self, output_stream: BinaryIO) -> bool: + """Generiert den Report in den angegebenen Stream""" + pass + + def add_data_section(self, name: str, data: List[Dict[str, Any]], headers: List[str] = None): + """Fügt eine Datensektion hinzu""" + self.data[name] = { + 'data': data, + 'headers': headers or (list(data[0].keys()) if data else []) + } + + def add_chart(self, chart: ChartData): + """Fügt ein Diagramm hinzu""" + self.charts.append(chart) + +class PDFReportGenerator(BaseReportGenerator): + """PDF-Report-Generator mit professionellem Layout""" + + def __init__(self, config: ReportConfig): + super().__init__(config) + if not PDF_AVAILABLE: + raise ImportError("ReportLab ist nicht installiert. Verwenden Sie: pip install reportlab") + + self.doc = None + self.story = [] + self.styles = getSampleStyleSheet() + self._setup_custom_styles() + + def _setup_custom_styles(self): + """Richtet benutzerdefinierte Styles ein""" + # Titel-Style + self.styles.add(ParagraphStyle( + name='CustomTitle', + parent=self.styles['Heading1'], + fontSize=24, + spaceAfter=30, + alignment=1, # Zentriert + textColor=colors.HexColor('#1f2937') + )) + + # Untertitel-Style + self.styles.add(ParagraphStyle( + name='CustomSubtitle', + parent=self.styles['Heading2'], + fontSize=16, + spaceAfter=20, + alignment=1, + textColor=colors.HexColor('#6b7280') + )) + + # Sektions-Header + self.styles.add(ParagraphStyle( + name='SectionHeader', + parent=self.styles['Heading2'], + fontSize=14, + spaceBefore=20, + spaceAfter=10, + textColor=colors.HexColor('#374151'), + borderWidth=1, + borderColor=colors.HexColor('#d1d5db'), + borderPadding=5 + )) + + def generate(self, output_stream: BinaryIO) -> bool: + """Generiert PDF-Report""" + try: + self.doc = SimpleDocTemplate( + output_stream, + pagesize=A4, + rightMargin=2*cm, + leftMargin=2*cm, + topMargin=2*cm, + bottomMargin=2*cm + ) + + self._build_header() + self._build_summary() + self._build_data_sections() + self._build_charts() + self._build_footer() + + self.doc.build(self.story) + return True + + except Exception as e: + logger.error(f"Fehler bei PDF-Generierung: {str(e)}") + return False + + def _build_header(self): + """Erstellt den Report-Header""" + # Logo (falls vorhanden) + if self.config.logo_path and os.path.exists(self.config.logo_path): + try: + logo = Image(self.config.logo_path, width=2*inch, height=1*inch) + self.story.append(logo) + self.story.append(Spacer(1, 0.2*inch)) + except Exception as e: + logger.warning(f"Logo konnte nicht geladen werden: {str(e)}") + + # Titel + title = Paragraph(self.config.title, self.styles['CustomTitle']) + self.story.append(title) + + # Untertitel + if self.config.subtitle: + subtitle = Paragraph(self.config.subtitle, self.styles['CustomSubtitle']) + self.story.append(subtitle) + + # Generierungsdatum + date_text = f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}" + date_para = Paragraph(date_text, self.styles['Normal']) + self.story.append(date_para) + + # Autor + author_text = f"Erstellt von: {self.config.author}" + author_para = Paragraph(author_text, self.styles['Normal']) + self.story.append(author_para) + + self.story.append(Spacer(1, 0.3*inch)) + + def _build_summary(self): + """Erstellt die Zusammenfassung""" + if not self.config.include_summary: + return + + header = Paragraph("Zusammenfassung", self.styles['SectionHeader']) + self.story.append(header) + + # Sammle Statistiken aus den Daten + total_records = sum(len(section['data']) for section in self.data.values()) + + summary_data = [ + ['Gesamtanzahl Datensätze', str(total_records)], + ['Berichtszeitraum', self._format_date_range()], + ['Anzahl Sektionen', str(len(self.data))], + ['Anzahl Diagramme', str(len(self.charts))] + ] + + summary_table = Table(summary_data, colWidths=[4*inch, 2*inch]) + summary_table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f3f4f6')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 12), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.white), + ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#d1d5db')) + ])) + + self.story.append(summary_table) + self.story.append(Spacer(1, 0.2*inch)) + + def _build_data_sections(self): + """Erstellt die Datensektionen""" + for section_name, section_data in self.data.items(): + # Sektions-Header + header = Paragraph(section_name, self.styles['SectionHeader']) + self.story.append(header) + + # Daten-Tabelle + table_data = [section_data['headers']] + table_data.extend([ + [str(row.get(header, '')) for header in section_data['headers']] + for row in section_data['data'] + ]) + + # Spaltenbreiten berechnen + col_count = len(section_data['headers']) + col_width = (self.doc.width - 2*inch) / col_count + col_widths = [col_width] * col_count + + table = Table(table_data, colWidths=col_widths, repeatRows=1) + table.setStyle(TableStyle([ + # Header-Styling + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3b82f6')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 10), + + # Daten-Styling + ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), + ('FONTSIZE', (0, 1), (-1, -1), 9), + ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9fafb')]), + + # Rahmen + ('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#d1d5db')), + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('LEFTPADDING', (0, 0), (-1, -1), 6), + ('RIGHTPADDING', (0, 0), (-1, -1), 6), + ('TOPPADDING', (0, 0), (-1, -1), 8), + ('BOTTOMPADDING', (0, 0), (-1, -1), 8), + ])) + + self.story.append(table) + self.story.append(Spacer(1, 0.2*inch)) + + # Seitenumbruch bei vielen Daten + if len(section_data['data']) > 20: + self.story.append(PageBreak()) + + def _build_charts(self): + """Erstellt die Diagramme""" + if not self.config.include_charts or not self.charts: + return + + header = Paragraph("Diagramme", self.styles['SectionHeader']) + self.story.append(header) + + for chart in self.charts: + chart_title = Paragraph(chart.title, self.styles['Heading3']) + self.story.append(chart_title) + + # Diagramm basierend auf Typ erstellen + drawing = self._create_chart_drawing(chart) + if drawing: + self.story.append(drawing) + self.story.append(Spacer(1, 0.2*inch)) + + def _create_chart_drawing(self, chart: ChartData) -> Optional[Drawing]: + """Erstellt ein Diagramm-Drawing""" + try: + drawing = Drawing(400, 300) + + if chart.chart_type == 'bar': + bar_chart = VerticalBarChart() + bar_chart.x = 50 + bar_chart.y = 50 + bar_chart.height = 200 + bar_chart.width = 300 + + # Daten vorbereiten + values = [[item.get('value', 0) for item in chart.data]] + categories = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)] + + bar_chart.data = values + bar_chart.categoryAxis.categoryNames = categories + bar_chart.valueAxis.valueMin = 0 + + # Farben setzen + if chart.colors: + bar_chart.bars[0].fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6') + + drawing.add(bar_chart) + + elif chart.chart_type == 'pie': + pie_chart = Pie() + pie_chart.x = 150 + pie_chart.y = 100 + pie_chart.width = 100 + pie_chart.height = 100 + + # Daten vorbereiten + pie_chart.data = [item.get('value', 0) for item in chart.data] + pie_chart.labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)] + + # Farben setzen + if chart.colors: + pie_chart.slices.fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6') + + drawing.add(pie_chart) + + return drawing + + except Exception as e: + logger.error(f"Fehler bei Diagramm-Erstellung: {str(e)}") + return None + + def _build_footer(self): + """Erstellt den Report-Footer""" + footer_text = self.config.footer_text + footer = Paragraph(footer_text, self.styles['Normal']) + self.story.append(Spacer(1, 0.3*inch)) + self.story.append(footer) + + def _format_date_range(self) -> str: + """Formatiert den Datumsbereich""" + if not self.config.date_range: + return "Alle verfügbaren Daten" + + start_date, end_date = self.config.date_range + return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" + +class ExcelReportGenerator(BaseReportGenerator): + """Excel-Report-Generator mit Diagrammen und Formatierungen""" + + def __init__(self, config: ReportConfig): + super().__init__(config) + if not EXCEL_AVAILABLE: + raise ImportError("XlsxWriter ist nicht installiert. Verwenden Sie: pip install xlsxwriter") + + self.workbook = None + self.formats = {} + + def generate(self, output_stream: BinaryIO) -> bool: + """Generiert Excel-Report""" + try: + self.workbook = xlsxwriter.Workbook(output_stream, {'in_memory': True}) + self._setup_formats() + + # Zusammenfassungs-Arbeitsblatt + if self.config.include_summary: + self._create_summary_worksheet() + + # Daten-Arbeitsblätter + for section_name, section_data in self.data.items(): + self._create_data_worksheet(section_name, section_data) + + # Diagramm-Arbeitsblätter + if self.config.include_charts and self.charts: + self._create_charts_worksheet() + + self.workbook.close() + return True + + except Exception as e: + logger.error(f"Fehler bei Excel-Generierung: {str(e)}") + return False + + def _setup_formats(self): + """Richtet Excel-Formate ein""" + self.formats = { + 'title': self.workbook.add_format({ + 'font_size': 18, + 'bold': True, + 'align': 'center', + 'bg_color': '#1f2937', + 'font_color': 'white', + 'border': 1 + }), + 'header': self.workbook.add_format({ + 'font_size': 12, + 'bold': True, + 'bg_color': '#3b82f6', + 'font_color': 'white', + 'align': 'center', + 'border': 1 + }), + 'data': self.workbook.add_format({ + 'align': 'center', + 'border': 1 + }), + 'data_alt': self.workbook.add_format({ + 'align': 'center', + 'bg_color': '#f9fafb', + 'border': 1 + }), + 'number': self.workbook.add_format({ + 'num_format': '#,##0', + 'align': 'right', + 'border': 1 + }), + 'currency': self.workbook.add_format({ + 'num_format': '#,##0.00 €', + 'align': 'right', + 'border': 1 + }), + 'percentage': self.workbook.add_format({ + 'num_format': '0.00%', + 'align': 'right', + 'border': 1 + }), + 'date': self.workbook.add_format({ + 'num_format': 'dd.mm.yyyy', + 'align': 'center', + 'border': 1 + }) + } + + def _create_summary_worksheet(self): + """Erstellt das Zusammenfassungs-Arbeitsblatt""" + worksheet = self.workbook.add_worksheet('Zusammenfassung') + + # Titel + worksheet.merge_range('A1:E1', self.config.title, self.formats['title']) + + # Untertitel + if self.config.subtitle: + worksheet.merge_range('A2:E2', self.config.subtitle, self.formats['header']) + + # Metadaten + row = 4 + metadata = [ + ['Generiert am:', datetime.now().strftime('%d.%m.%Y %H:%M')], + ['Erstellt von:', self.config.author], + ['Berichtszeitraum:', self._format_date_range()], + ['Anzahl Sektionen:', str(len(self.data))], + ['Anzahl Diagramme:', str(len(self.charts))] + ] + + for label, value in metadata: + worksheet.write(row, 0, label, self.formats['header']) + worksheet.write(row, 1, value, self.formats['data']) + row += 1 + + # Statistiken pro Sektion + row += 2 + worksheet.write(row, 0, 'Sektions-Übersicht:', self.formats['header']) + row += 1 + + for section_name, section_data in self.data.items(): + worksheet.write(row, 0, section_name, self.formats['data']) + worksheet.write(row, 1, len(section_data['data']), self.formats['number']) + row += 1 + + # Spaltenbreiten anpassen + worksheet.set_column('A:A', 25) + worksheet.set_column('B:B', 20) + + def _create_data_worksheet(self, section_name: str, section_data: Dict[str, Any]): + """Erstellt ein Daten-Arbeitsblatt""" + # Ungültige Zeichen für Arbeitsblatt-Namen ersetzen + safe_name = ''.join(c for c in section_name if c.isalnum() or c in ' -_')[:31] + worksheet = self.workbook.add_worksheet(safe_name) + + # Header schreiben + headers = section_data['headers'] + for col, header in enumerate(headers): + worksheet.write(0, col, header, self.formats['header']) + + # Daten schreiben + for row_idx, row_data in enumerate(section_data['data'], start=1): + for col_idx, header in enumerate(headers): + value = row_data.get(header, '') + + # Format basierend auf Datentyp wählen + cell_format = self._get_cell_format(value, row_idx) + worksheet.write(row_idx, col_idx, value, cell_format) + + # Autofilter hinzufügen + if section_data['data']: + worksheet.autofilter(0, 0, len(section_data['data']), len(headers) - 1) + + # Spaltenbreiten anpassen + for col_idx, header in enumerate(headers): + max_length = max( + len(str(header)), + max(len(str(row.get(header, ''))) for row in section_data['data']) if section_data['data'] else 0 + ) + worksheet.set_column(col_idx, col_idx, min(max_length + 2, 50)) + + def _create_charts_worksheet(self): + """Erstellt das Diagramm-Arbeitsblatt""" + worksheet = self.workbook.add_worksheet('Diagramme') + + row = 0 + for chart_idx, chart_data in enumerate(self.charts): + # Diagramm-Titel + worksheet.write(row, 0, chart_data.title, self.formats['header']) + row += 2 + + # Daten für Diagramm vorbereiten + data_worksheet_name = f'Chart_Data_{chart_idx}' + data_worksheet = self.workbook.add_worksheet(data_worksheet_name) + + # Daten ins Data-Arbeitsblatt schreiben + labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart_data.data)] + values = [item.get('value', 0) for item in chart_data.data] + + data_worksheet.write_column('A1', ['Label'] + labels) + data_worksheet.write_column('B1', ['Value'] + values) + + # Excel-Diagramm erstellen + if chart_data.chart_type == 'bar': + chart = self.workbook.add_chart({'type': 'column'}) + elif chart_data.chart_type == 'line': + chart = self.workbook.add_chart({'type': 'line'}) + elif chart_data.chart_type == 'pie': + chart = self.workbook.add_chart({'type': 'pie'}) + else: + chart = self.workbook.add_chart({'type': 'column'}) + + # Datenreihe hinzufügen + chart.add_series({ + 'name': chart_data.title, + 'categories': [data_worksheet_name, 1, 0, len(labels), 0], + 'values': [data_worksheet_name, 1, 1, len(values), 1], + }) + + chart.set_title({'name': chart_data.title}) + chart.set_x_axis({'name': 'Kategorien'}) + chart.set_y_axis({'name': 'Werte'}) + + # Diagramm ins Arbeitsblatt einfügen + worksheet.insert_chart(row, 0, chart) + row += 15 # Platz für nächstes Diagramm + + def _get_cell_format(self, value: Any, row_idx: int): + """Bestimmt das Zellformat basierend auf dem Wert""" + # Alternierende Zeilenfarben + base_format = self.formats['data'] if row_idx % 2 == 1 else self.formats['data_alt'] + + # Spezielle Formate für Zahlen, Daten, etc. + if isinstance(value, (int, float)): + return self.formats['number'] + elif isinstance(value, datetime): + return self.formats['date'] + elif isinstance(value, str) and value.endswith('%'): + return self.formats['percentage'] + elif isinstance(value, str) and '€' in value: + return self.formats['currency'] + + return base_format + + def _format_date_range(self) -> str: + """Formatiert den Datumsbereich""" + if not self.config.date_range: + return "Alle verfügbaren Daten" + + start_date, end_date = self.config.date_range + return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" + +class CSVReportGenerator(BaseReportGenerator): + """CSV-Report-Generator für Datenanalyse""" + + def generate(self, output_stream: BinaryIO) -> bool: + """Generiert CSV-Report""" + try: + # Text-Stream für CSV-Writer + text_stream = io.TextIOWrapper(output_stream, encoding='utf-8-sig', newline='') + writer = csv.writer(text_stream, delimiter=';', quoting=csv.QUOTE_MINIMAL) + + # Header mit Metadaten + writer.writerow([f'# {self.config.title}']) + writer.writerow([f'# Generiert am: {datetime.now().strftime("%d.%m.%Y %H:%M")}']) + writer.writerow([f'# Erstellt von: {self.config.author}']) + writer.writerow(['']) # Leerzeile + + # Daten-Sektionen + for section_name, section_data in self.data.items(): + writer.writerow([f'# Sektion: {section_name}']) + + # Headers + writer.writerow(section_data['headers']) + + # Daten + for row in section_data['data']: + csv_row = [str(row.get(header, '')) for header in section_data['headers']] + writer.writerow(csv_row) + + writer.writerow(['']) # Leerzeile zwischen Sektionen + + text_stream.flush() + return True + + except Exception as e: + logger.error(f"Fehler bei CSV-Generierung: {str(e)}") + return False + +class JSONReportGenerator(BaseReportGenerator): + """JSON-Report-Generator für API-Integration""" + + def generate(self, output_stream: BinaryIO) -> bool: + """Generiert JSON-Report""" + try: + report_data = { + 'metadata': { + 'title': self.config.title, + 'subtitle': self.config.subtitle, + 'author': self.config.author, + 'generated_at': datetime.now().isoformat(), + 'date_range': { + 'start': self.config.date_range[0].isoformat() if self.config.date_range else None, + 'end': self.config.date_range[1].isoformat() if self.config.date_range else None + } if self.config.date_range else None + }, + 'data': self.data, + 'charts': [asdict(chart) for chart in self.charts] if self.charts else [] + } + + json_str = json.dumps(report_data, ensure_ascii=False, indent=2, default=str) + output_stream.write(json_str.encode('utf-8')) + return True + + except Exception as e: + logger.error(f"Fehler bei JSON-Generierung: {str(e)}") + return False + +class ReportFactory: + """Factory für Report-Generatoren""" + + GENERATORS = { + 'pdf': PDFReportGenerator, + 'excel': ExcelReportGenerator, + 'xlsx': ExcelReportGenerator, + 'csv': CSVReportGenerator, + 'json': JSONReportGenerator + } + + @classmethod + def create_generator(cls, format_type: str, config: ReportConfig) -> BaseReportGenerator: + """Erstellt einen Report-Generator für das angegebene Format""" + format_type = format_type.lower() + + if format_type not in cls.GENERATORS: + raise ValueError(f"Unbekanntes Report-Format: {format_type}") + + generator_class = cls.GENERATORS[format_type] + return generator_class(config) + + @classmethod + def get_available_formats(cls) -> List[str]: + """Gibt verfügbare Report-Formate zurück""" + available = [] + + for format_type, generator_class in cls.GENERATORS.items(): + try: + # Test ob Generator funktioniert + if format_type in ['pdf'] and not PDF_AVAILABLE: + continue + elif format_type in ['excel', 'xlsx'] and not EXCEL_AVAILABLE: + continue + + available.append(format_type) + except ImportError: + continue + + return available + +# Vordefinierte Report-Templates +class JobReportBuilder: + """Builder für Job-Reports""" + + @staticmethod + def build_jobs_report( + start_date: datetime = None, + end_date: datetime = None, + user_id: int = None, + printer_id: int = None, + include_completed: bool = True, + include_cancelled: bool = False + ) -> Dict[str, Any]: + """Erstellt Job-Report-Daten""" + + with get_db_session() as db_session: + query = db_session.query(Job) + + # Filter anwenden + if start_date: + query = query.filter(Job.created_at >= start_date) + if end_date: + query = query.filter(Job.created_at <= end_date) + if user_id: + query = query.filter(Job.user_id == user_id) + if printer_id: + query = query.filter(Job.printer_id == printer_id) + + status_filters = [] + if include_completed: + status_filters.append('finished') + if include_cancelled: + status_filters.append('cancelled') + if not include_cancelled and not include_completed: + status_filters = ['scheduled', 'running', 'paused'] + + if status_filters: + query = query.filter(Job.status.in_(status_filters)) + + jobs = query.all() + + # Daten vorbereiten + job_data = [] + for job in jobs: + job_data.append({ + 'ID': job.id, + 'Name': job.name, + 'Benutzer': job.user.name if job.user else 'Unbekannt', + 'Drucker': job.printer.name if job.printer else 'Unbekannt', + 'Status': job.status, + 'Erstellt': job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '', + 'Gestartet': job.start_at.strftime('%d.%m.%Y %H:%M') if job.start_at else '', + 'Beendet': job.end_at.strftime('%d.%m.%Y %H:%M') if job.end_at else '', + 'Dauer (Min)': job.duration_minutes or 0, + 'Material (g)': job.material_used or 0, + 'Beschreibung': job.description or '' + }) + + return { + 'data': job_data, + 'headers': ['ID', 'Name', 'Benutzer', 'Drucker', 'Status', 'Erstellt', 'Gestartet', 'Beendet', 'Dauer (Min)', 'Material (g)', 'Beschreibung'] + } + +class UserReportBuilder: + """Builder für Benutzer-Reports""" + + @staticmethod + def build_users_report(include_inactive: bool = False) -> Dict[str, Any]: + """Erstellt Benutzer-Report-Daten""" + + with get_db_session() as db_session: + query = db_session.query(User) + + if not include_inactive: + query = query.filter(User.active == True) + + users = query.all() + + # Daten vorbereiten + user_data = [] + for user in users: + user_data.append({ + 'ID': user.id, + 'Name': user.name, + 'E-Mail': user.email, + 'Benutzername': user.username, + 'Rolle': user.role, + 'Aktiv': 'Ja' if user.active else 'Nein', + 'Abteilung': user.department or '', + 'Position': user.position or '', + 'Erstellt': user.created_at.strftime('%d.%m.%Y') if user.created_at else '', + 'Letzter Login': user.last_login.strftime('%d.%m.%Y %H:%M') if user.last_login else 'Nie' + }) + + return { + 'data': user_data, + 'headers': ['ID', 'Name', 'E-Mail', 'Benutzername', 'Rolle', 'Aktiv', 'Abteilung', 'Position', 'Erstellt', 'Letzter Login'] + } + +class PrinterReportBuilder: + """Builder für Drucker-Reports""" + + @staticmethod + def build_printers_report(include_inactive: bool = False) -> Dict[str, Any]: + """Erstellt Drucker-Report-Daten""" + + with get_db_session() as db_session: + query = db_session.query(Printer) + + if not include_inactive: + query = query.filter(Printer.active == True) + + printers = query.all() + + # Daten vorbereiten + printer_data = [] + for printer in printers: + printer_data.append({ + 'ID': printer.id, + 'Name': printer.name, + 'Modell': printer.model or '', + 'Standort': printer.location or '', + 'IP-Adresse': printer.ip_address or '', + 'MAC-Adresse': printer.mac_address, + 'Plug-IP': printer.plug_ip, + 'Status': printer.status, + 'Aktiv': 'Ja' if printer.active else 'Nein', + 'Erstellt': printer.created_at.strftime('%d.%m.%Y') if printer.created_at else '', + 'Letzte Prüfung': printer.last_checked.strftime('%d.%m.%Y %H:%M') if printer.last_checked else 'Nie' + }) + + return { + 'data': printer_data, + 'headers': ['ID', 'Name', 'Modell', 'Standort', 'IP-Adresse', 'MAC-Adresse', 'Plug-IP', 'Status', 'Aktiv', 'Erstellt', 'Letzte Prüfung'] + } + +def generate_comprehensive_report( + format_type: str, + start_date: datetime = None, + end_date: datetime = None, + include_jobs: bool = True, + include_users: bool = True, + include_printers: bool = True, + user_id: int = None +) -> bytes: + """Generiert einen umfassenden System-Report""" + + # Konfiguration + config = ReportConfig( + title="MYP System Report", + subtitle="Umfassende Systemübersicht", + author="MYP System", + date_range=(start_date, end_date) if start_date and end_date else None, + include_charts=True, + include_summary=True + ) + + # Generator erstellen + generator = ReportFactory.create_generator(format_type, config) + + # Daten hinzufügen + if include_jobs: + job_data = JobReportBuilder.build_jobs_report( + start_date=start_date, + end_date=end_date, + user_id=user_id + ) + generator.add_data_section("Jobs", job_data['data'], job_data['headers']) + + # Job-Status-Diagramm + status_counts = {} + for job in job_data['data']: + status = job['Status'] + status_counts[status] = status_counts.get(status, 0) + 1 + + chart_data = ChartData( + chart_type='pie', + title='Job-Status-Verteilung', + data=[{'label': status, 'value': count} for status, count in status_counts.items()] + ) + generator.add_chart(chart_data) + + if include_users: + user_data = UserReportBuilder.build_users_report() + generator.add_data_section("Benutzer", user_data['data'], user_data['headers']) + + if include_printers: + printer_data = PrinterReportBuilder.build_printers_report() + generator.add_data_section("Drucker", printer_data['data'], printer_data['headers']) + + # Report generieren + output = io.BytesIO() + success = generator.generate(output) + + if success: + output.seek(0) + return output.getvalue() + else: + raise Exception("Report-Generierung fehlgeschlagen") + +# Zusätzliche Abhängigkeiten zu requirements.txt hinzufügen +ADDITIONAL_REQUIREMENTS = [ + "reportlab>=4.0.0", + "xlsxwriter>=3.0.0" +] \ No newline at end of file