🎉 Refactor & Update Backend Code, Add Utils 🖥️📊
This commit is contained in:
parent
d4f899d280
commit
193164964e
459
backend/app.py
459
backend/app.py
@ -5439,237 +5439,9 @@ def export_guest_requests():
|
|||||||
}), 500
|
}), 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 =====
|
# ===== AUTO-OPTIMIERUNG-API-ENDPUNKTE =====
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/optimization/auto-optimize', methods=['POST'])
|
@app.route('/api/optimization/auto-optimize', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def auto_optimize_jobs():
|
def auto_optimize_jobs():
|
||||||
@ -5937,3 +5709,232 @@ def validate_optimization_settings(settings):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# ===== GASTANTRÄGE API-ROUTEN =====
|
# ===== 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)
|
@ -179,3 +179,438 @@ def control_printer_power(printer_id):
|
|||||||
"success": False,
|
"success": False,
|
||||||
"error": f"Allgemeiner Fehler: {str(e)}"
|
"error": f"Allgemeiner Fehler: {str(e)}"
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@printers_blueprint.route("/test/socket/<int:printer_id>", 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/<int:printer_id>/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)}"
|
||||||
|
}), 500
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 - [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 - [TIME] Startzeit: 31.05.2025 23:44:39
|
||||||
2025-05-31 23:44:39 - myp.app - INFO - ==================================================
|
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 - [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 - [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 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] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 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] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:25 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:25] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/css/optimization-animations.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/optimization-features.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:28 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:28] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:29 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:29] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/css/optimization-animations.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/optimization-features.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:32 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:32] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 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] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/optimization-animations.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/optimization-features.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/optimization-animations.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/main.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/optimization-features.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/core.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/timegrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/daygrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/interaction.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/fullcalendar/list.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:34] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[33mGET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:45:35 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:35] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[33mPOST /api/optimization/auto-optimize HTTP/1.1[0m" 404 -
|
||||||
|
2025-05-31 23:45:41 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:45:41] "[33mPOST /api/optimization/auto-optimize HTTP/1.1[0m" 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 - [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 - [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mPOST /api/optimization/auto-optimize HTTP/1.1[0m" 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] "[33mPOST /api/optimization/auto-optimize HTTP/1.1[0m" 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] "[33mGET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1[0m" 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] "[36mGET /static/js/fullcalendar/main.min.css HTTP/1.1[0m" 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] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 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] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/fullcalendar/daygrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/fullcalendar/core.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/fullcalendar/timegrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/fullcalendar/interaction.min.js HTTP/1.1[0m" 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] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/fullcalendar/list.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[33mGET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1[0m" 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] "[36mGET /static/favicon.svg HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:49:21 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:49:21] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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] "[33mPOST /api/optimization/auto-optimize HTTP/1.1[0m" 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 - [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* 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 - [33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1[0m" 404 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/css/professional-theme.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/offline-app.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/css/tailwind.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/css/components.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/optimization-features.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/core.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/daygrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/ui-components.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/css/optimization-animations.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/main.min.css HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/timegrid.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/list.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/fullcalendar/interaction.min.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/debug-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/job-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/dark-mode-fix.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/global-refresh-functions.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/event-handlers.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/csp-violation-handler.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/printer_monitor.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/notifications.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/session-manager.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/js/auto-logout.js HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[33mGET /api/calendar/events?start=2025-05-25T00:00:00%2B02:00&end=2025-06-01T00:00:00%2B02:00 HTTP/1.1[0m" 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] "[36mGET /static/manifest.json HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/favicon.svg HTTP/1.1[0m" 304 -
|
||||||
|
2025-05-31 23:50:34 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:50:34] "[36mGET /static/icons/icon-144x144.png HTTP/1.1[0m" 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
|
||||||
|
@ -2514,3 +2514,10 @@
|
|||||||
2025-05-31 23:35:03 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
|
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: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: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)
|
||||||
|
@ -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-Thread gestartet
|
||||||
2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler 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: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
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse-scale {
|
.animate-pulse-scale {
|
||||||
animation: pulse-scale 2s infinite ease-in-out;
|
animation: pulse-scale 3s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== FLOATING ANIMATIONS ===== */
|
/* ===== FLOATING ANIMATIONS ===== */
|
||||||
@ -82,12 +82,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 3s infinite ease-in-out;
|
animation: float 4s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-float-delay {
|
.animate-float-delay {
|
||||||
animation: float-delay 3s infinite ease-in-out;
|
animation: float-delay 4s infinite ease-in-out;
|
||||||
animation-delay: 1s;
|
animation-delay: 1.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== SLIDE-UP ANIMATIONS ===== */
|
/* ===== SLIDE-UP ANIMATIONS ===== */
|
||||||
@ -161,7 +161,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-glow {
|
.animate-glow {
|
||||||
animation: glow 2s infinite ease-in-out;
|
animation: glow 3s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== KONFETTI ANIMATION ===== */
|
/* ===== KONFETTI ANIMATION ===== */
|
||||||
@ -190,7 +190,7 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(100vh) rotate(720deg);
|
transform: translateY(120vh) rotate(720deg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,14 +366,14 @@ class OptimizationManager {
|
|||||||
// Sound-Effekt (optional)
|
// Sound-Effekt (optional)
|
||||||
this.playSuccessSound();
|
this.playSuccessSound();
|
||||||
|
|
||||||
// Auto-Close nach 10 Sekunden
|
// Auto-Close nach 20 Sekunden (verlängert für bessere Animation-Wirkung)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (modal && modal.parentNode) {
|
if (modal && modal.parentNode) {
|
||||||
modal.style.opacity = '0';
|
modal.style.opacity = '0';
|
||||||
modal.style.transform = 'scale(0.95)';
|
modal.style.transform = 'scale(0.95)';
|
||||||
setTimeout(() => modal.remove(), 300);
|
setTimeout(() => modal.remove(), 300);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -383,10 +383,10 @@ class OptimizationManager {
|
|||||||
const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
|
const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
|
||||||
let confetti = '';
|
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 color = colors[Math.floor(Math.random() * colors.length)];
|
||||||
const delay = Math.random() * 3;
|
const delay = Math.random() * 5;
|
||||||
const duration = 3 + Math.random() * 2;
|
const duration = 4 + Math.random() * 3;
|
||||||
const left = Math.random() * 100;
|
const left = Math.random() * 100;
|
||||||
|
|
||||||
confetti += `
|
confetti += `
|
||||||
|
936
backend/utils/advanced_tables.py
Normal file
936
backend/utils/advanced_tables.py
Normal file
@ -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 = `
|
||||||
|
<div class="table-controls-left">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" id="${this.tableId}-search" placeholder="Suchen..." class="search-input">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
</div>
|
||||||
|
<div class="page-size-selector">
|
||||||
|
<label>Einträge pro Seite:</label>
|
||||||
|
<select id="${this.tableId}-page-size">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25" selected>25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-controls-right">
|
||||||
|
<button class="btn-filter" id="${this.tableId}-filter-btn">Filter</button>
|
||||||
|
<button class="btn-export" id="${this.tableId}-export-btn">Export</button>
|
||||||
|
<button class="btn-refresh" id="${this.tableId}-refresh-btn">↻</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 ? `<span class="action-icon">${action.icon}</span>` : '';
|
||||||
|
|
||||||
|
return `<a href="${action.url}" class="action-btn ${action.class}" ${confirmAttr}>
|
||||||
|
${icon}${action.label}
|
||||||
|
</a>`;
|
||||||
|
}).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 = `
|
||||||
|
<div class="pagination-info">
|
||||||
|
Zeige ${pagination.start_item}-${pagination.end_item} von ${pagination.total_items} Einträgen
|
||||||
|
</div>
|
||||||
|
<div class="pagination-controls">
|
||||||
|
${this.renderPaginationButtons()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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(`
|
||||||
|
<button class="page-btn ${this.currentPage === 1 ? 'disabled' : ''}"
|
||||||
|
data-page="${this.currentPage - 1}" ${this.currentPage === 1 ? 'disabled' : ''}>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 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(`
|
||||||
|
<button class="page-btn ${i === this.currentPage ? 'active' : ''}"
|
||||||
|
data-page="${i}">
|
||||||
|
${i}
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
buttons.push(`
|
||||||
|
<button class="page-btn ${this.currentPage === this.totalPages ? 'disabled' : ''}"
|
||||||
|
data-page="${this.currentPage + 1}" ${this.currentPage === this.totalPages ? 'disabled' : ''}>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
1231
backend/utils/drag_drop_system.py
Normal file
1231
backend/utils/drag_drop_system.py
Normal file
File diff suppressed because it is too large
Load Diff
1137
backend/utils/realtime_dashboard.py
Normal file
1137
backend/utils/realtime_dashboard.py
Normal file
File diff suppressed because it is too large
Load Diff
909
backend/utils/report_generator.py
Normal file
909
backend/utils/report_generator.py
Normal file
@ -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"
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user