🎉 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
|
||||
|
||||
|
||||
# ===== STARTUP UND MAIN =====
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
|
||||
# Debug-Modus prüfen
|
||||
debug_mode = len(sys.argv) > 1 and sys.argv[1] == "--debug"
|
||||
|
||||
# Windows-spezifische Umgebungsvariablen setzen für bessere Flask-Kompatibilität
|
||||
if os.name == 'nt' and debug_mode:
|
||||
# Entferne problematische Werkzeug-Variablen
|
||||
os.environ.pop('WERKZEUG_SERVER_FD', None)
|
||||
os.environ.pop('WERKZEUG_RUN_MAIN', None)
|
||||
|
||||
# Setze saubere Umgebung
|
||||
os.environ['FLASK_ENV'] = 'development'
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
# Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown
|
||||
def signal_handler(sig, frame):
|
||||
"""Signal-Handler für ordnungsgemäßes Shutdown."""
|
||||
app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...")
|
||||
try:
|
||||
# Queue Manager stoppen
|
||||
app_logger.info("🔄 Beende Queue Manager...")
|
||||
stop_queue_manager()
|
||||
|
||||
# Scheduler stoppen falls aktiviert
|
||||
if SCHEDULER_ENABLED and scheduler:
|
||||
try:
|
||||
scheduler.stop()
|
||||
app_logger.info("Job-Scheduler gestoppt")
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}")
|
||||
|
||||
# ===== DATENBANKVERBINDUNGEN ORDNUNGSGEMÄSS SCHLIESSEN =====
|
||||
app_logger.info("💾 Führe Datenbank-Cleanup durch...")
|
||||
try:
|
||||
from models import get_db_session, create_optimized_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
# WAL-Checkpoint ausführen um .shm und .wal Dateien zu bereinigen
|
||||
engine = create_optimized_engine()
|
||||
|
||||
with engine.connect() as conn:
|
||||
# Vollständiger WAL-Checkpoint (TRUNCATE-Modus)
|
||||
app_logger.info("📝 Führe WAL-Checkpoint durch...")
|
||||
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||||
|
||||
if result:
|
||||
app_logger.info(f"WAL-Checkpoint abgeschlossen: {result[1]} Seiten übertragen, {result[2]} Seiten zurückgesetzt")
|
||||
|
||||
# Alle pending Transaktionen committen
|
||||
conn.commit()
|
||||
|
||||
# Journal-Mode zu DELETE wechseln (entfernt .wal/.shm Dateien)
|
||||
app_logger.info("📁 Schalte Journal-Mode um...")
|
||||
conn.execute(text("PRAGMA journal_mode=DELETE"))
|
||||
|
||||
# Optimize und Vacuum für sauberen Zustand
|
||||
conn.execute(text("PRAGMA optimize"))
|
||||
conn.execute(text("VACUUM"))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Engine-Connection-Pool schließen
|
||||
engine.dispose()
|
||||
|
||||
app_logger.info("✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein")
|
||||
|
||||
except Exception as db_error:
|
||||
app_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {str(db_error)}")
|
||||
|
||||
app_logger.info("✅ Shutdown abgeschlossen")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# Signal-Handler registrieren (Windows-kompatibel)
|
||||
if os.name == 'nt': # Windows
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
# Zusätzlich für Flask-Development-Server
|
||||
signal.signal(signal.SIGBREAK, signal_handler)
|
||||
else: # Unix/Linux
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
try:
|
||||
# Datenbank initialisieren
|
||||
init_database()
|
||||
create_initial_admin()
|
||||
|
||||
# Template-Hilfsfunktionen registrieren
|
||||
register_template_helpers(app)
|
||||
|
||||
# Drucker-Monitor Steckdosen-Initialisierung beim Start
|
||||
try:
|
||||
app_logger.info("🖨️ Starte automatische Steckdosen-Initialisierung...")
|
||||
initialization_results = printer_monitor.initialize_all_outlets_on_startup()
|
||||
|
||||
if initialization_results:
|
||||
success_count = sum(1 for success in initialization_results.values() if success)
|
||||
total_count = len(initialization_results)
|
||||
app_logger.info(f"✅ Steckdosen-Initialisierung: {success_count}/{total_count} Drucker erfolgreich")
|
||||
|
||||
if success_count < total_count:
|
||||
app_logger.warning(f"⚠️ {total_count - success_count} Drucker konnten nicht initialisiert werden")
|
||||
else:
|
||||
app_logger.info("ℹ️ Keine Drucker zur Initialisierung gefunden")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}")
|
||||
|
||||
# Queue-Manager für automatische Drucker-Überwachung starten
|
||||
# Nur im Produktionsmodus starten (nicht im Debug-Modus)
|
||||
if not debug_mode:
|
||||
try:
|
||||
queue_manager = start_queue_manager()
|
||||
app_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
|
||||
|
||||
# Verbesserte Shutdown-Handler registrieren
|
||||
def cleanup_queue_manager():
|
||||
try:
|
||||
app_logger.info("🔄 Beende Queue Manager...")
|
||||
stop_queue_manager()
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}")
|
||||
|
||||
atexit.register(cleanup_queue_manager)
|
||||
|
||||
# ===== DATENBANK-CLEANUP BEIM PROGRAMMENDE =====
|
||||
def cleanup_database():
|
||||
"""Führt Datenbank-Cleanup beim normalen Programmende aus."""
|
||||
try:
|
||||
app_logger.info("💾 Führe finales Datenbank-Cleanup durch...")
|
||||
from models import create_optimized_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
engine = create_optimized_engine()
|
||||
|
||||
with engine.connect() as conn:
|
||||
# WAL-Checkpoint für sauberes Beenden
|
||||
result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone()
|
||||
if result and result[1] > 0:
|
||||
app_logger.info(f"Final WAL-Checkpoint: {result[1]} Seiten übertragen")
|
||||
|
||||
# Journal-Mode umschalten um .wal/.shm Dateien zu entfernen
|
||||
conn.execute(text("PRAGMA journal_mode=DELETE"))
|
||||
conn.commit()
|
||||
|
||||
# Connection-Pool ordnungsgemäß schließen
|
||||
engine.dispose()
|
||||
app_logger.info("✅ Finales Datenbank-Cleanup abgeschlossen")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim finalen Datenbank-Cleanup: {str(e)}")
|
||||
|
||||
atexit.register(cleanup_database)
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}")
|
||||
else:
|
||||
app_logger.info("🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung")
|
||||
|
||||
# Scheduler starten (falls aktiviert)
|
||||
if SCHEDULER_ENABLED:
|
||||
try:
|
||||
scheduler.start()
|
||||
app_logger.info("Job-Scheduler gestartet")
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}")
|
||||
|
||||
if debug_mode:
|
||||
# Debug-Modus: HTTP auf Port 5000
|
||||
app_logger.info("Starte Debug-Server auf 0.0.0.0:5000 (HTTP)")
|
||||
|
||||
# Windows-spezifische Flask-Konfiguration
|
||||
run_kwargs = {
|
||||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"debug": True,
|
||||
"threaded": True
|
||||
}
|
||||
|
||||
if os.name == 'nt':
|
||||
# Windows: Deaktiviere Auto-Reload um WERKZEUG_SERVER_FD Fehler zu vermeiden
|
||||
run_kwargs["use_reloader"] = False
|
||||
run_kwargs["passthrough_errors"] = False
|
||||
app_logger.info("Windows-Debug-Modus: Auto-Reload deaktiviert")
|
||||
|
||||
app.run(**run_kwargs)
|
||||
else:
|
||||
# Produktions-Modus: HTTPS auf Port 443
|
||||
ssl_context = get_ssl_context()
|
||||
|
||||
if ssl_context:
|
||||
app_logger.info("Starte HTTPS-Server auf 0.0.0.0:443")
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=443,
|
||||
debug=False,
|
||||
ssl_context=ssl_context,
|
||||
threaded=True
|
||||
)
|
||||
else:
|
||||
app_logger.info("Starte HTTP-Server auf 0.0.0.0:80")
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=80,
|
||||
debug=False,
|
||||
threaded=True
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...")
|
||||
signal_handler(signal.SIGINT, None)
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}")
|
||||
# Cleanup bei Fehler
|
||||
try:
|
||||
stop_queue_manager()
|
||||
except:
|
||||
pass
|
||||
sys.exit(1)
|
||||
|
||||
# ===== AUTO-OPTIMIERUNG-API-ENDPUNKTE =====
|
||||
|
||||
|
||||
@app.route('/api/optimization/auto-optimize', methods=['POST'])
|
||||
@login_required
|
||||
def auto_optimize_jobs():
|
||||
@ -5937,3 +5709,232 @@ def validate_optimization_settings(settings):
|
||||
return False
|
||||
|
||||
# ===== 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,
|
||||
"error": f"Allgemeiner Fehler: {str(e)}"
|
||||
}), 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 - [TIME] Startzeit: 31.05.2025 23:44:39
|
||||
2025-05-31 23:44:39 - myp.app - INFO - ==================================================
|
||||
2025-05-31 23:44:45 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:44:45] "GET /api/notifications HTTP/1.1" 200 -
|
||||
2025-05-31 23:44:46 - werkzeug - INFO - 127.0.0.1 - - [31/May/2025 23:44:46] "GET /api/session/status HTTP/1.1" 200 -
|
||||
2025-05-31 23:45:02 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter...
|
||||
2025-05-31 23:45:02 - myp.app - INFO - 🔄 Beende Queue Manager...
|
||||
2025-05-31 23:45:02 - myp.app - INFO - Job-Scheduler gestoppt
|
||||
2025-05-31 23:45:02 - myp.app - INFO - 💾 Führe Datenbank-Cleanup durch...
|
||||
2025-05-31 23:45:02 - myp.app - INFO - 📝 Führe WAL-Checkpoint durch...
|
||||
2025-05-31 23:45:02 - myp.app - INFO - WAL-Checkpoint abgeschlossen: 0 Seiten übertragen, 0 Seiten zurückgesetzt
|
||||
2025-05-31 23:45:02 - myp.app - INFO - 📁 Schalte Journal-Mode um...
|
||||
2025-05-31 23:45:02 - myp.app - INFO - ✅ Datenbank-Cleanup abgeschlossen - WAL-Dateien sollten verschwunden sein
|
||||
2025-05-31 23:45:02 - myp.app - INFO - ✅ Shutdown abgeschlossen
|
||||
2025-05-31 23:45:22 - myp.windows_fixes - INFO - 🔧 Wende Windows-spezifische Fixes an...
|
||||
2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen)
|
||||
2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Globaler subprocess-Patch angewendet
|
||||
2025-05-31 23:45:22 - myp.windows_fixes - INFO - ✅ Alle Windows-Fixes erfolgreich angewendet
|
||||
2025-05-31 23:45:22 - myp.app - INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\database\myp.db
|
||||
2025-05-31 23:45:22 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert
|
||||
2025-05-31 23:45:22 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet
|
||||
2025-05-31 23:45:22 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet
|
||||
2025-05-31 23:45:22 - myp.analytics - INFO - 📈 Analytics Engine initialisiert
|
||||
2025-05-31 23:45:22 - myp.security - INFO - 🔒 Security System initialisiert
|
||||
2025-05-31 23:45:22 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert
|
||||
2025-05-31 23:45:22 - myp.app - INFO - ==================================================
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [START] MYP (Manage Your Printers) wird gestartet...
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [FOLDER] Log-Verzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\logs
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [CHART] Log-Level: INFO
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [PC] Betriebssystem: Windows 11
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [WEB] Hostname: C040L0079726760
|
||||
2025-05-31 23:45:22 - myp.app - INFO - [TIME] Startzeit: 31.05.2025 23:45:22
|
||||
2025-05-31 23:45:22 - myp.app - INFO - ==================================================
|
||||
2025-05-31 23:45:22 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen)
|
||||
2025-05-31 23:45:22 - myp.app - INFO - Datenbank mit Optimierungen initialisiert
|
||||
2025-05-31 23:45:23 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt.
|
||||
2025-05-31 23:45:23 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung...
|
||||
2025-05-31 23:45:23 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart...
|
||||
2025-05-31 23:45:23 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden
|
||||
2025-05-31 23:45:23 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden
|
||||
2025-05-31 23:45:23 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung
|
||||
2025-05-31 23:45:23 - myp.app - INFO - Job-Scheduler gestartet
|
||||
2025-05-31 23:45:23 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP)
|
||||
2025-05-31 23:45:23 - myp.app - INFO - Windows-Debug-Modus: Auto-Reload deaktiviert
|
||||
2025-05-31 23:45:23 - werkzeug - INFO - [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:04 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
|
||||
2025-05-31 23:35:14 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
|
||||
2025-05-31 23:45:25 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1)
|
||||
2025-05-31 23:45:25 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
|
||||
2025-05-31 23:45:28 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
|
||||
2025-05-31 23:45:28 - myp.printers - INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1)
|
||||
2025-05-31 23:45:28 - myp.printers - INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker
|
||||
2025-05-31 23:45:29 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
|
||||
2025-05-31 23:45:32 - myp.printers - INFO - Schnelles Laden abgeschlossen: 6 Drucker geladen (ohne Status-Check)
|
||||
|
@ -2687,3 +2687,21 @@
|
||||
2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler-Thread gestartet
|
||||
2025-05-31 23:34:22 - myp.scheduler - INFO - Scheduler gestartet
|
||||
2025-05-31 23:44:38 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
2025-05-31 23:45:02 - myp.scheduler - INFO - Scheduler-Thread beendet
|
||||
2025-05-31 23:45:02 - myp.scheduler - INFO - Scheduler gestoppt
|
||||
2025-05-31 23:45:22 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
2025-05-31 23:45:23 - myp.scheduler - INFO - Scheduler-Thread gestartet
|
||||
2025-05-31 23:45:23 - myp.scheduler - INFO - Scheduler gestartet
|
||||
2025-05-31 23:46:05 - myp.scheduler - INFO - Scheduler-Thread beendet
|
||||
2025-05-31 23:46:05 - myp.scheduler - INFO - Scheduler gestoppt
|
||||
2025-05-31 23:46:12 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
2025-05-31 23:46:12 - myp.scheduler - INFO - Scheduler-Thread gestartet
|
||||
2025-05-31 23:46:12 - myp.scheduler - INFO - Scheduler gestartet
|
||||
2025-05-31 23:49:27 - myp.scheduler - INFO - Scheduler-Thread beendet
|
||||
2025-05-31 23:49:27 - myp.scheduler - INFO - Scheduler gestoppt
|
||||
2025-05-31 23:49:29 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
2025-05-31 23:50:29 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True
|
||||
2025-05-31 23:50:30 - myp.scheduler - INFO - Scheduler-Thread gestartet
|
||||
2025-05-31 23:50:30 - myp.scheduler - INFO - Scheduler gestartet
|
||||
2025-05-31 23:51:08 - myp.scheduler - INFO - Scheduler-Thread beendet
|
||||
2025-05-31 23:51:08 - myp.scheduler - INFO - Scheduler gestoppt
|
||||
|
@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
.animate-pulse-scale {
|
||||
animation: pulse-scale 2s infinite ease-in-out;
|
||||
animation: pulse-scale 3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* ===== FLOATING ANIMATIONS ===== */
|
||||
@ -82,12 +82,12 @@
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 3s infinite ease-in-out;
|
||||
animation: float 4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.animate-float-delay {
|
||||
animation: float-delay 3s infinite ease-in-out;
|
||||
animation-delay: 1s;
|
||||
animation: float-delay 4s infinite ease-in-out;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
/* ===== SLIDE-UP ANIMATIONS ===== */
|
||||
@ -161,7 +161,7 @@
|
||||
}
|
||||
|
||||
.animate-glow {
|
||||
animation: glow 2s infinite ease-in-out;
|
||||
animation: glow 3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* ===== KONFETTI ANIMATION ===== */
|
||||
@ -190,7 +190,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
transform: translateY(120vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
@ -366,14 +366,14 @@ class OptimizationManager {
|
||||
// Sound-Effekt (optional)
|
||||
this.playSuccessSound();
|
||||
|
||||
// Auto-Close nach 10 Sekunden
|
||||
// Auto-Close nach 20 Sekunden (verlängert für bessere Animation-Wirkung)
|
||||
setTimeout(() => {
|
||||
if (modal && modal.parentNode) {
|
||||
modal.style.opacity = '0';
|
||||
modal.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => modal.remove(), 300);
|
||||
}
|
||||
}, 10000);
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -383,10 +383,10 @@ class OptimizationManager {
|
||||
const colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
|
||||
let confetti = '';
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
const delay = Math.random() * 3;
|
||||
const duration = 3 + Math.random() * 2;
|
||||
const delay = Math.random() * 5;
|
||||
const duration = 4 + Math.random() * 3;
|
||||
const left = Math.random() * 100;
|
||||
|
||||
confetti += `
|
||||
|
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