diff --git a/backend/app/app.py b/backend/app/app.py index 3fcfb9c1..83ac025c 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -45,6 +45,7 @@ from utils.file_manager import file_manager, save_job_file, save_guest_file, sav from blueprints.guest import guest_blueprint from blueprints.calendar import calendar_blueprint from blueprints.users import users_blueprint +from blueprints.printers import printers_blueprint # Scheduler importieren falls verfügbar try: @@ -129,6 +130,7 @@ def csrf_error(error): app.register_blueprint(guest_blueprint) app.register_blueprint(calendar_blueprint) app.register_blueprint(users_blueprint) +app.register_blueprint(printers_blueprint) # Login-Manager initialisieren login_manager = LoginManager() diff --git a/backend/app/blueprints/printers.py b/backend/app/blueprints/printers.py new file mode 100644 index 00000000..f273acc5 --- /dev/null +++ b/backend/app/blueprints/printers.py @@ -0,0 +1,181 @@ +""" +Drucker-Blueprint für MYP Platform +Enthält alle Routen und Funktionen zur Druckerverwaltung, Statusüberwachung und Steuerung. +""" + +import os +import json +import time +from datetime import datetime, timedelta +from flask import Blueprint, request, jsonify, current_app, abort, Response +from flask_login import login_required, current_user +from werkzeug.utils import secure_filename +from werkzeug.exceptions import NotFound, BadRequest +from sqlalchemy import func, desc, asc +from sqlalchemy.exc import SQLAlchemyError +from typing import Dict, List, Tuple, Any, Optional + +from models import Printer, User, Job, get_db_session +from utils.logging_config import get_logger, measure_execution_time +from utils.permissions import require_permission, Permission, check_permission +from utils.printer_monitor import printer_monitor + +# Logger initialisieren +printers_logger = get_logger("printers") + +# Blueprint erstellen +printers_blueprint = Blueprint("printers", __name__, url_prefix="/api/printers") + +@printers_blueprint.route("/monitor/live-status", methods=["GET"]) +@login_required +@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status-Abfrage") +def get_live_printer_status(): + """ + Liefert den aktuellen Live-Status aller Drucker. + + Query-Parameter: + - use_cache: ob Cache verwendet werden soll (default: true) + + Returns: + JSON mit Live-Status aller Drucker + """ + printers_logger.info(f"🔄 Live-Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})") + + # Parameter auslesen + use_cache_param = request.args.get("use_cache", "true").lower() + use_cache = use_cache_param == "true" + + try: + # Live-Status über den PrinterMonitor abrufen + status_data = printer_monitor.get_live_printer_status(use_session_cache=use_cache) + + # Zusammenfassung der Druckerstatus erstellen + summary = printer_monitor.get_printer_summary() + + # Antwort mit Status und Zusammenfassung + response = { + "success": True, + "status": status_data, + "summary": summary, + "timestamp": datetime.now().isoformat(), + "cache_used": use_cache + } + + printers_logger.info(f"✅ Live-Status-Abfrage erfolgreich: {len(status_data)} Drucker") + return jsonify(response) + + except Exception as e: + printers_logger.error(f"❌ Fehler bei Live-Status-Abfrage: {str(e)}") + return jsonify({ + "success": False, + "error": "Fehler bei Abfrage des Druckerstatus", + "message": str(e) + }), 500 + +@printers_blueprint.route("/control//power", methods=["POST"]) +@login_required +@require_permission(Permission.MANAGE_PRINTERS) +@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Stromversorgung-Steuerung") +def control_printer_power(printer_id): + """ + Steuert die Stromversorgung eines Druckers (ein-/ausschalten). + + Args: + printer_id: ID des zu steuernden Druckers + + JSON-Parameter: + - action: "on" oder "off" + + Returns: + JSON mit Ergebnis der Steuerungsaktion + """ + printers_logger.info(f"🔌 Stromsteuerung für Drucker {printer_id} von Benutzer {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 + + 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 + + # 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 + + # Steckdose ein- oder ausschalten + if action == "on": + p110.turnOn() + success = True + message = "Steckdose erfolgreich eingeschaltet" + printer.status = "starting" # Status aktualisieren + else: + p110.turnOff() + success = True + message = "Steckdose erfolgreich ausgeschaltet" + printer.status = "offline" # Status aktualisieren + + # Zeitpunkt der letzten Prüfung aktualisieren + printer.last_checked = datetime.now() + db_session.commit() + + # Cache leeren, damit neue Status-Abfragen aktuell sind + printer_monitor.clear_all_caches() + + printers_logger.info(f"✅ {action.upper()}: Drucker {printer.name} erfolgreich {message}") + + except Exception as e: + printers_logger.error(f"❌ Fehler bei 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, + "printer_id": printer_id, + "printer_name": printer.name, + "action": action, + "timestamp": datetime.now().isoformat() + }) + + except Exception as e: + printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}") + return jsonify({ + "success": False, + "error": f"Allgemeiner Fehler: {str(e)}" + }), 500 \ No newline at end of file diff --git a/backend/app/config/settings.py b/backend/app/config/settings.py index 54b9a05e..1e503c42 100644 --- a/backend/app/config/settings.py +++ b/backend/app/config/settings.py @@ -28,6 +28,9 @@ DATABASE_PATH = os.path.join(BASE_DIR, "database", "myp.db") TAPO_USERNAME = "till.tomczak@mercedes-benz.com" TAPO_PASSWORD = "744563017196" +# Automatische Steckdosen-Erkennung aktivieren +TAPO_AUTO_DISCOVERY = True + # Standard-Steckdosen-IPs (diese können später in der Datenbank überschrieben werden) DEFAULT_TAPO_IPS = [ "192.168.1.100", diff --git a/backend/app/static/js/debug-fix.js b/backend/app/static/js/debug-fix.js index 37a663ed..9c846604 100644 --- a/backend/app/static/js/debug-fix.js +++ b/backend/app/static/js/debug-fix.js @@ -104,13 +104,17 @@ // Error Handler für unbehandelte Fehler window.addEventListener('error', function(e) { - console.error('🐛 JavaScript Error abgefangen:', { - message: e.message, - filename: e.filename, - lineno: e.lineno, - colno: e.colno, - error: e.error - }); + // Bessere Fehler-Serialisierung + const errorInfo = { + message: e.message || 'Unbekannter Fehler', + filename: e.filename || 'Unbekannte Datei', + lineno: e.lineno || 0, + colno: e.colno || 0, + stack: e.error ? e.error.stack : 'Stack nicht verfügbar', + type: e.error ? e.error.constructor.name : 'Unbekannter Typ' + }; + + console.error('🐛 JavaScript Error abgefangen:', errorInfo); // Spezifische Fehlerbehebungen if (e.message.includes('MVP.UI.DarkModeManager is not a constructor')) { @@ -125,6 +129,19 @@ return false; } + if (e.message.includes('refreshStats is not defined')) { + console.log('🔧 refreshStats Fehler erkannt - lade global-refresh-functions.js'); + // Versuche, global-refresh-functions.js zu laden + const script = document.createElement('script'); + script.src = '/static/js/global-refresh-functions.js'; + script.onload = function() { + console.log('✅ global-refresh-functions.js nachgeladen'); + }; + document.head.appendChild(script); + e.preventDefault(); + return false; + } + if (e.message.includes('Cannot read properties of undefined')) { console.log('🔧 Undefined Properties Fehler erkannt - ignoriert für Stabilität'); e.preventDefault(); diff --git a/backend/app/static/js/global-refresh-functions.js b/backend/app/static/js/global-refresh-functions.js index a2aec288..44eee135 100644 --- a/backend/app/static/js/global-refresh-functions.js +++ b/backend/app/static/js/global-refresh-functions.js @@ -58,6 +58,62 @@ window.refreshDashboard = async function() { } }; +/** + * Statistiken-Refresh-Funktion + */ +window.refreshStats = async function() { + const refreshButton = document.querySelector('button[onclick="refreshStats()"]'); + if (refreshButton) { + refreshButton.disabled = true; + const icon = refreshButton.querySelector('svg'); + if (icon) { + icon.classList.add('animate-spin'); + } + } + + try { + // Basis-Statistiken laden + if (typeof loadBasicStats === 'function') { + await loadBasicStats(); + } else { + // Fallback: API-Aufruf für Statistiken + const response = await fetch('/api/stats'); + const data = await response.json(); + + if (response.ok) { + // Statistiken im DOM aktualisieren + updateStatsCounter('total-jobs-count', data.total_jobs); + updateStatsCounter('completed-jobs-count', data.completed_jobs); + updateStatsCounter('online-printers-count', data.online_printers); + updateStatsCounter('success-rate-percent', data.success_rate + '%'); + updateStatsCounter('active-jobs-count', data.active_jobs); + updateStatsCounter('failed-jobs-count', data.failed_jobs); + updateStatsCounter('total-users-count', data.total_users); + } else { + throw new Error(data.error || 'Fehler beim Laden der Statistiken'); + } + } + + // Charts aktualisieren + if (window.refreshAllCharts) { + window.refreshAllCharts(); + } + + showToast('✅ Statistiken erfolgreich aktualisiert', 'success'); + } catch (error) { + console.error('Stats-Refresh Fehler:', error); + showToast('❌ Fehler beim Aktualisieren der Statistiken', 'error'); + } finally { + if (refreshButton) { + refreshButton.disabled = false; + const icon = refreshButton.querySelector('svg'); + if (icon) { + icon.classList.remove('animate-spin'); + } + } + } +}; + /** * Jobs-Refresh-Funktion */ @@ -210,6 +266,57 @@ function updateDashboardStats(stats) { console.log('📊 Dashboard-Statistiken aktualisiert:', stats); } +/** + * Statistiken-Counter im DOM aktualisieren + */ +function updateStatsCounter(elementId, value, animate = true) { + const element = document.getElementById(elementId); + if (!element) return; + + if (animate) { + // Animierte Zählung + const currentValue = parseInt(element.textContent.replace(/[^\d]/g, '')) || 0; + const targetValue = parseInt(value.toString().replace(/[^\d]/g, '')) || 0; + + if (currentValue !== targetValue) { + animateCounter(element, currentValue, targetValue, value.toString()); + } + } else { + element.textContent = value; + } +} + +/** + * Animierte Counter-Funktion + */ +function animateCounter(element, start, end, finalText) { + const duration = 1000; // 1 Sekunde + const startTime = performance.now(); + + function updateCounter(currentTime) { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Easing-Funktion (ease-out) + const easeOut = 1 - Math.pow(1 - progress, 3); + const currentValue = Math.round(start + (end - start) * easeOut); + + if (finalText.includes('%')) { + element.textContent = currentValue + '%'; + } else { + element.textContent = currentValue; + } + + if (progress < 1) { + requestAnimationFrame(updateCounter); + } else { + element.textContent = finalText; + } + } + + requestAnimationFrame(updateCounter); +} + /** * CSRF-Token abrufen */ diff --git a/backend/app/templates/stats.html b/backend/app/templates/stats.html index 6010d304..ba32ed7d 100644 --- a/backend/app/templates/stats.html +++ b/backend/app/templates/stats.html @@ -152,6 +152,9 @@ + + +