diff --git a/backend/app/app.py b/backend/app/app.py index 820acbfb..3fcfb9c1 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -4164,6937 +4164,6 @@ def admin_update_printer_form(printer_id): flash("Fehler beim Aktualisieren des Druckers.", "error") return redirect(url_for("admin_edit_printer_page", printer_id=printer_id)) -# Login-Manager initialisieren -login_manager = LoginManager() -login_manager.init_app(app) -login_manager.login_view = "login" -login_manager.login_message = "Bitte melden Sie sich an, um auf diese Seite zuzugreifen." -login_manager.login_message_category = "info" - -@login_manager.user_loader -def load_user(user_id): - """ - Robuster User-Loader mit Error-Handling für Schema-Probleme. - """ - try: - # user_id von Flask-Login ist immer ein String - zu Integer konvertieren - try: - user_id_int = int(user_id) - except (ValueError, TypeError): - app_logger.error(f"Ungültige User-ID: {user_id}") - return None - - db_session = get_db_session() - - # Robuste Abfrage mit Error-Handling - try: - user = db_session.query(User).filter(User.id == user_id_int).first() - db_session.close() - return user - except Exception as db_error: - # Schema-Problem - versuche manuelle Abfrage - app_logger.warning(f"Schema-Problem beim User-Load für ID {user_id_int}: {str(db_error)}") - - # Manuelle Abfrage nur mit Basis-Feldern - try: - result = db_session.execute( - text("SELECT id, email, password_hash, name, role, active FROM users WHERE id = :user_id"), - {"user_id": user_id_int} - ).fetchone() - - if result: - # Manuell User-Objekt erstellen - user = User() - user.id = result[0] - user.email = result[1] if len(result) > 1 else f"user_{user_id_int}@system.local" - user.password_hash = result[2] if len(result) > 2 else "" - user.name = result[3] if len(result) > 3 else f"User {user_id_int}" - user.role = result[4] if len(result) > 4 else "user" - user.active = result[5] if len(result) > 5 else True - - # Standard-Werte für fehlende Felder - user.username = getattr(user, 'username', user.email.split('@')[0]) - user.created_at = getattr(user, 'created_at', datetime.now()) - user.last_login = getattr(user, 'last_login', None) - user.updated_at = getattr(user, 'updated_at', datetime.now()) - - db_session.close() - return user - - except Exception as manual_error: - app_logger.error(f"Auch manuelle User-Abfrage fehlgeschlagen: {str(manual_error)}") - - db_session.close() - return None - - except Exception as e: - app_logger.error(f"Kritischer Fehler im User-Loader für ID {user_id}: {str(e)}") - return None - -# Jinja2 Context Processors -@app.context_processor -def inject_now(): - """Inject the current datetime into templates.""" - return {'now': datetime.now()} - -# Custom Jinja2 filter für Datumsformatierung -@app.template_filter('format_datetime') -def format_datetime_filter(value, format='%d.%m.%Y %H:%M'): - """Format a datetime object to a German-style date and time string""" - if value is None: - return "" - if isinstance(value, str): - try: - value = datetime.fromisoformat(value) - except ValueError: - return value - return value.strftime(format) - -# Logging initialisieren -setup_logging() -log_startup_info() - -# Logger für verschiedene Komponenten -app_logger = get_logger("app") -auth_logger = get_logger("auth") -jobs_logger = get_logger("jobs") -printers_logger = get_logger("printers") -user_logger = get_logger("user") -kiosk_logger = get_logger("kiosk") - -# HTTP-Request/Response-Middleware für automatisches Debug-Logging -@app.before_request -def log_request_info(): - """Loggt detaillierte Informationen über eingehende HTTP-Anfragen.""" - # Nur für API-Endpunkte und wenn Debug-Level aktiviert ist - if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG: - debug_request(app_logger, request) - -@app.after_request -def log_response_info(response): - """Loggt detaillierte Informationen über ausgehende HTTP-Antworten.""" - # Nur für API-Endpunkte und wenn Debug-Level aktiviert ist - if request.path.startswith('/api/') or app_logger.level <= logging.DEBUG: - # Berechne Response-Zeit aus dem g-Objekt wenn verfügbar - duration_ms = None - if hasattr(request, '_start_time'): - duration_ms = (time.time() - request._start_time) * 1000 - - debug_response(app_logger, response, duration_ms) - - return response - -# Start-Zeit für Request-Timing setzen -@app.before_request -def start_timer(): - """Setzt einen Timer für die Request-Bearbeitung.""" - request._start_time = time.time() - -# Sicheres Passwort-Hash für Kiosk-Deaktivierung -KIOSK_PASSWORD_HASH = generate_password_hash("744563017196A") - -print("Alle Blueprints wurden in app.py integriert") - -# Custom decorator für Job-Besitzer-Check -def job_owner_required(f): - @wraps(f) - def decorated_function(job_id, *args, **kwargs): - db_session = get_db_session() - job = db_session.query(Job).filter(Job.id == job_id).first() - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - is_owner = job.user_id == int(current_user.id) or job.owner_id == int(current_user.id) - is_admin = current_user.is_admin - - if not (is_owner or is_admin): - db_session.close() - return jsonify({"error": "Keine Berechtigung"}), 403 - - db_session.close() - return f(job_id, *args, **kwargs) - return decorated_function - -# Custom decorator für Admin-Check -def admin_required(f): - @wraps(f) - @login_required - def decorated_function(*args, **kwargs): - app_logger.info(f"Admin-Check für Funktion {f.__name__}: User authenticated: {current_user.is_authenticated}, User ID: {current_user.id if current_user.is_authenticated else 'None'}, Is Admin: {current_user.is_admin if current_user.is_authenticated else 'None'}") - if not current_user.is_admin: - app_logger.warning(f"Admin-Zugriff verweigert für User {current_user.id if current_user.is_authenticated else 'Anonymous'} auf Funktion {f.__name__}") - return jsonify({"error": "Nur Administratoren haben Zugriff"}), 403 - return f(*args, **kwargs) - return decorated_function - -# ===== AUTHENTIFIZIERUNGS-ROUTEN (ehemals auth.py) ===== - -@app.route("/auth/login", methods=["GET", "POST"]) -def login(): - if current_user.is_authenticated: - return redirect(url_for("index")) - - error = None - if request.method == "POST": - # Unterscheiden zwischen JSON-Anfragen und normalen Formular-Anfragen - is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json' - - # Daten je nach Anfrageart auslesen - if is_json_request: - data = request.get_json() - username = data.get("username") or data.get("email") # Fallback für email - password = data.get("password") - remember_me = data.get("remember_me", False) - else: - # Korrigierte Feldnamen - Template verwendet "email" nicht "username" - username = request.form.get("email") # Geändert von "username" zu "email" - password = request.form.get("password") - remember_me = request.form.get("remember_me") == "on" # Geändert von "remember-me" - - if not username or not password: - error = "Benutzername und Passwort müssen angegeben werden." - if is_json_request: - return jsonify({"error": error}), 400 - else: - try: - db_session = get_db_session() - # Suche nach Benutzer mit übereinstimmendem Benutzernamen oder E-Mail - user = db_session.query(User).filter( - (User.username == username) | (User.email == username) - ).first() - - if user and user.check_password(password): - # Update last login timestamp - user.update_last_login() - db_session.commit() - - login_user(user, remember=remember_me) - auth_logger.info(f"Benutzer {username} hat sich angemeldet") - - next_page = request.args.get("next") - db_session.close() - - if is_json_request: - return jsonify({"success": True, "redirect_url": next_page or url_for("index")}) - else: - if next_page: - return redirect(next_page) - return redirect(url_for("index")) - else: - error = "Ungültiger Benutzername oder Passwort." - auth_logger.warning(f"Fehlgeschlagener Login-Versuch für Benutzer {username}") - db_session.close() - - if is_json_request: - return jsonify({"error": error}), 401 - except Exception as e: - # Fehlerbehandlung für Datenbankprobleme - error = "Anmeldefehler. Bitte versuchen Sie es später erneut." - auth_logger.error(f"Fehler bei der Anmeldung: {str(e)}") - if is_json_request: - return jsonify({"error": error}), 500 - - return render_template("login.html", error=error) - -@app.route("/auth/logout", methods=["GET", "POST"]) -@login_required -def auth_logout(): - """Meldet den Benutzer ab.""" - app_logger.info(f"Benutzer {current_user.email} hat sich abgemeldet") - logout_user() - flash("Sie wurden erfolgreich abgemeldet.", "info") - return redirect(url_for("login")) - -@app.route("/auth/reset-password-request", methods=["GET", "POST"]) -def reset_password_request(): - """Passwort-Reset anfordern (Placeholder).""" - # TODO: Implement password reset functionality - flash("Passwort-Reset-Funktionalität ist noch nicht implementiert.", "info") - return redirect(url_for("login")) - -@app.route("/auth/api/login", methods=["POST"]) -def api_login(): - """API-Login-Endpunkt für Frontend""" - try: - data = request.get_json() - if not data: - return jsonify({"error": "Keine Daten erhalten"}), 400 - - username = data.get("username") - password = data.get("password") - remember_me = data.get("remember_me", False) - - if not username or not password: - return jsonify({"error": "Benutzername und Passwort müssen angegeben werden"}), 400 - - db_session = get_db_session() - user = db_session.query(User).filter( - (User.username == username) | (User.email == username) - ).first() - - if user and user.check_password(password): - # Update last login timestamp - user.update_last_login() - db_session.commit() - - login_user(user, remember=remember_me) - auth_logger.info(f"API-Login erfolgreich für Benutzer {username}") - - user_data = { - "id": user.id, - "username": user.username, - "name": user.name, - "email": user.email, - "is_admin": user.is_admin - } - - db_session.close() - return jsonify({ - "success": True, - "user": user_data, - "redirect_url": url_for("index") - }) - else: - auth_logger.warning(f"Fehlgeschlagener API-Login für Benutzer {username}") - db_session.close() - return jsonify({"error": "Ungültiger Benutzername oder Passwort"}), 401 - - except Exception as e: - auth_logger.error(f"Fehler beim API-Login: {str(e)}") - return jsonify({"error": "Anmeldefehler. Bitte versuchen Sie es später erneut"}), 500 - -@app.route("/auth/api/callback", methods=["GET", "POST"]) -def api_callback(): - """OAuth-Callback-Endpunkt für externe Authentifizierung""" - try: - # OAuth-Provider bestimmen - provider = request.args.get('provider', 'github') - - if request.method == "GET": - # Authorization Code aus URL-Parameter extrahieren - code = request.args.get('code') - state = request.args.get('state') - error = request.args.get('error') - - if error: - auth_logger.warning(f"OAuth-Fehler von {provider}: {error}") - return jsonify({ - "error": f"OAuth-Authentifizierung fehlgeschlagen: {error}", - "redirect_url": url_for("login") - }), 400 - - if not code: - auth_logger.warning(f"Kein Authorization Code von {provider} erhalten") - return jsonify({ - "error": "Kein Authorization Code erhalten", - "redirect_url": url_for("login") - }), 400 - - # State-Parameter validieren (CSRF-Schutz) - session_state = session.get('oauth_state') - if not state or state != session_state: - auth_logger.warning(f"Ungültiger State-Parameter von {provider}") - return jsonify({ - "error": "Ungültiger State-Parameter", - "redirect_url": url_for("login") - }), 400 - - # OAuth-Token austauschen - if provider == 'github': - user_data = handle_github_callback(code) - else: - auth_logger.error(f"Unbekannter OAuth-Provider: {provider}") - return jsonify({ - "error": "Unbekannter OAuth-Provider", - "redirect_url": url_for("login") - }), 400 - - if not user_data: - return jsonify({ - "error": "Fehler beim Abrufen der Benutzerdaten", - "redirect_url": url_for("login") - }), 400 - - # Benutzer in Datenbank suchen oder erstellen - db_session = get_db_session() - try: - user = db_session.query(User).filter( - User.email == user_data['email'] - ).first() - - if not user: - # Neuen Benutzer erstellen - user = User( - username=user_data['username'], - email=user_data['email'], - name=user_data['name'], - is_admin=False, - oauth_provider=provider, - oauth_id=str(user_data['id']) - ) - # Zufälliges Passwort setzen (wird nicht verwendet) - import secrets - user.set_password(secrets.token_urlsafe(32)) - db_session.add(user) - db_session.commit() - auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}") - else: - # Bestehenden Benutzer aktualisieren - user.oauth_provider = provider - user.oauth_id = str(user_data['id']) - user.name = user_data['name'] - user.updated_at = datetime.now() - db_session.commit() - auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}") - - # Update last login timestamp - user.update_last_login() - db_session.commit() - - login_user(user, remember=True) - - # Session-State löschen - session.pop('oauth_state', None) - - response_data = { - "success": True, - "user": { - "id": user.id, - "username": user.username, - "name": user.name, - "email": user.email, - "is_admin": user.is_admin - }, - "redirect_url": url_for("index") - } - - db_session.close() - return jsonify(response_data) - - except Exception as e: - db_session.rollback() - db_session.close() - auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}") - return jsonify({ - "error": "Datenbankfehler bei der Benutzeranmeldung", - "redirect_url": url_for("login") - }), 500 - - elif request.method == "POST": - # POST-Anfragen für manuelle Token-Übermittlung - data = request.get_json() - if not data: - return jsonify({"error": "Keine Daten erhalten"}), 400 - - access_token = data.get('access_token') - provider = data.get('provider', 'github') - - if not access_token: - return jsonify({"error": "Kein Access Token erhalten"}), 400 - - # Benutzerdaten mit Access Token abrufen - if provider == 'github': - user_data = get_github_user_data(access_token) - else: - return jsonify({"error": "Unbekannter OAuth-Provider"}), 400 - - if not user_data: - return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 400 - - # Benutzer verarbeiten (gleiche Logik wie bei GET) - db_session = get_db_session() - try: - user = db_session.query(User).filter( - User.email == user_data['email'] - ).first() - - if not user: - user = User( - username=user_data['username'], - email=user_data['email'], - name=user_data['name'], - is_admin=False, - oauth_provider=provider, - oauth_id=str(user_data['id']) - ) - import secrets - user.set_password(secrets.token_urlsafe(32)) - db_session.add(user) - db_session.commit() - auth_logger.info(f"Neuer OAuth-Benutzer erstellt: {user.username} via {provider}") - else: - user.oauth_provider = provider - user.oauth_id = str(user_data['id']) - user.name = user_data['name'] - user.updated_at = datetime.now() - db_session.commit() - auth_logger.info(f"OAuth-Benutzer aktualisiert: {user.username} via {provider}") - - # Update last login timestamp - user.update_last_login() - db_session.commit() - - login_user(user, remember=True) - - response_data = { - "success": True, - "user": { - "id": user.id, - "username": user.username, - "name": user.name, - "email": user.email, - "is_admin": user.is_admin - }, - "redirect_url": url_for("index") - } - - db_session.close() - return jsonify(response_data) - - except Exception as e: - db_session.rollback() - db_session.close() - auth_logger.error(f"Datenbankfehler bei OAuth-Callback: {str(e)}") - return jsonify({ - "error": "Datenbankfehler bei der Benutzeranmeldung", - "redirect_url": url_for("login") - }), 500 - - except Exception as e: - auth_logger.error(f"Fehler im OAuth-Callback: {str(e)}") - return jsonify({ - "error": "OAuth-Callback-Fehler", - "redirect_url": url_for("login") - }), 500 - -def handle_github_callback(code): - """GitHub OAuth-Callback verarbeiten""" - try: - import requests - - # GitHub OAuth-Konfiguration (sollte aus Umgebungsvariablen kommen) - client_id = "7c5d8bef1a5519ec1fdc" - client_secret = "5f1e586204358fbd53cf5fb7d418b3f06ccab8fd" - - if not client_id or not client_secret: - auth_logger.error("GitHub OAuth-Konfiguration fehlt") - return None - - # Access Token anfordern - token_url = "https://github.com/login/oauth/access_token" - token_data = { - 'client_id': client_id, - 'client_secret': client_secret, - 'code': code - } - - token_response = requests.post( - token_url, - data=token_data, - headers={'Accept': 'application/json'}, - timeout=10 - ) - - if token_response.status_code != 200: - auth_logger.error(f"GitHub Token-Anfrage fehlgeschlagen: {token_response.status_code}") - return None - - token_json = token_response.json() - access_token = token_json.get('access_token') - - if not access_token: - auth_logger.error("Kein Access Token von GitHub erhalten") - return None - - return get_github_user_data(access_token) - - except Exception as e: - auth_logger.error(f"Fehler bei GitHub OAuth-Callback: {str(e)}") - return None - -def get_github_user_data(access_token): - """GitHub-Benutzerdaten mit Access Token abrufen""" - try: - import requests - - # Benutzerdaten von GitHub API abrufen - user_url = "https://api.github.com/user" - headers = { - 'Authorization': f'token {access_token}', - 'Accept': 'application/vnd.github.v3+json' - } - - user_response = requests.get(user_url, headers=headers, timeout=10) - - if user_response.status_code != 200: - auth_logger.error(f"GitHub User-API-Anfrage fehlgeschlagen: {user_response.status_code}") - return None - - user_data = user_response.json() - - # E-Mail-Adresse separat abrufen (falls nicht öffentlich) - email = user_data.get('email') - if not email: - email_url = "https://api.github.com/user/emails" - email_response = requests.get(email_url, headers=headers, timeout=10) - - if email_response.status_code == 200: - emails = email_response.json() - # Primäre E-Mail-Adresse finden - for email_obj in emails: - if email_obj.get('primary', False): - email = email_obj.get('email') - break - - # Fallback: Erste E-Mail-Adresse verwenden - if not email and emails: - email = emails[0].get('email') - - if not email: - auth_logger.error("Keine E-Mail-Adresse von GitHub erhalten") - return None - - return { - 'id': user_data.get('id'), - 'username': user_data.get('login'), - 'name': user_data.get('name') or user_data.get('login'), - 'email': email - } - - except Exception as e: - auth_logger.error(f"Fehler beim Abrufen der GitHub-Benutzerdaten: {str(e)}") - return None - -# ===== BENUTZER-ROUTEN (ehemals user.py) ===== - -@app.route("/user/profile", methods=["GET"]) -@login_required -def user_profile(): - """Profil-Seite anzeigen""" - user_logger.info(f"Benutzer {current_user.username} hat seine Profilseite aufgerufen") - return render_template("profile.html", user=current_user) - -@app.route("/user/settings", methods=["GET"]) -@login_required -def user_settings(): - """Einstellungen-Seite anzeigen""" - user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungsseite aufgerufen") - return render_template("settings.html", user=current_user) - -@app.route("/user/update-profile", methods=["POST"]) -@login_required -def user_update_profile(): - """Benutzerprofilinformationen aktualisieren""" - try: - # Überprüfen, ob es sich um eine JSON-Anfrage handelt - is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json' - - if is_json_request: - data = request.get_json() - name = data.get("name") - email = data.get("email") - department = data.get("department") - position = data.get("position") - phone = data.get("phone") - else: - name = request.form.get("name") - email = request.form.get("email") - department = request.form.get("department") - position = request.form.get("position") - phone = request.form.get("phone") - - db_session = get_db_session() - user = db_session.query(User).filter(User.id == int(current_user.id)).first() - - if user: - # Aktualisiere die Benutzerinformationen - if name: - user.name = name - if email: - user.email = email - if department: - user.department = department - if position: - user.position = position - if phone: - user.phone = phone - - user.updated_at = datetime.now() - db_session.commit() - user_logger.info(f"Benutzer {current_user.username} hat sein Profil aktualisiert") - - if is_json_request: - return jsonify({ - "success": True, - "message": "Profil erfolgreich aktualisiert" - }) - else: - flash("Profil erfolgreich aktualisiert", "success") - return redirect(url_for("user_profile")) - else: - error = "Benutzer nicht gefunden." - if is_json_request: - return jsonify({"error": error}), 404 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - - except Exception as e: - error = f"Fehler beim Aktualisieren des Profils: {str(e)}" - user_logger.error(error) - if request.is_json: - return jsonify({"error": error}), 500 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - finally: - db_session.close() - -@app.route("/user/api/update-settings", methods=["POST"]) -@login_required -def user_api_update_settings(): - """API-Endpunkt für Einstellungen-Updates (JSON)""" - return user_update_profile() - -@app.route("/user/update-settings", methods=["POST"]) -@login_required -def user_update_settings(): - """Benutzereinstellungen aktualisieren""" - db_session = get_db_session() - try: - # Überprüfen, ob es sich um eine JSON-Anfrage handelt - is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json' - - # Einstellungen aus der Anfrage extrahieren - if is_json_request: - data = request.get_json() - if not data: - return jsonify({"error": "Keine Daten empfangen"}), 400 - - theme = data.get("theme", "system") - reduced_motion = bool(data.get("reduced_motion", False)) - contrast = data.get("contrast", "normal") - notifications = data.get("notifications", {}) - privacy = data.get("privacy", {}) - else: - theme = request.form.get("theme", "system") - reduced_motion = request.form.get("reduced_motion") == "on" - contrast = request.form.get("contrast", "normal") - notifications = { - "new_jobs": request.form.get("notify_new_jobs") == "on", - "job_updates": request.form.get("notify_job_updates") == "on", - "system": request.form.get("notify_system") == "on", - "email": request.form.get("notify_email") == "on" - } - privacy = { - "activity_logs": request.form.get("activity_logs") == "on", - "two_factor": request.form.get("two_factor") == "on", - "auto_logout": int(request.form.get("auto_logout", "60")) - } - - # Validierung der Eingaben - valid_themes = ["light", "dark", "system"] - if theme not in valid_themes: - theme = "system" - - valid_contrasts = ["normal", "high"] - if contrast not in valid_contrasts: - contrast = "normal" - - # Benutzer aus der Datenbank laden - user = db_session.query(User).filter(User.id == int(current_user.id)).first() - - if not user: - error = "Benutzer nicht gefunden." - if is_json_request: - return jsonify({"error": error}), 404 - else: - flash(error, "error") - return redirect(url_for("user_settings")) - - # Einstellungen-Dictionary erstellen - settings = { - "theme": theme, - "reduced_motion": reduced_motion, - "contrast": contrast, - "notifications": { - "new_jobs": bool(notifications.get("new_jobs", True)), - "job_updates": bool(notifications.get("job_updates", True)), - "system": bool(notifications.get("system", True)), - "email": bool(notifications.get("email", False)) - }, - "privacy": { - "activity_logs": bool(privacy.get("activity_logs", True)), - "two_factor": bool(privacy.get("two_factor", False)), - "auto_logout": max(5, min(480, int(privacy.get("auto_logout", 60)))) # 5-480 Minuten - }, - "last_updated": datetime.now().isoformat() - } - - # Prüfen, ob User-Tabelle eine settings-Spalte hat - if hasattr(user, 'settings'): - # Einstellungen in der Datenbank speichern - import json - user.settings = json.dumps(settings) - else: - # Fallback: In Session speichern (temporär) - session['user_settings'] = settings - - user.updated_at = datetime.now() - db_session.commit() - - user_logger.info(f"Benutzer {current_user.username} hat seine Einstellungen aktualisiert") - - if is_json_request: - return jsonify({ - "success": True, - "message": "Einstellungen erfolgreich aktualisiert", - "settings": settings - }) - else: - flash("Einstellungen erfolgreich aktualisiert", "success") - return redirect(url_for("user_settings")) - - except ValueError as e: - error = f"Ungültige Eingabedaten: {str(e)}" - user_logger.warning(f"Ungültige Einstellungsdaten von Benutzer {current_user.username}: {str(e)}") - if is_json_request: - return jsonify({"error": error}), 400 - else: - flash(error, "error") - return redirect(url_for("user_settings")) - except Exception as e: - db_session.rollback() - error = f"Fehler beim Aktualisieren der Einstellungen: {str(e)}" - user_logger.error(f"Fehler beim Aktualisieren der Einstellungen für Benutzer {current_user.username}: {str(e)}") - if is_json_request: - return jsonify({"error": "Interner Serverfehler"}), 500 - else: - flash("Fehler beim Speichern der Einstellungen", "error") - return redirect(url_for("user_settings")) - finally: - db_session.close() - -@app.route("/api/user/settings", methods=["GET"]) -@login_required -def get_user_settings(): - """Holt die aktuellen Benutzereinstellungen""" - try: - # Einstellungen aus Session oder Datenbank laden - user_settings = session.get('user_settings', {}) - - # Standard-Einstellungen falls keine vorhanden - default_settings = { - "theme": "system", - "reduced_motion": False, - "contrast": "normal", - "notifications": { - "new_jobs": True, - "job_updates": True, - "system": True, - "email": False - }, - "privacy": { - "activity_logs": True, - "two_factor": False, - "auto_logout": 60 - } - } - - # Merge mit Standard-Einstellungen - settings = {**default_settings, **user_settings} - - return jsonify({ - "success": True, - "settings": settings - }) - - except Exception as e: - user_logger.error(f"Fehler beim Laden der Benutzereinstellungen: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Einstellungen" - }), 500 - -@app.route("/user/change-password", methods=["POST"]) -@login_required -def user_change_password(): - """Benutzerpasswort ändern""" - try: - # Überprüfen, ob es sich um eine JSON-Anfrage handelt - is_json_request = request.is_json or request.headers.get('Content-Type') == 'application/json' - - if is_json_request: - data = request.get_json() - current_password = data.get("current_password") - new_password = data.get("new_password") - confirm_password = data.get("confirm_password") - else: - current_password = request.form.get("current_password") - new_password = request.form.get("new_password") - confirm_password = request.form.get("confirm_password") - - # Prüfen, ob alle Felder ausgefüllt sind - if not current_password or not new_password or not confirm_password: - error = "Alle Passwortfelder müssen ausgefüllt sein." - if is_json_request: - return jsonify({"error": error}), 400 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - - # Prüfen, ob das neue Passwort und die Bestätigung übereinstimmen - if new_password != confirm_password: - error = "Das neue Passwort und die Bestätigung stimmen nicht überein." - if is_json_request: - return jsonify({"error": error}), 400 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - - db_session = get_db_session() - user = db_session.query(User).filter(User.id == int(current_user.id)).first() - - if user and user.check_password(current_password): - # Passwort aktualisieren - user.set_password(new_password) - user.updated_at = datetime.now() - db_session.commit() - - user_logger.info(f"Benutzer {current_user.username} hat sein Passwort geändert") - - if is_json_request: - return jsonify({ - "success": True, - "message": "Passwort erfolgreich geändert" - }) - else: - flash("Passwort erfolgreich geändert", "success") - return redirect(url_for("user_profile")) - else: - error = "Das aktuelle Passwort ist nicht korrekt." - if is_json_request: - return jsonify({"error": error}), 401 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - - except Exception as e: - error = f"Fehler beim Ändern des Passworts: {str(e)}" - user_logger.error(error) - if request.is_json: - return jsonify({"error": error}), 500 - else: - flash(error, "error") - return redirect(url_for("user_profile")) - finally: - db_session.close() - -@app.route("/user/export", methods=["GET"]) -@login_required -def user_export_data(): - """Exportiert alle Benutzerdaten als JSON für DSGVO-Konformität""" - try: - db_session = get_db_session() - user = db_session.query(User).filter(User.id == int(current_user.id)).first() - - if not user: - db_session.close() - return jsonify({"error": "Benutzer nicht gefunden"}), 404 - - # Benutzerdaten abrufen - user_data = user.to_dict() - - # Jobs des Benutzers abrufen - jobs = db_session.query(Job).filter(Job.user_id == user.id).all() - user_data["jobs"] = [job.to_dict() for job in jobs] - - # Aktivitäten und Einstellungen hinzufügen - user_data["settings"] = session.get('user_settings', {}) - - # Persönliche Statistiken - user_data["statistics"] = { - "total_jobs": len(jobs), - "completed_jobs": len([j for j in jobs if j.status == "finished"]), - "failed_jobs": len([j for j in jobs if j.status == "failed"]), - "account_created": user.created_at.isoformat() if user.created_at else None, - "last_login": user.last_login.isoformat() if user.last_login else None - } - - db_session.close() - - # Daten als JSON-Datei zum Download anbieten - response = make_response(json.dumps(user_data, indent=4)) - response.headers["Content-Disposition"] = f"attachment; filename=user_data_{user.username}.json" - response.headers["Content-Type"] = "application/json" - - user_logger.info(f"Benutzer {current_user.username} hat seine Daten exportiert") - return response - - except Exception as e: - error = f"Fehler beim Exportieren der Benutzerdaten: {str(e)}" - user_logger.error(error) - return jsonify({"error": error}), 500 - -@app.route("/user/profile", methods=["PUT"]) -@login_required -def user_update_profile_api(): - """API-Endpunkt zum Aktualisieren des Benutzerprofils""" - try: - if not request.is_json: - return jsonify({"error": "Anfrage muss im JSON-Format sein"}), 400 - - data = request.get_json() - db_session = get_db_session() - user = db_session.query(User).filter(User.id == int(current_user.id)).first() - - if not user: - db_session.close() - return jsonify({"error": "Benutzer nicht gefunden"}), 404 - - # Aktualisiere nur die bereitgestellten Felder - if "name" in data: - user.name = data["name"] - if "email" in data: - user.email = data["email"] - if "department" in data: - user.department = data["department"] - if "position" in data: - user.position = data["position"] - if "phone" in data: - user.phone = data["phone"] - if "bio" in data: - user.bio = data["bio"] - - user.updated_at = datetime.now() - db_session.commit() - - # Aktualisierte Benutzerdaten zurückgeben - user_data = user.to_dict() - db_session.close() - - user_logger.info(f"Benutzer {current_user.username} hat sein Profil über die API aktualisiert") - return jsonify({ - "success": True, - "message": "Profil erfolgreich aktualisiert", - "user": user_data - }) - - except Exception as e: - error = f"Fehler beim Aktualisieren des Profils: {str(e)}" - user_logger.error(error) - return jsonify({"error": error}), 500 - -# ===== KIOSK-KONTROLL-ROUTEN (ehemals kiosk_control.py) ===== - -@app.route('/api/kiosk/status', methods=['GET']) -def kiosk_get_status(): - """Kiosk-Status abrufen.""" - try: - # Prüfen ob Kiosk-Modus aktiv ist - kiosk_active = os.path.exists('/tmp/kiosk_active') - - return jsonify({ - "active": kiosk_active, - "message": "Kiosk-Status erfolgreich abgerufen" - }) - except Exception as e: - kiosk_logger.error(f"Fehler beim Abrufen des Kiosk-Status: {str(e)}") - return jsonify({"error": "Fehler beim Abrufen des Status"}), 500 - -@app.route('/api/kiosk/deactivate', methods=['POST']) -def kiosk_deactivate(): - """Kiosk-Modus mit Passwort deaktivieren.""" - try: - data = request.get_json() - if not data or 'password' not in data: - return jsonify({"error": "Passwort erforderlich"}), 400 - - password = data['password'] - - # Passwort überprüfen - if not check_password_hash(KIOSK_PASSWORD_HASH, password): - kiosk_logger.warning(f"Fehlgeschlagener Kiosk-Deaktivierungsversuch von IP: {request.remote_addr}") - return jsonify({"error": "Ungültiges Passwort"}), 401 - - # Kiosk deaktivieren - try: - # Kiosk-Service stoppen - subprocess.run(['sudo', 'systemctl', 'stop', 'myp-kiosk'], check=True) - subprocess.run(['sudo', 'systemctl', 'disable', 'myp-kiosk'], check=True) - - # Kiosk-Marker entfernen - if os.path.exists('/tmp/kiosk_active'): - os.remove('/tmp/kiosk_active') - - # Normale Desktop-Umgebung wiederherstellen - subprocess.run(['sudo', 'systemctl', 'set-default', 'graphical.target'], check=True) - - kiosk_logger.info(f"Kiosk-Modus erfolgreich deaktiviert von IP: {request.remote_addr}") - - return jsonify({ - "success": True, - "message": "Kiosk-Modus erfolgreich deaktiviert. System wird neu gestartet." - }) - - except subprocess.CalledProcessError as e: - kiosk_logger.error(f"Fehler beim Deaktivieren des Kiosk-Modus: {str(e)}") - return jsonify({"error": "Fehler beim Deaktivieren des Kiosk-Modus"}), 500 - - except Exception as e: - kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Deaktivierung: {str(e)}") - return jsonify({"error": "Unerwarteter Fehler"}), 500 -@app.route('/api/kiosk/activate', methods=['POST']) -@login_required -def kiosk_activate(): - """Kiosk-Modus aktivieren (nur für Admins).""" - try: - # Admin-Authentifizierung prüfen - if not current_user.is_admin: - kiosk_logger.warning(f"Nicht-Admin-Benutzer {current_user.username} versuchte Kiosk-Aktivierung") - return jsonify({"error": "Nur Administratoren können den Kiosk-Modus aktivieren"}), 403 - - # Kiosk aktivieren - try: - # Kiosk-Marker setzen - with open('/tmp/kiosk_active', 'w') as f: - f.write('1') - - # Kiosk-Service aktivieren - subprocess.run(['sudo', 'systemctl', 'enable', 'myp-kiosk'], check=True) - subprocess.run(['sudo', 'systemctl', 'start', 'myp-kiosk'], check=True) - - kiosk_logger.info(f"Kiosk-Modus erfolgreich aktiviert von Admin {current_user.username} (IP: {request.remote_addr})") - - return jsonify({ - "success": True, - "message": "Kiosk-Modus erfolgreich aktiviert" - }) - - except subprocess.CalledProcessError as e: - kiosk_logger.error(f"Fehler beim Aktivieren des Kiosk-Modus: {str(e)}") - return jsonify({"error": "Fehler beim Aktivieren des Kiosk-Modus"}), 500 - - except Exception as e: - kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Aktivierung: {str(e)}") - return jsonify({"error": "Unerwarteter Fehler"}), 500 - -@app.route('/api/kiosk/restart', methods=['POST']) -def kiosk_restart_system(): - """System neu starten (nur nach Kiosk-Deaktivierung).""" - try: - data = request.get_json() - if not data or 'password' not in data: - return jsonify({"error": "Passwort erforderlich"}), 400 - - password = data['password'] - - # Passwort überprüfen - if not check_password_hash(KIOSK_PASSWORD_HASH, password): - kiosk_logger.warning(f"Fehlgeschlagener Neustart-Versuch von IP: {request.remote_addr}") - return jsonify({"error": "Ungültiges Passwort"}), 401 - - kiosk_logger.info(f"System-Neustart initiiert von IP: {request.remote_addr}") - - # System nach kurzer Verzögerung neu starten - subprocess.Popen(['sudo', 'shutdown', '-r', '+1']) - - return jsonify({ - "success": True, - "message": "System wird in 1 Minute neu gestartet" - }) - - except Exception as e: - kiosk_logger.error(f"Fehler beim System-Neustart: {str(e)}") - return jsonify({"error": "Fehler beim Neustart"}), 500 - -# ===== HILFSFUNKTIONEN ===== - -@measure_execution_time(logger=printers_logger, task_name="Drucker-Status-Prüfung") -def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: - """ - Überprüft den Status eines Druckers über TP-Link Tapo P110-Steckdosenabfrage. - - Args: - ip_address: IP-Adresse der Drucker-Steckdose - timeout: Timeout in Sekunden (Standard: 7) - - Returns: - Tuple[str, bool]: (Status, Aktiv) - Status ist "online" oder "offline", Aktiv ist True/False - """ - if not ip_address or ip_address.strip() == "": - printers_logger.debug(f"Keine IP-Adresse angegeben") - return "offline", False - - try: - # IP-Adresse validieren - import ipaddress - try: - ipaddress.ip_address(ip_address.strip()) - except ValueError: - printers_logger.debug(f"Ungültige IP-Adresse: {ip_address}") - return "offline", False - - # Importiere PyP100 für Tapo-Unterstützung - try: - from PyP100 import PyP110 - except ImportError: - printers_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen nicht abfragen") - return "offline", False - - # Verwende IMMER die globalen hardkodierten Tapo-Anmeldedaten - username = TAPO_USERNAME - password = TAPO_PASSWORD - - printers_logger.debug(f"🔌 Teste Tapo-Steckdose {ip_address} mit hardkodierten Anmeldedaten") - - # TP-Link Tapo P110 Verbindung herstellen - p110 = PyP110.P110(ip_address.strip(), username, password) - p110.handshake() # Authentifizierung - p110.login() # Login - - # Geräteinformationen abrufen - device_info = p110.getDeviceInfo() - device_on = device_info.get('device_on', False) - - if device_on: - printers_logger.debug(f"✅ Drucker {ip_address}: ONLINE (Steckdose eingeschaltet)") - return "online", True - else: - printers_logger.debug(f"🔄 Drucker {ip_address}: STANDBY (Steckdose ausgeschaltet)") - return "standby", False - - except Exception as e: - printers_logger.debug(f"❌ Fehler beim Tapo-Status-Check für {ip_address}: {str(e)}") - return "offline", False - -@measure_execution_time(logger=printers_logger, task_name="Mehrere-Drucker-Status-Prüfung") -def check_multiple_printers_status(printers: List[Dict], timeout: int = 7) -> Dict[int, Tuple[str, bool]]: - """ - Überprüft den Status mehrerer Drucker parallel. - - Args: - printers: Liste der zu prüfenden Drucker - timeout: Timeout für jeden einzelnen Drucker - - Returns: - Dict[int, Tuple[str, bool]]: Dictionary mit Drucker-ID als Key und (Status, Aktiv) als Value - """ - results = {} - - # Wenn keine Drucker vorhanden sind, gebe leeres Dict zurück - if not printers: - printers_logger.info("ℹ️ Keine Drucker zum Status-Check gefunden") - return results - - printers_logger.info(f"🔍 Prüfe Status von {len(printers)} Druckern parallel...") - - # Parallel-Ausführung mit ThreadPoolExecutor - # Sicherstellen, dass max_workers mindestens 1 ist - max_workers = min(max(len(printers), 1), 10) - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - # Futures für alle Drucker erstellen - future_to_printer = { - executor.submit(check_printer_status, printer.get('ip_address'), timeout): printer - for printer in printers - } - - # Ergebnisse sammeln - for future in as_completed(future_to_printer, timeout=timeout + 2): - printer = future_to_printer[future] - try: - status, active = future.result() - results[printer['id']] = (status, active) - printers_logger.info(f"Drucker {printer['name']} ({printer.get('ip_address')}): {status}") - except Exception as e: - printers_logger.error(f"Fehler bei Status-Check für Drucker {printer['name']}: {str(e)}") - results[printer['id']] = ("offline", False) - - printers_logger.info(f"✅ Status-Check abgeschlossen für {len(results)} Drucker") - - return results - -# ===== UI-ROUTEN ===== - -@app.route("/") -def index(): - if current_user.is_authenticated: - return render_template("index.html") - return redirect(url_for("login")) - -@app.route("/dashboard") -@login_required -def dashboard(): - return render_template("dashboard.html") - -@app.route("/profile") -@login_required -def profile_redirect(): - """Leitet zur neuen Profilseite im User-Blueprint weiter.""" - return redirect(url_for("user_profile")) - -@app.route("/profil") -@login_required -def profil_redirect(): - """Leitet zur neuen Profilseite im User-Blueprint weiter (deutsche URL).""" - return redirect(url_for("user_profile")) - -@app.route("/settings") -@login_required -def settings_redirect(): - """Leitet zur neuen Einstellungsseite im User-Blueprint weiter.""" - return redirect(url_for("user_settings")) - -@app.route("/einstellungen") -@login_required -def einstellungen_redirect(): - """Leitet zur neuen Einstellungsseite im User-Blueprint weiter (deutsche URL).""" - return redirect(url_for("user_settings")) - -@app.route("/admin") -@login_required -def admin(): - """Leitet zur neuen Admin-Dashboard-Route weiter.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - return redirect(url_for("admin_page")) - -@app.route("/demo") -@login_required -def components_demo(): - """Demo-Seite für UI-Komponenten""" - return render_template("components_demo.html") - -@app.route("/printers") -@login_required -def printers_page(): - """Zeigt die Übersichtsseite für Drucker an.""" - return render_template("printers.html") - -@app.route("/jobs") -@login_required -def jobs_page(): - """Zeigt die Übersichtsseite für Druckaufträge an.""" - return render_template("jobs.html") - -@app.route("/jobs/new") -@login_required -def new_job_page(): - """Zeigt die Seite zum Erstellen neuer Druckaufträge an.""" - return render_template("jobs.html") - -@app.route("/stats") -@login_required -def stats_page(): - """Zeigt die Statistik-Seite an.""" - return render_template("stats.html") - -@app.route("/admin-dashboard") -@login_required -def admin_page(): - """Zeigt die Administrationsseite an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - # Aktives Tab aus der URL auslesen oder Default-Wert verwenden - active_tab = request.args.get('tab', 'users') - - # Daten für das Admin-Panel direkt beim Laden vorbereiten - stats = {} - users = [] - printers = [] - scheduler_status = {"running": False, "message": "Nicht verfügbar"} - system_info = {"cpu": 0, "memory": 0, "disk": 0} - logs = [] - - db_session = get_db_session() - - try: - # Statistiken laden - from sqlalchemy.orm import joinedload - - # Benutzeranzahl - stats["total_users"] = db_session.query(User).count() - - # Druckeranzahl und Online-Status - all_printers = db_session.query(Printer).all() - stats["total_printers"] = len(all_printers) - stats["online_printers"] = len([p for p in all_printers if p.status == "online"]) - - # Aktive Jobs und Warteschlange - stats["active_jobs"] = db_session.query(Job).filter( - Job.status.in_(["printing", "running"]) - ).count() - - stats["queued_jobs"] = db_session.query(Job).filter( - Job.status == "scheduled" - ).count() - - # Erfolgsrate - total_jobs = db_session.query(Job).filter( - Job.status.in_(["completed", "failed", "cancelled"]) - ).count() - - successful_jobs = db_session.query(Job).filter( - Job.status == "completed" - ).count() - - if total_jobs > 0: - stats["success_rate"] = int((successful_jobs / total_jobs) * 100) - else: - stats["success_rate"] = 0 - - # Benutzer laden - if active_tab == 'users': - users = db_session.query(User).all() - users = [user.to_dict() for user in users] - - # Drucker laden - if active_tab == 'printers': - printers = db_session.query(Printer).all() - printers = [printer.to_dict() for printer in printers] - - # Scheduler-Status laden - if active_tab == 'scheduler': - try: - from utils.scheduler import scheduler_is_running - is_running = scheduler_is_running() - scheduler_status = { - "running": is_running, - "message": "Der Scheduler läuft" if is_running else "Der Scheduler ist gestoppt" - } - except (ImportError, AttributeError): - scheduler_status = { - "running": False, - "message": "Scheduler-Status nicht verfügbar" - } - - # System-Informationen laden - if active_tab == 'system': - import os - import psutil - - # CPU und Memory - cpu_percent = psutil.cpu_percent(interval=1) - memory = psutil.virtual_memory() - disk = psutil.disk_usage('/') - - # Uptime - boot_time = psutil.boot_time() - uptime_seconds = time.time() - boot_time - uptime_days = int(uptime_seconds // 86400) - uptime_hours = int((uptime_seconds % 86400) // 3600) - uptime_minutes = int((uptime_seconds % 3600) // 60) - - # Datenbank-Status - db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'database', 'myp.db') - db_size = 0 - if os.path.exists(db_path): - db_size = os.path.getsize(db_path) / (1024 * 1024) # MB - - # Scheduler-Status - scheduler_running = False - scheduler_jobs = 0 - try: - from utils.job_scheduler import scheduler - scheduler_running = scheduler.running - if hasattr(scheduler, 'get_jobs'): - scheduler_jobs = len(scheduler.get_jobs()) - except: - pass - - # Nächster Job - next_job = db_session.query(Job).filter( - Job.status == "scheduled" - ).order_by(Job.created_at.asc()).first() - - next_job_time = "Keine geplanten Jobs" - if next_job: - next_job_time = next_job.created_at.strftime("%d.%m.%Y %H:%M") - - system_info = { - "cpu_usage": round(cpu_percent, 1), - "memory_usage": round(memory.percent, 1), - "disk_usage": round((disk.used / disk.total) * 100, 1), - "uptime": f"{uptime_days}d {uptime_hours}h {uptime_minutes}m", - "db_size": f"{db_size:.1f} MB", - "db_connections": "Aktiv", - "scheduler_running": scheduler_running, - "scheduler_jobs": scheduler_jobs, - "next_job": next_job_time - } - - # Logs laden - if active_tab == 'logs': - import os - log_level = request.args.get('log_level', 'all') - log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') - - # Logeinträge sammeln - app_logs = [] - for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']: - log_file = os.path.join(log_dir, category, f'{category}.log') - if os.path.exists(log_file): - with open(log_file, 'r') as f: - for line in f.readlines()[-100:]: # Nur die letzten 100 Zeilen pro Datei - if log_level != 'all': - if log_level.upper() not in line: - continue - app_logs.append({ - 'timestamp': line.split(' - ')[0] if ' - ' in line else '', - 'level': line.split(' - ')[1].split(' - ')[0] if ' - ' in line and len(line.split(' - ')) > 2 else 'INFO', - 'category': category, - 'message': ' - '.join(line.split(' - ')[2:]) if ' - ' in line and len(line.split(' - ')) > 2 else line - }) - - # Nach Zeitstempel sortieren (neueste zuerst) - logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100] - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Daten: {str(e)}") - finally: - db_session.close() - - return render_template( - "admin.html", - active_tab=active_tab, - stats=stats, - users=users, - printers=printers, - scheduler_status=scheduler_status, - system_info=system_info, - logs=logs - ) - -# ===== ERROR MONITORING SYSTEM ===== - -@app.route("/api/admin/system-health", methods=['GET']) -@login_required -def api_admin_system_health(): - """API-Endpunkt für System-Gesundheitscheck.""" - if not current_user.is_admin: - return jsonify({"error": "Berechtigung verweigert"}), 403 - - db_session = get_db_session() - critical_errors = [] - warnings = [] - - try: - # 1. Datenbank-Schema-Integrität prüfen - try: - # Test verschiedene kritische Tabellen und Spalten - db_session.execute(text("SELECT COUNT(*) FROM guest_requests WHERE duration_minutes IS NOT NULL")) - schema_integrity = "OK" - except Exception as e: - critical_errors.append({ - "type": "database_schema", - "message": f"Datenbank-Schema-Fehler: {str(e)}", - "severity": "critical", - "suggested_fix": "Datenbank-Migration ausführen", - "timestamp": datetime.now().isoformat() - }) - schema_integrity = "FEHLER" - - # 2. Prüfe kritische Spalten in wichtigen Tabellen - schema_checks = [ - ("guest_requests", "duration_minutes"), - ("guest_requests", "file_name"), - ("guest_requests", "processed_by"), - ("users", "updated_at"), - ("jobs", "duration_minutes") - ] - - missing_columns = [] - for table, column in schema_checks: - try: - db_session.execute(text(f"SELECT {column} FROM {table} LIMIT 1")) - except Exception: - missing_columns.append(f"{table}.{column}") - - if missing_columns: - critical_errors.append({ - "type": "missing_columns", - "message": f"Fehlende Datenbank-Spalten: {', '.join(missing_columns)}", - "severity": "critical", - "suggested_fix": "python utils/database_schema_migration.py ausführen", - "timestamp": datetime.now().isoformat(), - "details": missing_columns - }) - - # 3. Prüfe auf wiederkehrende Datenbankfehler in den Logs - import os - log_file = os.path.join("logs", "app", f"myp_app_{datetime.now().strftime('%Y_%m_%d')}.log") - recent_db_errors = 0 - - if os.path.exists(log_file): - try: - with open(log_file, 'r', encoding='utf-8') as f: - last_lines = f.readlines()[-100:] # Letzte 100 Zeilen - for line in last_lines: - if "OperationalError" in line or "no such column" in line: - recent_db_errors += 1 - except Exception: - pass - - if recent_db_errors > 5: - critical_errors.append({ - "type": "frequent_db_errors", - "message": f"{recent_db_errors} Datenbankfehler in letzter Zeit erkannt", - "severity": "high", - "suggested_fix": "System-Logs überprüfen und Migration ausführen", - "timestamp": datetime.now().isoformat() - }) - - # 4. Prüfe Drucker-Konnektivität - offline_printers = db_session.query(Printer).filter( - Printer.status == "offline", - Printer.active == True - ).count() - - if offline_printers > 0: - warnings.append({ - "type": "printer_offline", - "message": f"{offline_printers} aktive Drucker sind offline", - "severity": "warning", - "suggested_fix": "Drucker-Status überprüfen", - "timestamp": datetime.now().isoformat() - }) - - # 5. System-Performance Metriken - import psutil - cpu_usage = psutil.cpu_percent(interval=1) - memory_usage = psutil.virtual_memory().percent - disk_usage = psutil.disk_usage('/').percent - - if cpu_usage > 90: - warnings.append({ - "type": "high_cpu", - "message": f"Hohe CPU-Auslastung: {cpu_usage:.1f}%", - "severity": "warning", - "suggested_fix": "System-Ressourcen überprüfen", - "timestamp": datetime.now().isoformat() - }) - - if memory_usage > 85: - warnings.append({ - "type": "high_memory", - "message": f"Hohe Speicher-Auslastung: {memory_usage:.1f}%", - "severity": "warning", - "suggested_fix": "Speicher-Verbrauch optimieren", - "timestamp": datetime.now().isoformat() - }) - - # 6. Letzte Migration info - try: - backup_dir = os.path.join("database", "backups") - if os.path.exists(backup_dir): - backup_files = [f for f in os.listdir(backup_dir) if f.endswith('.backup')] - if backup_files: - latest_backup = max(backup_files, key=lambda x: os.path.getctime(os.path.join(backup_dir, x))) - last_migration = latest_backup.replace('.backup', '').replace('myp.db.backup_', '') - else: - last_migration = "Keine Backups gefunden" - else: - last_migration = "Backup-Verzeichnis nicht gefunden" - except Exception: - last_migration = "Unbekannt" - - return jsonify({ - "success": True, - "health_status": "critical" if critical_errors else ("warning" if warnings else "healthy"), - "critical_errors": critical_errors, - "warnings": warnings, - "schema_integrity": schema_integrity, - "last_migration": last_migration, - "recent_errors_count": recent_db_errors, - "system_metrics": { - "cpu_usage": cpu_usage, - "memory_usage": memory_usage, - "disk_usage": disk_usage - }, - "timestamp": datetime.now().isoformat() - }) - - except Exception as e: - app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim System-Gesundheitscheck", - "critical_errors": [{ - "type": "system_check_failed", - "message": f"System-Check fehlgeschlagen: {str(e)}", - "severity": "critical", - "suggested_fix": "System-Logs überprüfen", - "timestamp": datetime.now().isoformat() - }] - }), 500 - finally: - db_session.close() - -@app.route("/api/admin/fix-errors", methods=['POST']) -@login_required -@csrf.exempt -def api_admin_fix_errors(): - """API-Endpunkt um automatische Fehler-Reparatur auszuführen.""" - if not current_user.is_admin: - return jsonify({"error": "Berechtigung verweigert"}), 403 - - try: - # Automatische Migration ausführen - import subprocess - import sys - - # Migration in separatem Prozess ausführen - result = subprocess.run( - [sys.executable, "utils/database_schema_migration.py"], - cwd=os.path.dirname(os.path.abspath(__file__)), - capture_output=True, - text=True, - timeout=60 - ) - - if result.returncode == 0: - app_logger.info(f"Automatische Migration erfolgreich ausgeführt von Admin {current_user.email}") - return jsonify({ - "success": True, - "message": "Automatische Reparatur erfolgreich durchgeführt", - "details": result.stdout - }) - else: - app_logger.error(f"Automatische Migration fehlgeschlagen: {result.stderr}") - return jsonify({ - "success": False, - "error": "Automatische Reparatur fehlgeschlagen", - "details": result.stderr - }), 500 - - except subprocess.TimeoutExpired: - return jsonify({ - "success": False, - "error": "Migration-Timeout - Vorgang dauerte zu lange" - }), 500 - except Exception as e: - app_logger.error(f"Fehler bei automatischer Reparatur: {str(e)}") - return jsonify({ - "success": False, - "error": f"Fehler bei automatischer Reparatur: {str(e)}" - }), 500 - -# Direkter Zugriff auf Logout-Route (für Fallback) -@app.route("/logout", methods=["GET", "POST"]) -def logout_redirect(): - """Leitet zur Blueprint-Logout-Route weiter.""" - return redirect(url_for("auth_logout")) - -# ===== JOB-ROUTEN ===== - -@app.route("/api/jobs", methods=["GET"]) -@login_required -def get_jobs(): - db_session = get_db_session() - - try: - # Import joinedload for eager loading - from sqlalchemy.orm import joinedload - - # Admin sieht alle Jobs, User nur eigene - if current_user.is_admin: - # Eagerly load the user and printer relationships to avoid detached instance errors - jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).all() - else: - jobs = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.user_id == int(current_user.id)).all() - - # Convert jobs to dictionaries before closing the session - job_dicts = [job.to_dict() for job in jobs] - - db_session.close() - - return jsonify({ - "jobs": job_dicts - }) - except Exception as e: - jobs_logger.error(f"Fehler beim Abrufen von Jobs: {str(e)}") - db_session.close() - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/jobs/", methods=["GET"]) -@login_required -@job_owner_required -def get_job(job_id): - db_session = get_db_session() - - try: - from sqlalchemy.orm import joinedload - # Eagerly load the user and printer relationships - job = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.id == job_id).first() - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - # Convert to dict before closing session - job_dict = job.to_dict() - db_session.close() - - return jsonify(job_dict) - except Exception as e: - jobs_logger.error(f"Fehler beim Abrufen des Jobs {job_id}: {str(e)}") - db_session.close() - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route('/api/jobs/check-waiting', methods=['POST']) -@login_required -def check_waiting_jobs(): - """Überprüft wartende Jobs und startet sie, wenn Drucker online gehen.""" - try: - db_session = get_db_session() - - # Alle wartenden Jobs finden - waiting_jobs = db_session.query(Job).filter( - Job.status == "waiting_for_printer" - ).all() - - if not waiting_jobs: - db_session.close() - return jsonify({ - "message": "Keine wartenden Jobs gefunden", - "updated_jobs": [] - }) - - updated_jobs = [] - - for job in waiting_jobs: - # Drucker-Status prüfen - printer = db_session.query(Printer).get(job.printer_id) - if printer and printer.plug_ip: - status, active = check_printer_status(printer.plug_ip) - - if status == "online" and active: - # Drucker ist jetzt online - Job kann geplant werden - job.status = "scheduled" - updated_jobs.append({ - "id": job.id, - "name": job.name, - "printer_name": printer.name, - "status": "scheduled" - }) - - jobs_logger.info(f"Job {job.id} von 'waiting_for_printer' zu 'scheduled' geändert - Drucker {printer.name} ist online") - - if updated_jobs: - db_session.commit() - - db_session.close() - - return jsonify({ - "message": f"{len(updated_jobs)} Jobs aktualisiert", - "updated_jobs": updated_jobs - }) - - except Exception as e: - jobs_logger.error(f"Fehler beim Überprüfen wartender Jobs: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route('/api/jobs/active', methods=['GET']) -@login_required -def get_active_jobs(): - """ - Gibt alle aktiven Jobs zurück. - """ - try: - db_session = get_db_session() - from sqlalchemy.orm import joinedload - - active_jobs = db_session.query(Job).options( - joinedload(Job.user), - joinedload(Job.printer) - ).filter( - Job.status.in_(["scheduled", "running"]) - ).all() - - result = [] - for job in active_jobs: - job_dict = job.to_dict() - # Aktuelle Restzeit berechnen - if job.status == "running" and job.end_at: - remaining_time = job.end_at - datetime.now() - if remaining_time.total_seconds() > 0: - job_dict["remaining_minutes"] = int(remaining_time.total_seconds() / 60) - else: - job_dict["remaining_minutes"] = 0 - - result.append(job_dict) - - db_session.close() - return jsonify({"jobs": result}) - except Exception as e: - jobs_logger.error(f"Fehler beim Abrufen aktiver Jobs: {str(e)}") - return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 - -@app.route('/api/jobs', methods=['POST']) -@login_required -@measure_execution_time(logger=jobs_logger, task_name="API-Job-Erstellung") -def create_job(): - """ - Erstellt einen neuen Job mit dem Status "scheduled". - - Body: { - "printer_id": int, - "start_iso": str, # ISO-Datum-String - "duration_minutes": int - } - """ - try: - data = request.json - - # Pflichtfelder prüfen - required_fields = ["printer_id", "start_iso", "duration_minutes"] - for field in required_fields: - if field not in data: - return jsonify({"error": f"Feld '{field}' fehlt"}), 400 - - # Daten extrahieren und validieren - printer_id = int(data["printer_id"]) - start_iso = data["start_iso"] - duration_minutes = int(data["duration_minutes"]) - - # Optional: Jobtitel und Dateipfad - name = data.get("name", f"Druckjob vom {datetime.now().strftime('%d.%m.%Y')}") - file_path = data.get("file_path") - - # Start-Zeit parsen - try: - start_at = datetime.fromisoformat(start_iso) - except ValueError: - return jsonify({"error": "Ungültiges Startdatum"}), 400 - - # Dauer validieren - if duration_minutes <= 0: - return jsonify({"error": "Dauer muss größer als 0 sein"}), 400 - - # End-Zeit berechnen - end_at = start_at + timedelta(minutes=duration_minutes) - - db_session = get_db_session() - - # Prüfen, ob der Drucker existiert - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Prüfen, ob der Drucker online ist - printer_status, printer_active = check_printer_status(printer.plug_ip if printer.plug_ip else "") - - # Status basierend auf Drucker-Verfügbarkeit setzen - if printer_status == "online" and printer_active: - job_status = "scheduled" - else: - job_status = "waiting_for_printer" - - # Neuen Job erstellen - new_job = Job( - name=name, - printer_id=printer_id, - user_id=current_user.id, - owner_id=current_user.id, - start_at=start_at, - end_at=end_at, - status=job_status, - file_path=file_path, - duration_minutes=duration_minutes - ) - - db_session.add(new_job) - db_session.commit() - - # Job-Objekt für die Antwort serialisieren - job_dict = new_job.to_dict() - db_session.close() - - jobs_logger.info(f"Neuer Job {new_job.id} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten") - return jsonify({"job": job_dict}), 201 - - except Exception as e: - jobs_logger.error(f"Fehler beim Erstellen eines Jobs: {str(e)}") - return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 - -@app.route('/api/jobs//extend', methods=['POST']) -@login_required -@job_owner_required -def extend_job(job_id): - """ - Verlängert die Endzeit eines Jobs. - - Body: { - "extra_minutes": int - } - """ - try: - data = request.json - - # Prüfen, ob die erforderlichen Daten vorhanden sind - if "extra_minutes" not in data: - return jsonify({"error": "Feld 'extra_minutes' fehlt"}), 400 - - extra_minutes = int(data["extra_minutes"]) - - # Validieren - if extra_minutes <= 0: - return jsonify({"error": "Zusätzliche Minuten müssen größer als 0 sein"}), 400 - - db_session = get_db_session() - job = db_session.query(Job).get(job_id) - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - # Prüfen, ob der Job verlängert werden kann - if job.status not in ["scheduled", "running"]: - db_session.close() - return jsonify({"error": f"Job kann im Status '{job.status}' nicht verlängert werden"}), 400 - - # Endzeit aktualisieren - job.end_at = job.end_at + timedelta(minutes=extra_minutes) - job.duration_minutes += extra_minutes - - db_session.commit() - - # Job-Objekt für die Antwort serialisieren - job_dict = job.to_dict() - db_session.close() - - jobs_logger.info(f"Job {job_id} um {extra_minutes} Minuten verlängert, neue Endzeit: {job.end_at}") - return jsonify({"job": job_dict}) - - except Exception as e: - jobs_logger.error(f"Fehler beim Verlängern von Job {job_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 - -@app.route('/api/jobs//finish', methods=['POST']) -@login_required -def finish_job(job_id): - """ - Beendet einen Job manuell und schaltet die Steckdose aus. - Nur für Administratoren erlaubt. - """ - try: - # Prüfen, ob der Benutzer Administrator ist - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Jobs manuell beenden"}), 403 - - db_session = get_db_session() - job = db_session.query(Job).options(joinedload(Job.printer)).get(job_id) - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - # Prüfen, ob der Job beendet werden kann - if job.status not in ["scheduled", "running"]: - db_session.close() - return jsonify({"error": f"Job kann im Status '{job.status}' nicht beendet werden"}), 400 - - # Steckdose ausschalten - from utils.job_scheduler import toggle_plug - if not toggle_plug(job.printer_id, False): - # Trotzdem weitermachen, aber Warnung loggen - jobs_logger.warning(f"Steckdose für Job {job_id} konnte nicht ausgeschaltet werden") - - # Job als beendet markieren - job.status = "finished" - job.actual_end_time = datetime.now() - - db_session.commit() - - # Job-Objekt für die Antwort serialisieren - job_dict = job.to_dict() - db_session.close() - - jobs_logger.info(f"Job {job_id} manuell beendet durch Admin {current_user.id}") - return jsonify({"job": job_dict}) - - except Exception as e: - jobs_logger.error(f"Fehler beim manuellen Beenden von Job {job_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500 - -# ===== DRUCKER-ROUTEN ===== - -@app.route("/api/printers", methods=["GET"]) -@login_required -def get_printers(): - """Gibt alle Drucker zurück - OHNE Status-Check für schnelleres Laden.""" - db_session = get_db_session() - - try: - # Windows-kompatible Timeout-Implementierung - import threading - import time - - printers = None - timeout_occurred = False - - def fetch_printers(): - nonlocal printers, timeout_occurred - try: - printers = db_session.query(Printer).all() - except Exception as e: - printers_logger.error(f"Datenbankfehler beim Laden der Drucker: {str(e)}") - timeout_occurred = True - - # Starte Datenbankabfrage in separatem Thread - thread = threading.Thread(target=fetch_printers) - thread.daemon = True - thread.start() - thread.join(timeout=5) # 5 Sekunden Timeout - - if thread.is_alive() or timeout_occurred or printers is None: - printers_logger.warning("Database timeout when fetching printers for basic loading") - return jsonify({ - 'error': 'Database timeout beim Laden der Drucker', - 'timeout': True, - 'printers': [] - }), 408 - - # Drucker-Daten OHNE Status-Check zusammenstellen für schnelles Laden - printer_data = [] - current_time = datetime.now() - - for printer in printers: - printer_data.append({ - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status or "offline", # Letzter bekannter Status - "active": printer.active if hasattr(printer, 'active') else True, - "ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None), - "created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(), - "last_checked": printer.last_checked.isoformat() if hasattr(printer, 'last_checked') and printer.last_checked else None - }) - - db_session.close() - - printers_logger.info(f"Schnelles Laden abgeschlossen: {len(printer_data)} Drucker geladen (ohne Status-Check)") - - return jsonify({ - "printers": printer_data, - "count": len(printer_data), - "message": "Drucker erfolgreich geladen" - }) - - except Exception as e: - db_session.rollback() - db_session.close() - printers_logger.error(f"Fehler beim Abrufen der Drucker: {str(e)}") - return jsonify({ - "error": f"Fehler beim Laden der Drucker: {str(e)}", - "printers": [] - }), 500 - -@app.route("/api/printers/status", methods=["GET"]) -@login_required -@measure_execution_time(logger=printers_logger, task_name="API-Drucker-Status-Abfrage") -def get_printers_with_status(): - """Gibt alle Drucker MIT aktuellem Status-Check zurück - für Aktualisierung.""" - db_session = get_db_session() - - try: - # Windows-kompatible Timeout-Implementierung - import threading - import time - - printers = None - timeout_occurred = False - - def fetch_printers(): - nonlocal printers, timeout_occurred - try: - printers = db_session.query(Printer).all() - except Exception as e: - printers_logger.error(f"Datenbankfehler beim Status-Check: {str(e)}") - timeout_occurred = True - - # Starte Datenbankabfrage in separatem Thread - thread = threading.Thread(target=fetch_printers) - thread.daemon = True - thread.start() - thread.join(timeout=8) # 8 Sekunden Timeout für Status-Check - - if thread.is_alive() or timeout_occurred or printers is None: - printers_logger.warning("Database timeout when fetching printers for status check") - return jsonify({ - 'error': 'Database timeout beim Status-Check der Drucker', - 'timeout': True - }), 408 - - # Drucker-Daten für Status-Check vorbereiten - printer_data = [] - for printer in printers: - # Verwende plug_ip als primäre IP-Adresse, fallback auf ip_address - ip_to_check = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None) - printer_data.append({ - 'id': printer.id, - 'name': printer.name, - 'ip_address': ip_to_check, - 'location': printer.location, - 'model': printer.model - }) - - # Status aller Drucker parallel überprüfen mit 7-Sekunden-Timeout - printers_logger.info(f"Starte Status-Check für {len(printer_data)} Drucker mit 7-Sekunden-Timeout") - - # Fallback: Wenn keine IP-Adressen vorhanden sind, alle als offline markieren - if not any(p['ip_address'] for p in printer_data): - printers_logger.warning("Keine IP-Adressen für Drucker gefunden - alle als offline markiert") - status_results = {p['id']: ("offline", False) for p in printer_data} - else: - try: - status_results = check_multiple_printers_status(printer_data, timeout=7) - except Exception as e: - printers_logger.error(f"Fehler beim Status-Check: {str(e)}") - # Fallback: alle als offline markieren - status_results = {p['id']: ("offline", False) for p in printer_data} - - # Ergebnisse zusammenstellen und Datenbank aktualisieren - status_data = [] - current_time = datetime.now() - - for printer in printers: - if printer.id in status_results: - status, active = status_results[printer.id] - # Mapping für Frontend-Kompatibilität - if status == "online": - frontend_status = "available" - else: - frontend_status = "offline" - else: - # Fallback falls kein Ergebnis vorliegt - frontend_status = "offline" - active = False - - # Status in der Datenbank aktualisieren - printer.status = frontend_status - printer.active = active - - # Setze last_checked falls das Feld existiert - if hasattr(printer, 'last_checked'): - printer.last_checked = current_time - - status_data.append({ - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": frontend_status, - "active": active, - "ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None), - "created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(), - "last_checked": current_time.isoformat() - }) - - # Speichere die aktualisierten Status - try: - db_session.commit() - printers_logger.info("Drucker-Status erfolgreich in Datenbank aktualisiert") - except Exception as e: - printers_logger.warning(f"Fehler beim Speichern der Status-Updates: {str(e)}") - # Nicht kritisch, Status-Check kann trotzdem zurückgegeben werden - - db_session.close() - - online_count = len([s for s in status_data if s['status'] == 'available']) - printers_logger.info(f"Status-Check abgeschlossen: {online_count} von {len(status_data)} Drucker online") - - return jsonify(status_data) - - except Exception as e: - db_session.rollback() - db_session.close() - printers_logger.error(f"Fehler beim Status-Check der Drucker: {str(e)}") - return jsonify({ - "error": f"Fehler beim Status-Check: {str(e)}", - "printers": [] - }), 500 - -@app.route("/api/jobs/current", methods=["GET"]) -@login_required -def get_current_job(): - """Gibt den aktuellen Job des Benutzers zurück.""" - db_session = get_db_session() - try: - current_job = db_session.query(Job).filter( - Job.user_id == int(current_user.id), - Job.status.in_(["scheduled", "running"]) - ).order_by(Job.start_at).first() - - if current_job: - job_data = current_job.to_dict() - else: - job_data = None - - db_session.close() - return jsonify(job_data) - except Exception as e: - db_session.close() - return jsonify({"error": str(e)}), 500 - -# ===== WEITERE API-ROUTEN ===== - -@app.route("/api/printers/", methods=["GET"]) -@login_required -def get_printer(printer_id): - """Gibt einen spezifischen Drucker zurück.""" - db_session = get_db_session() - - try: - printer = db_session.query(Printer).get(printer_id) - - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Status-Check für diesen Drucker - ip_to_check = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None) - if ip_to_check: - status, active = check_printer_status(ip_to_check) - printer.status = "available" if status == "online" else "offline" - printer.active = active - db_session.commit() - - printer_data = { - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status or "offline", - "active": printer.active if hasattr(printer, 'active') else True, - "ip_address": ip_to_check, - "created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat() - } - - db_session.close() - return jsonify(printer_data) - - except Exception as e: - db_session.close() - printers_logger.error(f"Fehler beim Abrufen des Druckers {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/printers", methods=["POST"]) -@login_required -def create_printer(): - """Erstellt einen neuen Drucker (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Drucker erstellen"}), 403 - - try: - data = request.json - - # Pflichtfelder prüfen - required_fields = ["name", "plug_ip"] - for field in required_fields: - if field not in data: - return jsonify({"error": f"Feld '{field}' fehlt"}), 400 - - db_session = get_db_session() - - # Prüfen, ob bereits ein Drucker mit diesem Namen existiert - existing_printer = db_session.query(Printer).filter(Printer.name == data["name"]).first() - if existing_printer: - db_session.close() - return jsonify({"error": "Ein Drucker mit diesem Namen existiert bereits"}), 400 - - # Neuen Drucker erstellen - new_printer = Printer( - name=data["name"], - model=data.get("model", ""), - location=data.get("location", ""), - mac_address=data.get("mac_address", ""), - plug_ip=data["plug_ip"], - status="offline", - active=True, # Neue Drucker sind standardmäßig aktiv - created_at=datetime.now() - ) - - db_session.add(new_printer) - db_session.commit() - - # Sofortiger Status-Check für den neuen Drucker - ip_to_check = new_printer.plug_ip - if ip_to_check: - status, active = check_printer_status(ip_to_check) - new_printer.status = "available" if status == "online" else "offline" - new_printer.active = active - db_session.commit() - - printer_data = { - "id": new_printer.id, - "name": new_printer.name, - "model": new_printer.model, - "location": new_printer.location, - "mac_address": new_printer.mac_address, - "plug_ip": new_printer.plug_ip, - "status": new_printer.status, - "active": new_printer.active, - "created_at": new_printer.created_at.isoformat() - } - - db_session.close() - - printers_logger.info(f"Neuer Drucker '{new_printer.name}' erstellt von Admin {current_user.id}") - return jsonify({"printer": printer_data, "message": "Drucker erfolgreich erstellt"}), 201 - - except Exception as e: - printers_logger.error(f"Fehler beim Erstellen eines Druckers: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/printers/add", methods=["POST"]) -@login_required -def add_printer(): - """Alternativer Endpunkt zum Hinzufügen von Druckern (für Frontend-Kompatibilität).""" - return create_printer() - -@app.route("/api/printers/", methods=["PUT"]) -@login_required -def update_printer(printer_id): - """Aktualisiert einen Drucker (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Drucker bearbeiten"}), 403 - - try: - data = request.json - db_session = get_db_session() - - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Aktualisierbare Felder - updatable_fields = ["name", "model", "location", "mac_address", "plug_ip"] - for field in updatable_fields: - if field in data: - setattr(printer, field, data[field]) - - db_session.commit() - - printer_data = { - "id": printer.id, - "name": printer.name, - "model": printer.model, - "location": printer.location, - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status, - "created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat() - } - - db_session.close() - - printers_logger.info(f"Drucker {printer_id} aktualisiert von Admin {current_user.id}") - return jsonify({"printer": printer_data}) - - except Exception as e: - printers_logger.error(f"Fehler beim Aktualisieren des Druckers {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/printers/", methods=["DELETE"]) -@login_required -def delete_printer(printer_id): - """Löscht einen Drucker (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Drucker löschen"}), 403 - - try: - db_session = get_db_session() - - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Prüfen, ob noch aktive Jobs für diesen Drucker existieren - active_jobs = db_session.query(Job).filter( - Job.printer_id == printer_id, - Job.status.in_(["scheduled", "running"]) - ).count() - - if active_jobs > 0: - db_session.close() - return jsonify({"error": f"Drucker kann nicht gelöscht werden: {active_jobs} aktive Jobs vorhanden"}), 400 - - printer_name = printer.name - db_session.delete(printer) - db_session.commit() - db_session.close() - - printers_logger.info(f"Drucker '{printer_name}' (ID: {printer_id}) gelöscht von Admin {current_user.id}") - return jsonify({"message": "Drucker erfolgreich gelöscht"}) - - except Exception as e: - printers_logger.error(f"Fehler beim Löschen des Druckers {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/jobs/", methods=["DELETE"]) -@login_required -@job_owner_required -def delete_job(job_id): - """Löscht einen Job.""" - try: - db_session = get_db_session() - job = db_session.query(Job).get(job_id) - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - # Prüfen, ob der Job gelöscht werden kann - if job.status == "running": - db_session.close() - return jsonify({"error": "Laufende Jobs können nicht gelöscht werden"}), 400 - - job_name = job.name - db_session.delete(job) - db_session.commit() - db_session.close() - - jobs_logger.info(f"Job '{job_name}' (ID: {job_id}) gelöscht von Benutzer {current_user.id}") - return jsonify({"message": "Job erfolgreich gelöscht"}) - - except Exception as e: - jobs_logger.error(f"Fehler beim Löschen des Jobs {job_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/jobs//cancel", methods=["POST"]) -@login_required -@job_owner_required -def cancel_job(job_id): - """Bricht einen Job ab.""" - try: - db_session = get_db_session() - job = db_session.query(Job).get(job_id) - - if not job: - db_session.close() - return jsonify({"error": "Job nicht gefunden"}), 404 - - # Prüfen, ob der Job abgebrochen werden kann - if job.status not in ["scheduled", "running"]: - db_session.close() - return jsonify({"error": f"Job kann im Status '{job.status}' nicht abgebrochen werden"}), 400 - - # Job als abgebrochen markieren - job.status = "cancelled" - job.actual_end_time = datetime.now() - - # Wenn der Job läuft, Steckdose ausschalten - if job.status == "running": - from utils.job_scheduler import toggle_plug - toggle_plug(job.printer_id, False) - - db_session.commit() - - job_dict = job.to_dict() - db_session.close() - - jobs_logger.info(f"Job {job_id} abgebrochen von Benutzer {current_user.id}") - return jsonify({"job": job_dict}) - - except Exception as e: - jobs_logger.error(f"Fehler beim Abbrechen des Jobs {job_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats", methods=["GET"]) -@login_required -def get_stats(): - """Gibt Statistiken zurück.""" - try: - db_session = get_db_session() - - # Grundlegende Statistiken - total_users = db_session.query(User).count() - total_printers = db_session.query(Printer).count() - total_jobs = db_session.query(Job).count() - - # Jobs nach Status - completed_jobs = db_session.query(Job).filter(Job.status == "completed").count() - failed_jobs = db_session.query(Job).filter(Job.status == "failed").count() - cancelled_jobs = db_session.query(Job).filter(Job.status == "cancelled").count() - active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count() - - # Online-Drucker - online_printers = db_session.query(Printer).filter(Printer.status == "available").count() - - # Erfolgsrate - finished_jobs = completed_jobs + failed_jobs + cancelled_jobs - success_rate = (completed_jobs / finished_jobs * 100) if finished_jobs > 0 else 0 - - # Benutzer-spezifische Statistiken (falls nicht Admin) - user_stats = {} - if not current_user.is_admin: - user_jobs = db_session.query(Job).filter(Job.user_id == int(current_user.id)).count() - user_completed = db_session.query(Job).filter( - Job.user_id == int(current_user.id), - Job.status == "completed" - ).count() - user_stats = { - "total_jobs": user_jobs, - "completed_jobs": user_completed, - "success_rate": (user_completed / user_jobs * 100) if user_jobs > 0 else 0 - } - - db_session.close() - - stats = { - "total_users": total_users, - "total_printers": total_printers, - "online_printers": online_printers, - "total_jobs": total_jobs, - "completed_jobs": completed_jobs, - "failed_jobs": failed_jobs, - "cancelled_jobs": cancelled_jobs, - "active_jobs": active_jobs, - "success_rate": round(success_rate, 1), - "user_stats": user_stats - } - - return jsonify(stats) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats/charts/job-status", methods=["GET"]) -@login_required -def get_job_status_chart_data(): - """Gibt Diagrammdaten für Job-Status-Verteilung zurück.""" - try: - db_session = get_db_session() - - # Job-Status zählen - job_status_counts = { - 'completed': db_session.query(Job).filter(Job.status == 'completed').count(), - 'failed': db_session.query(Job).filter(Job.status == 'failed').count(), - 'cancelled': db_session.query(Job).filter(Job.status == 'cancelled').count(), - 'running': db_session.query(Job).filter(Job.status == 'running').count(), - 'scheduled': db_session.query(Job).filter(Job.status == 'scheduled').count() - } - - db_session.close() - - chart_data = { - 'labels': ['Abgeschlossen', 'Fehlgeschlagen', 'Abgebrochen', 'Läuft', 'Geplant'], - 'datasets': [{ - 'label': 'Anzahl Jobs', - 'data': [ - job_status_counts['completed'], - job_status_counts['failed'], - job_status_counts['cancelled'], - job_status_counts['running'], - job_status_counts['scheduled'] - ], - 'backgroundColor': [ - '#10b981', # Grün für abgeschlossen - '#ef4444', # Rot für fehlgeschlagen - '#6b7280', # Grau für abgebrochen - '#3b82f6', # Blau für läuft - '#f59e0b' # Orange für geplant - ] - }] - } - - return jsonify(chart_data) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Job-Status-Diagrammdaten: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats/charts/printer-usage", methods=["GET"]) -@login_required -def get_printer_usage_chart_data(): - """Gibt Diagrammdaten für Drucker-Nutzung zurück.""" - try: - db_session = get_db_session() - - # Drucker mit Job-Anzahl - printer_usage = db_session.query( - Printer.name, - func.count(Job.id).label('job_count') - ).outerjoin(Job).group_by(Printer.id, Printer.name).all() - - db_session.close() - - chart_data = { - 'labels': [usage[0] for usage in printer_usage], - 'datasets': [{ - 'label': 'Anzahl Jobs', - 'data': [usage[1] for usage in printer_usage], - 'backgroundColor': '#3b82f6', - 'borderColor': '#1d4ed8', - 'borderWidth': 1 - }] - } - - return jsonify(chart_data) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Drucker-Nutzung-Diagrammdaten: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats/charts/jobs-timeline", methods=["GET"]) -@login_required -def get_jobs_timeline_chart_data(): - """Gibt Diagrammdaten für Jobs-Timeline der letzten 30 Tage zurück.""" - try: - db_session = get_db_session() - - # Letzte 30 Tage - end_date = datetime.now().date() - start_date = end_date - timedelta(days=30) - - # Jobs pro Tag der letzten 30 Tage - daily_jobs = db_session.query( - func.date(Job.created_at).label('date'), - func.count(Job.id).label('count') - ).filter( - func.date(Job.created_at) >= start_date, - func.date(Job.created_at) <= end_date - ).group_by(func.date(Job.created_at)).all() - - # Alle Tage füllen (auch ohne Jobs) - date_dict = {job_date: count for job_date, count in daily_jobs} - - labels = [] - data = [] - current_date = start_date - - while current_date <= end_date: - labels.append(current_date.strftime('%d.%m')) - data.append(date_dict.get(current_date, 0)) - current_date += timedelta(days=1) - - db_session.close() - - chart_data = { - 'labels': labels, - 'datasets': [{ - 'label': 'Jobs pro Tag', - 'data': data, - 'fill': True, - 'backgroundColor': 'rgba(59, 130, 246, 0.1)', - 'borderColor': '#3b82f6', - 'tension': 0.4 - }] - } - - return jsonify(chart_data) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Jobs-Timeline-Diagrammdaten: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats/charts/user-activity", methods=["GET"]) -@login_required -def get_user_activity_chart_data(): - """Gibt Diagrammdaten für Top-Benutzer-Aktivität zurück.""" - try: - db_session = get_db_session() - - # Top 10 Benutzer nach Job-Anzahl - top_users = db_session.query( - User.username, - func.count(Job.id).label('job_count') - ).join(Job).group_by( - User.id, User.username - ).order_by( - func.count(Job.id).desc() - ).limit(10).all() - - db_session.close() - - chart_data = { - 'labels': [user[0] for user in top_users], - 'datasets': [{ - 'label': 'Anzahl Jobs', - 'data': [user[1] for user in top_users], - 'backgroundColor': '#8b5cf6', - 'borderColor': '#7c3aed', - 'borderWidth': 1 - }] - } - - return jsonify(chart_data) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Benutzer-Aktivität-Diagrammdaten: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/stats/export", methods=["GET"]) -@login_required -def export_stats(): - """Exportiert Statistiken als CSV.""" - try: - db_session = get_db_session() - - # Basis-Statistiken sammeln - total_users = db_session.query(User).count() - total_printers = db_session.query(Printer).count() - total_jobs = db_session.query(Job).count() - completed_jobs = db_session.query(Job).filter(Job.status == "completed").count() - failed_jobs = db_session.query(Job).filter(Job.status == "failed").count() - - # CSV-Inhalt erstellen - import io - import csv - - output = io.StringIO() - writer = csv.writer(output) - - # Header - writer.writerow(['Metrik', 'Wert']) - - # Daten - writer.writerow(['Gesamte Benutzer', total_users]) - writer.writerow(['Gesamte Drucker', total_printers]) - writer.writerow(['Gesamte Jobs', total_jobs]) - writer.writerow(['Abgeschlossene Jobs', completed_jobs]) - writer.writerow(['Fehlgeschlagene Jobs', failed_jobs]) - writer.writerow(['Erfolgsrate (%)', round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0]) - writer.writerow(['Exportiert am', datetime.now().strftime('%d.%m.%Y %H:%M:%S')]) - - db_session.close() - - # Response vorbereiten - output.seek(0) - - response = Response( - output.getvalue(), - mimetype='text/csv', - headers={ - 'Content-Disposition': f'attachment; filename=statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' - } - ) - - return response - - except Exception as e: - app_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/admin/users", methods=["GET"]) -@login_required -def get_users(): - """Gibt alle Benutzer zurück (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Benutzer anzeigen"}), 403 - - try: - db_session = get_db_session() - users = db_session.query(User).all() - - user_data = [] - for user in users: - user_data.append({ - "id": user.id, - "username": user.username, - "email": user.email, - "first_name": user.first_name, - "last_name": user.last_name, - "is_admin": user.is_admin, - "created_at": user.created_at.isoformat() if user.created_at else None, - "last_login": user.last_login.isoformat() if hasattr(user, 'last_login') and user.last_login else None - }) - - db_session.close() - return jsonify({"users": user_data}) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Benutzer: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/admin/users/", methods=["PUT"]) -@login_required -def update_user(user_id): - """Aktualisiert einen Benutzer (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Benutzer bearbeiten"}), 403 - - try: - data = request.json - db_session = get_db_session() - - user = db_session.get(User, user_id) - if not user: - db_session.close() - return jsonify({"error": "Benutzer nicht gefunden"}), 404 - - # Aktualisierbare Felder - updatable_fields = ["username", "email", "first_name", "last_name", "is_admin"] - for field in updatable_fields: - if field in data: - setattr(user, field, data[field]) - - # Passwort separat behandeln - if "password" in data and data["password"]: - user.set_password(data["password"]) - - db_session.commit() - - user_data = { - "id": user.id, - "username": user.username, - "email": user.email, - "first_name": user.first_name, - "last_name": user.last_name, - "is_admin": user.is_admin, - "created_at": user.created_at.isoformat() if user.created_at else None - } - - db_session.close() - - user_logger.info(f"Benutzer {user_id} aktualisiert von Admin {current_user.id}") - return jsonify({"user": user_data}) - - except Exception as e: - user_logger.error(f"Fehler beim Aktualisieren des Benutzers {user_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/admin/users/", methods=["DELETE"]) -@login_required -def delete_user(user_id): - """Löscht einen Benutzer (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Benutzer löschen"}), 403 - - # Verhindern, dass sich der Admin selbst löscht - if user_id == current_user.id: - return jsonify({"error": "Sie können sich nicht selbst löschen"}), 400 - - try: - db_session = get_db_session() - - user = db_session.get(User, user_id) - if not user: - db_session.close() - return jsonify({"error": "Benutzer nicht gefunden"}), 404 - - # Prüfen, ob noch aktive Jobs für diesen Benutzer existieren - active_jobs = db_session.query(Job).filter( - Job.user_id == user_id, - Job.status.in_(["scheduled", "running"]) - ).count() - - if active_jobs > 0: - db_session.close() - return jsonify({"error": f"Benutzer kann nicht gelöscht werden: {active_jobs} aktive Jobs vorhanden"}), 400 - - username = user.username - db_session.delete(user) - db_session.commit() - db_session.close() - - user_logger.info(f"Benutzer '{username}' (ID: {user_id}) gelöscht von Admin {current_user.id}") - return jsonify({"message": "Benutzer erfolgreich gelöscht"}) - - except Exception as e: - user_logger.error(f"Fehler beim Löschen des Benutzers {user_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -# ===== FEHLERBEHANDLUNG ===== - -@app.errorhandler(404) -def not_found_error(error): - return render_template('errors/404.html'), 404 - -@app.errorhandler(500) -def internal_error(error): - return render_template('errors/500.html'), 500 - -@app.errorhandler(403) -def forbidden_error(error): - return render_template('errors/403.html'), 403 - -# ===== ADMIN - DATENBANK-VERWALTUNG ===== - -@app.route('/api/admin/database/stats', methods=['GET']) -@admin_required -def get_database_stats(): - """Gibt Datenbank-Statistiken zurück.""" - try: - if database_monitor is None: - return jsonify({ - "success": False, - "error": "Database Monitor nicht verfügbar" - }), 503 - - stats = database_monitor.get_database_stats() - return jsonify({ - "success": True, - "stats": stats - }) - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Datenbank-Statistiken: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/health', methods=['GET']) -@admin_required -def check_database_health(): - """Führt eine Datenbank-Gesundheitsprüfung durch.""" - try: - if database_monitor is None: - return jsonify({ - "success": False, - "error": "Database Monitor nicht verfügbar" - }), 503 - - health = database_monitor.check_database_health() - return jsonify({ - "success": True, - "health": health - }) - except Exception as e: - app_logger.error(f"Fehler bei Datenbank-Gesundheitsprüfung: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/optimize', methods=['POST']) -@admin_required -def optimize_database(): - """Führt Datenbank-Optimierung durch.""" - try: - if database_monitor is None: - return jsonify({ - "success": False, - "error": "Database Monitor nicht verfügbar" - }), 503 - - result = database_monitor.optimize_database() - return jsonify({ - "success": result["success"], - "result": result - }) - except Exception as e: - app_logger.error(f"Fehler bei Datenbank-Optimierung: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/backup', methods=['POST']) -@admin_required -def create_database_backup(): - """Erstellt ein manuelles Datenbank-Backup.""" - try: - if backup_manager is None: - return jsonify({ - "success": False, - "error": "Backup Manager nicht verfügbar" - }), 503 - - data = request.get_json() or {} - compress = data.get('compress', True) - - backup_path = backup_manager.create_backup(compress=compress) - - return jsonify({ - "success": True, - "backup_path": backup_path, - "message": "Backup erfolgreich erstellt" - }) - except Exception as e: - app_logger.error(f"Fehler beim Erstellen des Backups: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/backups', methods=['GET']) -@admin_required -def list_database_backups(): - """Listet alle verfügbaren Datenbank-Backups auf.""" - try: - if backup_manager is None: - return jsonify({ - "success": False, - "error": "Backup Manager nicht verfügbar" - }), 503 - - backups = backup_manager.get_backup_list() - - # Konvertiere datetime-Objekte zu Strings für JSON - for backup in backups: - backup['created'] = backup['created'].isoformat() - - return jsonify({ - "success": True, - "backups": backups - }) - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Backup-Liste: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/backup/restore', methods=['POST']) -@admin_required -def restore_database_backup(): - """Stellt ein Datenbank-Backup wieder her.""" - try: - if backup_manager is None: - return jsonify({ - "success": False, - "error": "Backup Manager nicht verfügbar" - }), 503 - - data = request.get_json() - if not data or 'backup_path' not in data: - return jsonify({ - "success": False, - "error": "Backup-Pfad erforderlich" - }), 400 - - backup_path = data['backup_path'] - - # Sicherheitsprüfung: Nur Backups aus dem Backup-Verzeichnis erlauben - if not backup_path.startswith(backup_manager.backup_dir): - return jsonify({ - "success": False, - "error": "Ungültiger Backup-Pfad" - }), 400 - - success = backup_manager.restore_backup(backup_path) - - if success: - return jsonify({ - "success": True, - "message": "Backup erfolgreich wiederhergestellt" - }) - else: - return jsonify({ - "success": False, - "error": "Fehler beim Wiederherstellen des Backups" - }), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Wiederherstellen des Backups: {str(e)}") - return jsonify({ - "success": False, - "error": str(e) - }), 500 - -@app.route('/api/admin/database/backup/cleanup', methods=['POST']) -@admin_required -def cleanup_old_backups(): - """Löscht alte Datenbank-Backups.""" - try: - backup_dir = os.path.join(os.path.dirname(__file__), 'database', 'backups') - if not os.path.exists(backup_dir): - return jsonify({"error": "Backup-Verzeichnis nicht gefunden"}), 404 - - # Backups älter als 30 Tage löschen - cutoff_date = datetime.now() - timedelta(days=30) - deleted_count = 0 - - for filename in os.listdir(backup_dir): - if filename.endswith('.sql'): - file_path = os.path.join(backup_dir, filename) - file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path)) - - if file_mtime < cutoff_date: - os.remove(file_path) - deleted_count += 1 - - return jsonify({ - "message": f"{deleted_count} alte Backups gelöscht", - "deleted_count": deleted_count - }) - - except Exception as e: - app_logger.error(f"Fehler beim Löschen alter Backups: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route('/api/admin/stats/live', methods=['GET']) -@admin_required -def get_admin_live_stats(): - """Liefert Live-Statistiken für das Admin-Dashboard.""" - try: - db_session = get_db_session() - - # Aktuelle Statistiken sammeln - total_users = db_session.query(User).count() - total_printers = db_session.query(Printer).count() - total_jobs = db_session.query(Job).count() - active_jobs = db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count() - - # Printer-Status - available_printers = db_session.query(Printer).filter(Printer.status == "available").count() - offline_printers = db_session.query(Printer).filter(Printer.status == "offline").count() - maintenance_printers = db_session.query(Printer).filter(Printer.status == "maintenance").count() - - # Jobs heute - today = datetime.now().date() - jobs_today = db_session.query(Job).filter( - func.date(Job.created_at) == today - ).count() - - # Erfolgreiche Jobs heute - completed_jobs_today = db_session.query(Job).filter( - func.date(Job.created_at) == today, - Job.status == "completed" - ).count() - - db_session.close() - - stats = { - "users": { - "total": total_users - }, - "printers": { - "total": total_printers, - "available": available_printers, - "offline": offline_printers, - "maintenance": maintenance_printers - }, - "jobs": { - "total": total_jobs, - "active": active_jobs, - "today": jobs_today, - "completed_today": completed_jobs_today - }, - "timestamp": datetime.now().isoformat() - } - - return jsonify(stats) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Live-Statistiken: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route('/api/admin/system/status', methods=['GET']) -@admin_required -def get_system_status(): - """Liefert System-Status-Informationen.""" - try: - import psutil - import platform - - # CPU und Memory - cpu_percent = psutil.cpu_percent(interval=1) - memory = psutil.virtual_memory() - disk = psutil.disk_usage('/') - - # Netzwerk (vereinfacht) - network = psutil.net_io_counters() - - system_info = { - "platform": platform.system(), - "platform_release": platform.release(), - "platform_version": platform.version(), - "machine": platform.machine(), - "processor": platform.processor(), - "cpu": { - "percent": cpu_percent, - "count": psutil.cpu_count() - }, - "memory": { - "total": memory.total, - "available": memory.available, - "percent": memory.percent, - "used": memory.used - }, - "disk": { - "total": disk.total, - "used": disk.used, - "free": disk.free, - "percent": (disk.used / disk.total) * 100 - }, - "network": { - "bytes_sent": network.bytes_sent, - "bytes_recv": network.bytes_recv - }, - "timestamp": datetime.now().isoformat() - } - - return jsonify(system_info) - - except ImportError: - return jsonify({ - "error": "psutil nicht installiert", - "message": "Systemstatus kann nicht abgerufen werden" - }), 500 - except Exception as e: - app_logger.error(f"Fehler beim Abrufen des Systemstatus: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route('/api/admin/database/status', methods=['GET']) -@admin_required -def get_database_status(): - """Liefert Datenbank-Status-Informationen.""" - try: - db_session = get_db_session() - - # Tabellen-Informationen sammeln - table_stats = {} - - # User-Tabelle - user_count = db_session.query(User).count() - latest_user = db_session.query(User).order_by(User.created_at.desc()).first() - - # Printer-Tabelle - printer_count = db_session.query(Printer).count() - latest_printer = db_session.query(Printer).order_by(Printer.created_at.desc()).first() - - # Job-Tabelle - job_count = db_session.query(Job).count() - latest_job = db_session.query(Job).order_by(Job.created_at.desc()).first() - - table_stats = { - "users": { - "count": user_count, - "latest": latest_user.created_at.isoformat() if latest_user else None - }, - "printers": { - "count": printer_count, - "latest": latest_printer.created_at.isoformat() if latest_printer else None - }, - "jobs": { - "count": job_count, - "latest": latest_job.created_at.isoformat() if latest_job else None - } - } - - db_session.close() - - # Datenbank-Dateigröße (falls SQLite) - db_file_size = None - try: - db_path = os.path.join(os.path.dirname(__file__), 'database', 'app.db') - if os.path.exists(db_path): - db_file_size = os.path.getsize(db_path) - except: - pass - - status = { - "tables": table_stats, - "database_size": db_file_size, - "timestamp": datetime.now().isoformat(), - "connection_status": "connected" - } - - return jsonify(status) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen des Datenbankstatus: {str(e)}") - return jsonify({ - "error": "Datenbankfehler", - "connection_status": "error", - "timestamp": datetime.now().isoformat() - }), 500 - -# ===== WEITERE UI-ROUTEN ===== - -@app.route("/terms") -def terms(): - """Zeigt die Nutzungsbedingungen an.""" - return render_template("terms.html") - -@app.route("/privacy") -def privacy(): - """Zeigt die Datenschutzerklärung an.""" - return render_template("privacy.html") - -@app.route("/admin/users/add") -@login_required -def admin_add_user_page(): - """Zeigt die Seite zum Hinzufügen eines neuen Benutzers an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - return render_template("admin_add_user.html") - -@app.route("/admin/printers/add") -@login_required -def admin_add_printer_page(): - """Zeigt die Seite zum Hinzufügen eines neuen Druckers an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - return render_template("admin_add_printer.html") - -@app.route("/admin/printers//manage") -@login_required -def admin_manage_printer_page(printer_id): - """Zeigt die Drucker-Verwaltungsseite an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - db_session = get_db_session() - try: - printer = db_session.get(Printer, printer_id) - if not printer: - flash("Drucker nicht gefunden.", "error") - return redirect(url_for("admin_page")) - - printer_data = { - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status or "offline", - "active": printer.active if hasattr(printer, 'active') else True, - "created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat() - } - - db_session.close() - return render_template("admin_manage_printer.html", printer=printer_data) - - except Exception as e: - db_session.close() - app_logger.error(f"Fehler beim Laden der Drucker-Verwaltung: {str(e)}") - flash("Fehler beim Laden der Drucker-Daten.", "error") - return redirect(url_for("admin_page")) - -@app.route("/admin/printers//settings") -@login_required -def admin_printer_settings_page(printer_id): - """Zeigt die Drucker-Einstellungsseite an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - db_session = get_db_session() - try: - printer = db_session.get(Printer, printer_id) - if not printer: - flash("Drucker nicht gefunden.", "error") - return redirect(url_for("admin_page")) - - printer_data = { - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status or "offline", - "active": printer.active if hasattr(printer, 'active') else True, - "created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat() - } - - db_session.close() - return render_template("admin_printer_settings.html", printer=printer_data) - - except Exception as e: - db_session.close() - app_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}") - flash("Fehler beim Laden der Drucker-Daten.", "error") - return redirect(url_for("admin_page")) - -@app.route("/admin/guest-requests") -@login_required -@admin_required -def admin_guest_requests(): - """Admin-Seite für Gastanfragen Verwaltung""" - try: - app_logger.info(f"Admin-Gastanfragen Seite aufgerufen von User {current_user.id}") - return render_template("admin_guest_requests.html") - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Gastanfragen Seite: {str(e)}") - flash("Fehler beim Laden der Gastanfragen-Verwaltung.", "danger") - return redirect(url_for("admin")) - -@app.route("/requests/overview") -@login_required -@admin_required -def admin_guest_requests_overview(): - """Admin-Oberfläche für die Verwaltung von Gastanfragen mit direkten Aktionen.""" - try: - app_logger.info(f"Admin-Gastanträge Übersicht aufgerufen von User {current_user.id}") - return render_template("admin_guest_requests_overview.html") - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Gastanträge Übersicht: {str(e)}") - flash("Fehler beim Laden der Gastanträge-Übersicht.", "danger") - return redirect(url_for("admin")) - -# ===== ADMIN API-ROUTEN FÜR BENUTZER UND DRUCKER ===== - -@app.route("/api/admin/users", methods=["POST"]) -@login_required -def create_user_api(): - """Erstellt einen neuen Benutzer (nur für Admins).""" - if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Benutzer erstellen"}), 403 - - try: - data = request.json - - # Pflichtfelder prüfen - required_fields = ["username", "email", "password"] - for field in required_fields: - if field not in data or not data[field]: - return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400 - - db_session = get_db_session() - - # Prüfen, ob bereits ein Benutzer mit diesem Benutzernamen oder E-Mail existiert - existing_user = db_session.query(User).filter( - (User.username == data["username"]) | (User.email == data["email"]) - ).first() - - if existing_user: - db_session.close() - return jsonify({"error": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert bereits"}), 400 - - # Neuen Benutzer erstellen - new_user = User( - username=data["username"], - email=data["email"], - first_name=data.get("first_name", ""), - last_name=data.get("last_name", ""), - is_admin=data.get("is_admin", False), - created_at=datetime.now() - ) - - # Passwort setzen - new_user.set_password(data["password"]) - - db_session.add(new_user) - db_session.commit() - - user_data = { - "id": new_user.id, - "username": new_user.username, - "email": new_user.email, - "first_name": new_user.first_name, - "last_name": new_user.last_name, - "is_admin": new_user.is_admin, - "created_at": new_user.created_at.isoformat() - } - - db_session.close() - - user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}") - return jsonify({"user": user_data}), 201 - - except Exception as e: - user_logger.error(f"Fehler beim Erstellen eines Benutzers: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/admin/printers//toggle", methods=["POST"]) -@login_required -def toggle_printer_power(printer_id): - """ - Schaltet einen Drucker über die zugehörige Steckdose ein/aus. - """ - if not current_user.is_admin: - return jsonify({"error": "Administratorrechte erforderlich"}), 403 - - try: - # Robuste JSON-Datenverarbeitung - data = {} - try: - if request.is_json and request.get_json(): - data = request.get_json() - elif request.form: - # Fallback für Form-Daten - data = request.form.to_dict() - except Exception as json_error: - printers_logger.warning(f"Fehler beim Parsen der JSON-Daten für Drucker {printer_id}: {str(json_error)}") - # Verwende Standard-Werte wenn JSON-Parsing fehlschlägt - data = {} - - # Standard-Zustand ermitteln (Toggle-Verhalten) - db_session = get_db_session() - printer = db_session.query(Printer).get(printer_id) - - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Aktuellen Status ermitteln für Toggle-Verhalten - current_status = getattr(printer, 'status', 'offline') - current_active = getattr(printer, 'active', False) - - # Zielzustand bestimmen - if 'state' in data: - # Expliziter Zustand angegeben - state = bool(data.get("state", True)) - else: - # Toggle-Verhalten: Umschalten basierend auf aktuellem Status - state = not (current_status == "available" and current_active) - - db_session.close() - - # Steckdose schalten - from utils.job_scheduler import toggle_plug - success = toggle_plug(printer_id, state) - - if success: - action = "eingeschaltet" if state else "ausgeschaltet" - printers_logger.info(f"Drucker {printer.name} (ID: {printer_id}) erfolgreich {action} von Admin {current_user.name}") - - return jsonify({ - "success": True, - "message": f"Drucker erfolgreich {action}", - "printer_id": printer_id, - "printer_name": printer.name, - "state": state, - "action": action - }) - else: - printers_logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}") - return jsonify({ - "success": False, - "error": "Fehler beim Schalten der Steckdose", - "printer_id": printer_id - }), 500 - - except Exception as e: - printers_logger.error(f"Fehler beim Schalten von Drucker {printer_id}: {str(e)}") - return jsonify({ - "success": False, - "error": "Interner Serverfehler", - "details": str(e) - }), 500 - -@app.route("/api/admin/printers//test-tapo", methods=["POST"]) -@login_required -@admin_required -def test_printer_tapo_connection(printer_id): - """ - Testet die Tapo-Steckdosen-Verbindung für einen Drucker. - """ - try: - db_session = get_db_session() - printer = db_session.query(Printer).get(printer_id) - - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - if not printer.plug_ip or not printer.plug_username or not printer.plug_password: - db_session.close() - return jsonify({ - "error": "Unvollständige Tapo-Konfiguration", - "missing": [ - key for key, value in { - "plug_ip": printer.plug_ip, - "plug_username": printer.plug_username, - "plug_password": printer.plug_password - }.items() if not value - ] - }), 400 - - db_session.close() - - # Tapo-Verbindung testen - from utils.job_scheduler import test_tapo_connection - test_result = test_tapo_connection( - printer.plug_ip, - printer.plug_username, - printer.plug_password - ) - - return jsonify({ - "printer_id": printer_id, - "printer_name": printer.name, - "tapo_test": test_result - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Testen der Tapo-Verbindung für Drucker {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler beim Verbindungstest"}), 500 - -@app.route("/api/admin/printers/test-all-tapo", methods=["POST"]) -@login_required -@admin_required -def test_all_printers_tapo_connection(): - """ - Testet die Tapo-Steckdosen-Verbindung für alle Drucker. - Nützlich für Diagnose und Setup-Validierung. - """ - try: - db_session = get_db_session() - printers = db_session.query(Printer).filter(Printer.active == True).all() - db_session.close() - - if not printers: - return jsonify({ - "message": "Keine aktiven Drucker gefunden", - "results": [] - }) - - # Alle Drucker testen - from utils.job_scheduler import test_tapo_connection - results = [] - - for printer in printers: - result = { - "printer_id": printer.id, - "printer_name": printer.name, - "plug_ip": printer.plug_ip, - "has_config": bool(printer.plug_ip and printer.plug_username and printer.plug_password) - } - - if result["has_config"]: - # Tapo-Verbindung testen - test_result = test_tapo_connection( - printer.plug_ip, - printer.plug_username, - printer.plug_password - ) - result["tapo_test"] = test_result - else: - result["tapo_test"] = { - "success": False, - "error": "Unvollständige Tapo-Konfiguration", - "device_info": None, - "status": "unconfigured" - } - result["missing_config"] = [ - key for key, value in { - "plug_ip": printer.plug_ip, - "plug_username": printer.plug_username, - "plug_password": printer.plug_password - }.items() if not value - ] - - results.append(result) - - # Zusammenfassung erstellen - total_printers = len(results) - successful_connections = sum(1 for r in results if r["tapo_test"]["success"]) - configured_printers = sum(1 for r in results if r["has_config"]) - - return jsonify({ - "summary": { - "total_printers": total_printers, - "configured_printers": configured_printers, - "successful_connections": successful_connections, - "success_rate": round(successful_connections / total_printers * 100, 1) if total_printers > 0 else 0 - }, - "results": results - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Testen aller Tapo-Verbindungen: {str(e)}") - return jsonify({"error": "Interner Serverfehler beim Massentest"}), 500 - -# ===== ADMIN FORM ENDPOINTS ===== - -@app.route("/admin/users/create", methods=["POST"]) -@login_required -def admin_create_user_form(): - """Erstellt einen neuen Benutzer über HTML-Form (nur für Admins).""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - try: - # Form-Daten lesen - email = request.form.get("email", "").strip() - name = request.form.get("name", "").strip() - password = request.form.get("password", "").strip() - role = request.form.get("role", "user").strip() - - # Pflichtfelder prüfen - if not email or not password: - flash("E-Mail und Passwort sind erforderlich.", "error") - return redirect(url_for("admin_add_user_page")) - - # E-Mail validieren - import re - email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' - if not re.match(email_pattern, email): - flash("Ungültige E-Mail-Adresse.", "error") - return redirect(url_for("admin_add_user_page")) - - db_session = get_db_session() - - # Prüfen, ob bereits ein Benutzer mit dieser E-Mail existiert - existing_user = db_session.query(User).filter(User.email == email).first() - if existing_user: - db_session.close() - flash("Ein Benutzer mit dieser E-Mail existiert bereits.", "error") - return redirect(url_for("admin_add_user_page")) - - # E-Mail als Username verwenden (falls kein separates Username-Feld) - username = email.split('@')[0] - counter = 1 - original_username = username - while db_session.query(User).filter(User.username == username).first(): - username = f"{original_username}{counter}" - counter += 1 - - # Neuen Benutzer erstellen - new_user = User( - username=username, - email=email, - first_name=name.split(' ')[0] if name else "", - last_name=" ".join(name.split(' ')[1:]) if name and ' ' in name else "", - is_admin=(role == "admin"), - created_at=datetime.now() - ) - - # Passwort setzen - new_user.set_password(password) - - db_session.add(new_user) - db_session.commit() - db_session.close() - - user_logger.info(f"Neuer Benutzer '{new_user.username}' erstellt von Admin {current_user.id}") - flash(f"Benutzer '{new_user.email}' erfolgreich erstellt.", "success") - return redirect(url_for("admin_page", tab="users")) - - except Exception as e: - user_logger.error(f"Fehler beim Erstellen eines Benutzers über Form: {str(e)}") - flash("Fehler beim Erstellen des Benutzers.", "error") - return redirect(url_for("admin_add_user_page")) - -@app.route("/admin/printers/create", methods=["POST"]) -@login_required -def admin_create_printer_form(): - """Erstellt einen neuen Drucker über HTML-Form (nur für Admins).""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - try: - # Form-Daten lesen - name = request.form.get("name", "").strip() - ip_address = request.form.get("ip_address", "").strip() - model = request.form.get("model", "").strip() - location = request.form.get("location", "").strip() - description = request.form.get("description", "").strip() - status = request.form.get("status", "available").strip() - - # Pflichtfelder prüfen - if not name or not ip_address: - flash("Name und IP-Adresse sind erforderlich.", "error") - return redirect(url_for("admin_add_printer_page")) - - # IP-Adresse validieren - import re - ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' - if not re.match(ip_pattern, ip_address): - flash("Ungültige IP-Adresse.", "error") - return redirect(url_for("admin_add_printer_page")) - - db_session = get_db_session() - - # Prüfen, ob bereits ein Drucker mit diesem Namen existiert - existing_printer = db_session.query(Printer).filter(Printer.name == name).first() - if existing_printer: - db_session.close() - flash("Ein Drucker mit diesem Namen existiert bereits.", "error") - return redirect(url_for("admin_add_printer_page")) - - # Neuen Drucker erstellen - new_printer = Printer( - name=name, - model=model, - location=location, - description=description, - mac_address="", # Wird später ausgefüllt - plug_ip=ip_address, - status=status, - created_at=datetime.now() - ) - - db_session.add(new_printer) - db_session.commit() - db_session.close() - - printers_logger.info(f"Neuer Drucker '{new_printer.name}' erstellt von Admin {current_user.id}") - flash(f"Drucker '{new_printer.name}' erfolgreich erstellt.", "success") - return redirect(url_for("admin_page", tab="printers")) - - except Exception as e: - printers_logger.error(f"Fehler beim Erstellen eines Druckers über Form: {str(e)}") - flash("Fehler beim Erstellen des Druckers.", "error") - return redirect(url_for("admin_add_printer_page")) - -@app.route("/admin/users//edit", methods=["GET"]) -@login_required -def admin_edit_user_page(user_id): - """Zeigt die Benutzer-Bearbeitungsseite an.""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - db_session = get_db_session() - try: - user = db_session.get(User, user_id) - if not user: - flash("Benutzer nicht gefunden.", "error") - return redirect(url_for("admin_page", tab="users")) - - user_data = { - "id": user.id, - "username": user.username, - "email": user.email, - "name": user.name or "", - "is_admin": user.is_admin, - "active": user.active, - "created_at": user.created_at.isoformat() if user.created_at else datetime.now().isoformat() - } - - db_session.close() - return render_template("admin_edit_user.html", user=user_data) - - except Exception as e: - db_session.close() - app_logger.error(f"Fehler beim Laden der Benutzer-Daten: {str(e)}") - flash("Fehler beim Laden der Benutzer-Daten.", "error") - return redirect(url_for("admin_page", tab="users")) - -@app.route("/admin/users//update", methods=["POST"]) -@login_required -def admin_update_user_form(user_id): - """Aktualisiert einen Benutzer über HTML-Form (nur für Admins).""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - try: - # Form-Daten lesen - email = request.form.get("email", "").strip() - name = request.form.get("name", "").strip() - password = request.form.get("password", "").strip() - role = request.form.get("role", "user").strip() - is_active = request.form.get("is_active", "true").strip() == "true" - - # Pflichtfelder prüfen - if not email: - flash("E-Mail-Adresse ist erforderlich.", "error") - return redirect(url_for("admin_edit_user_page", user_id=user_id)) - - # E-Mail validieren - import re - email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' - if not re.match(email_pattern, email): - flash("Ungültige E-Mail-Adresse.", "error") - return redirect(url_for("admin_edit_user_page", user_id=user_id)) - - db_session = get_db_session() - - user = db_session.query(User).get(user_id) - if not user: - db_session.close() - flash("Benutzer nicht gefunden.", "error") - return redirect(url_for("admin_page", tab="users")) - - # Prüfen, ob bereits ein anderer Benutzer mit dieser E-Mail existiert - existing_user = db_session.query(User).filter( - User.email == email, - User.id != user_id - ).first() - if existing_user: - db_session.close() - flash("Ein Benutzer mit dieser E-Mail-Adresse existiert bereits.", "error") - return redirect(url_for("admin_edit_user_page", user_id=user_id)) - - # Benutzer aktualisieren - user.email = email - if name: - user.name = name - - # Passwort nur ändern, wenn eines angegeben wurde - if password: - user.password_hash = generate_password_hash(password) - - user.role = "admin" if role == "admin" else "user" - user.active = is_active - - db_session.commit() - db_session.close() - - auth_logger.info(f"Benutzer '{user.email}' (ID: {user_id}) aktualisiert von Admin {current_user.id}") - flash(f"Benutzer '{user.email}' erfolgreich aktualisiert.", "success") - return redirect(url_for("admin_page", tab="users")) - - except Exception as e: - auth_logger.error(f"Fehler beim Aktualisieren eines Benutzers über Form: {str(e)}") - flash("Fehler beim Aktualisieren des Benutzers.", "error") - return redirect(url_for("admin_edit_user_page", user_id=user_id)) - -@app.route("/admin/printers//update", methods=["POST"]) -@login_required -def admin_update_printer_form(printer_id): - """Aktualisiert einen Drucker über HTML-Form (nur für Admins).""" - if not current_user.is_admin: - flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error") - return redirect(url_for("index")) - - try: - # Form-Daten lesen - name = request.form.get("name", "").strip() - ip_address = request.form.get("ip_address", "").strip() - model = request.form.get("model", "").strip() - location = request.form.get("location", "").strip() - description = request.form.get("description", "").strip() - status = request.form.get("status", "available").strip() - - # Pflichtfelder prüfen - if not name or not ip_address: - flash("Name und IP-Adresse sind erforderlich.", "error") - return redirect(url_for("admin_printer_settings_page", printer_id=printer_id)) - - # IP-Adresse validieren - import re - ip_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' - if not re.match(ip_pattern, ip_address): - flash("Ungültige IP-Adresse.", "error") - return redirect(url_for("admin_printer_settings_page", printer_id=printer_id)) - - db_session = get_db_session() - - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - flash("Drucker nicht gefunden.", "error") - return redirect(url_for("admin_page", tab="printers")) - - # Prüfen, ob bereits ein anderer Drucker mit diesem Namen existiert - existing_printer = db_session.query(Printer).filter( - Printer.name == name, - Printer.id != printer_id - ).first() - if existing_printer: - db_session.close() - flash("Ein Drucker mit diesem Namen existiert bereits.", "error") - return redirect(url_for("admin_printer_settings_page", printer_id=printer_id)) - - # Drucker aktualisieren - printer.name = name - printer.model = model - printer.location = location - printer.description = description - printer.plug_ip = ip_address - printer.status = status - - db_session.commit() - db_session.close() - - printers_logger.info(f"Drucker '{printer.name}' (ID: {printer_id}) aktualisiert von Admin {current_user.id}") - flash(f"Drucker '{printer.name}' erfolgreich aktualisiert.", "success") - return redirect(url_for("admin_manage_printer_page", printer_id=printer_id)) - - except Exception as e: - printers_logger.error(f"Fehler beim Aktualisieren eines Druckers über Form: {str(e)}") - flash("Fehler beim Aktualisieren des Druckers.", "error") - return redirect(url_for("admin_printer_settings_page", printer_id=printer_id)) - -# Neue API-Endpunkte für erweiterte Drucker-Status-Verwaltung hinzufügen -@app.route("/api/printers/online", methods=["GET"]) -@login_required -def get_online_printers(): - """Gibt nur die online/verfügbaren Drucker zurück - optimiert für schnelle Anzeige.""" - db_session = get_db_session() - printers_logger = get_logger("printers") - - try: - # Session-Cache für Online-Drucker prüfen - cache_key = f"online_printers_{current_user.id}" - cached_data = session.get(cache_key) - cache_timestamp = session.get(f"{cache_key}_timestamp") - - # Cache ist 30 Sekunden gültig - if cached_data and cache_timestamp: - cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds() - if cache_age < 30: - printers_logger.info(f"Online-Drucker aus Session-Cache geladen (Alter: {cache_age:.1f}s)") - return jsonify({ - "printers": cached_data, - "count": len(cached_data), - "cached": True, - "cache_age": cache_age - }) - - # Nur verfügbare/online Drucker aus Datenbank laden - printers = db_session.query(Printer).filter( - Printer.status.in_(["available", "online", "idle"]), - Printer.active == True - ).all() - - current_time = datetime.now() - online_printers = [] - - for printer in printers: - printer_data = { - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": printer.status, - "active": printer.active, - "ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None), - "created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(), - "last_checked": printer.last_checked.isoformat() if hasattr(printer, 'last_checked') and printer.last_checked else None, - "is_online": True # Alle Drucker in dieser Liste sind online - } - online_printers.append(printer_data) - - # In Session-Cache speichern - session[cache_key] = online_printers - session[f"{cache_key}_timestamp"] = current_time.isoformat() - session.permanent = True - - db_session.close() - - printers_logger.info(f"Online-Drucker geladen: {len(online_printers)} verfügbare Drucker") - - return jsonify({ - "printers": online_printers, - "count": len(online_printers), - "cached": False, - "message": f"{len(online_printers)} online Drucker gefunden" - }) - - except Exception as e: - db_session.rollback() - db_session.close() - printers_logger.error(f"Fehler beim Abrufen der Online-Drucker: {str(e)}") - return jsonify({ - "error": f"Fehler beim Laden der Online-Drucker: {str(e)}", - "printers": [] - }), 500 - -@app.route("/api/printers/status/live", methods=["GET"]) -@login_required -@measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status") -def get_live_printer_status(): - """Gibt Live-Status aller Drucker zurück mit Session-Caching und Echtzeit-Updates.""" - db_session = get_db_session() - printers_logger = get_logger("printers") - - try: - # Session-Cache für Live-Status prüfen - cache_key = f"live_printer_status_{current_user.id}" - cached_data = session.get(cache_key) - cache_timestamp = session.get(f"{cache_key}_timestamp") - - # Cache ist 15 Sekunden gültig für Live-Status - if cached_data and cache_timestamp: - cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds() - if cache_age < 15: - printers_logger.info(f"Live-Status aus Session-Cache geladen (Alter: {cache_age:.1f}s)") - return jsonify({ - "printers": cached_data, - "cached": True, - "cache_age": cache_age, - "next_update": 15 - cache_age - }) - - # Alle Drucker aus der Datenbank laden - printers = db_session.query(Printer).all() - - if not printers: - return jsonify({ - "printers": [], - "count": 0, - "message": "Keine Drucker in der Datenbank gefunden" - }) - - # Drucker-Daten für Status-Check vorbereiten - printer_data = [] - for printer in printers: - printer_data.append({ - 'id': printer.id, - 'name': printer.name, - 'ip_address': printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None) - }) - - # Paralleler Status-Check mit kürzerem Timeout für Live-Updates - try: - status_results = check_multiple_printers_status(printer_data, timeout=3) - except Exception as e: - printers_logger.warning(f"Status-Check fehlgeschlagen, verwende letzte bekannte Status: {str(e)}") - # Fallback: verwende letzte bekannte Status - status_results = {p['id']: (p.get('last_status', 'offline'), False) for p in printer_data} - - # Live-Status-Daten zusammenstellen - live_status_data = [] - current_time = datetime.now() - online_count = 0 - - for printer in printers: - if printer.id in status_results: - status, active = status_results[printer.id] - frontend_status = "available" if status == "online" else "offline" - if frontend_status == "available": - online_count += 1 - else: - frontend_status = printer.status or "offline" - active = printer.active if hasattr(printer, 'active') else False - - # Status in Datenbank aktualisieren (asynchron) - printer.status = frontend_status - printer.active = active - if hasattr(printer, 'last_checked'): - printer.last_checked = current_time - - live_status_data.append({ - "id": printer.id, - "name": printer.name, - "model": printer.model or 'Unbekanntes Modell', - "location": printer.location or 'Unbekannter Standort', - "mac_address": printer.mac_address, - "plug_ip": printer.plug_ip, - "status": frontend_status, - "active": active, - "ip_address": printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None), - "created_at": printer.created_at.isoformat() if printer.created_at else current_time.isoformat(), - "last_checked": current_time.isoformat(), - "is_online": frontend_status == "available", - "status_changed": True # Für Frontend-Animationen - }) - - # Änderungen in Datenbank speichern - try: - db_session.commit() - except Exception as e: - printers_logger.warning(f"Fehler beim Speichern der Live-Status-Updates: {str(e)}") - - # In Session-Cache speichern - session[cache_key] = live_status_data - session[f"{cache_key}_timestamp"] = current_time.isoformat() - session.permanent = True - - # Online-Drucker-Cache invalidieren - online_cache_key = f"online_printers_{current_user.id}" - if online_cache_key in session: - del session[online_cache_key] - del session[f"{online_cache_key}_timestamp"] - - db_session.close() - - printers_logger.info(f"Live-Status aktualisiert: {online_count} von {len(live_status_data)} Drucker online") - - return jsonify({ - "printers": live_status_data, - "count": len(live_status_data), - "online_count": online_count, - "offline_count": len(live_status_data) - online_count, - "cached": False, - "timestamp": current_time.isoformat(), - "next_update": 15 - }) - - except Exception as e: - db_session.rollback() - db_session.close() - printers_logger.error(f"Fehler beim Live-Status-Check: {str(e)}") - return jsonify({ - "error": f"Fehler beim Live-Status-Check: {str(e)}", - "printers": [] - }), 500 - -@app.route("/api/printers/status/summary", methods=["GET"]) -@login_required -def get_printer_status_summary(): - """Gibt eine Zusammenfassung des Drucker-Status zurück - sehr schnell.""" - db_session = get_db_session() - - try: - # Session-Cache für Status-Zusammenfassung - cache_key = f"printer_summary_{current_user.id}" - cached_data = session.get(cache_key) - cache_timestamp = session.get(f"{cache_key}_timestamp") - - # Cache ist 60 Sekunden gültig - if cached_data and cache_timestamp: - cache_age = (datetime.now() - datetime.fromisoformat(cache_timestamp)).total_seconds() - if cache_age < 60: - return jsonify({ - **cached_data, - "cached": True, - "cache_age": cache_age - }) - - # Status-Zusammenfassung aus Datenbank - total_printers = db_session.query(Printer).count() - online_printers = db_session.query(Printer).filter( - Printer.status.in_(["available", "online", "idle"]), - Printer.active == True - ).count() - offline_printers = total_printers - online_printers - - # Letzte Aktualisierung ermitteln - last_checked = db_session.query(func.max(Printer.last_checked)).scalar() - - summary_data = { - "total": total_printers, - "online": online_printers, - "offline": offline_printers, - "percentage_online": round((online_printers / total_printers * 100) if total_printers > 0 else 0, 1), - "last_checked": last_checked.isoformat() if last_checked else None, - "timestamp": datetime.now().isoformat() - } - - # In Session-Cache speichern - session[cache_key] = summary_data - session[f"{cache_key}_timestamp"] = datetime.now().isoformat() - session.permanent = True - - db_session.close() - - return jsonify({ - **summary_data, - "cached": False - }) - - except Exception as e: - db_session.close() - return jsonify({ - "error": f"Fehler beim Laden der Status-Zusammenfassung: {str(e)}", - "total": 0, - "online": 0, - "offline": 0 - }), 500 - -# Session-Cache-Management -@app.route("/api/printers/cache/clear", methods=["POST"]) -@login_required -def clear_printer_cache(): - """ - Löscht den Drucker-Cache für eine Aktualisierung. - """ - try: - # Invalidate model cache for printers - from models import invalidate_model_cache - invalidate_model_cache("Printer") - - # Clear any additional printer-specific caches - # Hier können Sie weitere Cache-Löschungen hinzufügen - - printers_logger.info(f"Drucker-Cache geleert von Benutzer {current_user.name}") - - return jsonify({ - "success": True, - "message": "Drucker-Cache erfolgreich geleert" - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Leeren des Drucker-Caches: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Leeren des Caches", - "details": str(e) - }), 500 - -@app.route("/api/printers/monitor/live-status", methods=["GET"]) -@login_required -@limit_requests("printer_monitor_live") -def get_live_printer_monitor_status(): - """ - Live-Druckerstatus über den neuen PrinterMonitor mit Session-Caching. - """ - try: - use_cache = request.args.get('use_cache', 'true').lower() == 'true' - - printers_logger.info(f"Live-Druckerstatus angefordert von {current_user.name} (Cache: {use_cache})") - - # Drucker-Status über Monitor abrufen - status_dict = printer_monitor.get_live_printer_status(use_session_cache=use_cache) - - # Zusätzliche Statistiken hinzufügen - summary = printer_monitor.get_printer_summary() - - response_data = { - "success": True, - "printers": status_dict, - "summary": summary, - "cache_used": use_cache, - "timestamp": datetime.now().isoformat(), - "total_printers": len(status_dict) - } - - printers_logger.info(f"Live-Status für {len(status_dict)} Drucker zurückgegeben") - - return jsonify(response_data) - - except Exception as e: - printers_logger.error(f"Fehler beim Abrufen des Live-Druckerstatus: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Abrufen des Live-Status", - "details": str(e), - "printers": {}, - "summary": {"total": 0, "online": 0, "offline": 0} - }), 500 - -@app.route("/api/printers/monitor/summary", methods=["GET"]) -@login_required -@limit_requests("printer_monitor_summary") -def get_printer_monitor_summary(): - """ - Schnelle Zusammenfassung des Druckerstatus ohne vollständige Details. - """ - try: - summary = printer_monitor.get_printer_summary() - - return jsonify({ - "success": True, - "summary": summary, - "timestamp": datetime.now().isoformat() - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Abrufen der Drucker-Zusammenfassung: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Abrufen der Zusammenfassung", - "summary": {"total": 0, "online": 0, "offline": 0} - }), 500 - -@app.route("/api/printers/monitor/clear-cache", methods=["POST"]) -@login_required -@limit_requests("printer_monitor_cache") -def clear_printer_monitor_cache(): - """ - Löscht alle Caches des Drucker-Monitors. - """ - try: - printer_monitor.clear_all_caches() - - printers_logger.info(f"Drucker-Monitor-Cache geleert von Benutzer {current_user.name}") - - return jsonify({ - "success": True, - "message": "Drucker-Monitor-Cache erfolgreich geleert" - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Leeren des Drucker-Monitor-Caches: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Leeren des Monitor-Caches", - "details": str(e) - }), 500 - -@app.route("/api/printers/monitor/initialize-outlets", methods=["POST"]) -@login_required -@admin_required -@limit_requests("printer_monitor_init") -def initialize_printer_outlets(): - """ - Initialisiert alle Drucker-Steckdosen (schaltet sie aus für einheitlichen Zustand). - Nur für Administratoren. - """ - try: - printers_logger.info(f"Steckdosen-Initialisierung gestartet von Admin {current_user.name}") - - # Steckdosen initialisieren - results = printer_monitor.initialize_all_outlets_on_startup() - - success_count = sum(1 for success in results.values() if success) - total_count = len(results) - - return jsonify({ - "success": True, - "message": f"Steckdosen-Initialisierung abgeschlossen: {success_count}/{total_count} erfolgreich", - "results": results, - "statistics": { - "total": total_count, - "successful": success_count, - "failed": total_count - success_count - } - }) - - except Exception as e: - printers_logger.error(f"Fehler bei Steckdosen-Initialisierung: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler bei der Steckdosen-Initialisierung", - "details": str(e) - }), 500 - -# ===== FEHLENDE ADMIN-API-ENDPUNKTE ===== - -@app.route('/api/admin/cache/clear', methods=['POST']) -@admin_required -def clear_admin_cache(): - """Leert den System-Cache""" - try: - # Cache-Verzeichnisse leeren - import shutil - import os - - cache_dirs = [ - os.path.join(os.path.dirname(__file__), 'static', 'cache'), - os.path.join(os.path.dirname(__file__), '__pycache__'), - ] - - cleared_items = 0 - for cache_dir in cache_dirs: - if os.path.exists(cache_dir): - for item in os.listdir(cache_dir): - item_path = os.path.join(cache_dir, item) - try: - if os.path.isfile(item_path): - os.unlink(item_path) - cleared_items += 1 - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - cleared_items += 1 - except Exception as e: - app_logger.warning(f"Konnte Cache-Element nicht löschen: {item_path} - {str(e)}") - - # Modell-Cache leeren - try: - from models import clear_cache - clear_cache() - except (ImportError, AttributeError): - app_logger.warning("clear_cache Funktion nicht verfügbar") - - app_logger.info(f"System-Cache geleert: {cleared_items} Elemente entfernt") - return jsonify({ - "success": True, - "message": f"Cache erfolgreich geleert ({cleared_items} Elemente)", - "cleared_items": cleared_items - }) - - except Exception as e: - app_logger.error(f"Fehler beim Leeren des Cache: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Leeren des Cache: {str(e)}" - }), 500 - -@app.route('/api/admin/system/restart', methods=['POST']) -@admin_required -def restart_admin_system(): - """Startet das System neu (nur für Entwicklung)""" - try: - import os - import signal - - app_logger.warning("System-Neustart durch Admin angefordert") - - # In Produktionsumgebung sollte dies anders gehandhabt werden - if os.environ.get('FLASK_ENV') == 'development': - # Graceful shutdown für Development - def shutdown_server(): - func = request.environ.get('werkzeug.server.shutdown') - if func is None: - raise RuntimeError('Not running with the Werkzeug Server') - func() - - shutdown_server() - return jsonify({ - "success": True, - "message": "System wird neugestartet..." - }) - else: - # Für Produktion - Signal an Parent Process - os.kill(os.getpid(), signal.SIGTERM) - return jsonify({ - "success": True, - "message": "Neustart-Signal gesendet" - }) - - except Exception as e: - app_logger.error(f"Fehler beim System-Neustart: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Neustart: {str(e)}" - }), 500 - -@app.route('/api/admin/printers/update-all', methods=['POST']) -@admin_required -def update_all_printers(): - """Aktualisiert den Status aller Drucker""" - try: - db_session = get_db_session() - printers = db_session.query(Printer).all() - - updated_printers = [] - - for printer in printers: - if printer.plug_ip: - try: - status, active = check_printer_status(printer.plug_ip) - old_status = printer.status - - printer.update_status(status, active) - - updated_printers.append({ - "id": printer.id, - "name": printer.name, - "old_status": old_status, - "new_status": status, - "active": active - }) - - except Exception as e: - printers_logger.warning(f"Fehler beim Aktualisieren von Drucker {printer.name}: {str(e)}") - - db_session.commit() - db_session.close() - - app_logger.info(f"Status von {len(updated_printers)} Druckern aktualisiert") - return jsonify({ - "success": True, - "message": f"Status von {len(updated_printers)} Druckern aktualisiert", - "updated_printers": updated_printers - }) - - except Exception as e: - app_logger.error(f"Fehler beim Aktualisieren aller Drucker: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Aktualisieren: {str(e)}" - }), 500 - -@app.route('/api/admin/settings', methods=['GET']) -@admin_required -def get_admin_settings(): - """Holt die aktuellen Admin-Einstellungen""" - try: - from config.settings import ( - FLASK_HOST, FLASK_PORT, FLASK_DEBUG, SESSION_LIFETIME, - SCHEDULER_INTERVAL, SCHEDULER_ENABLED, SSL_ENABLED - ) - - settings = { - "server": { - "host": FLASK_HOST, - "port": FLASK_PORT, - "debug": FLASK_DEBUG, - "ssl_enabled": SSL_ENABLED - }, - "session": { - "lifetime_minutes": SESSION_LIFETIME.total_seconds() / 60 - }, - "scheduler": { - "interval_seconds": SCHEDULER_INTERVAL, - "enabled": SCHEDULER_ENABLED - } - } - - return jsonify({ - "success": True, - "settings": settings - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Einstellungen: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Laden der Einstellungen: {str(e)}" - }), 500 - -@app.route('/api/admin/settings', methods=['POST']) -@admin_required -def update_admin_settings(): - """Aktualisiert die Admin-Einstellungen""" - try: - data = request.get_json() - - if not data: - return jsonify({ - "success": False, - "message": "Keine Daten empfangen" - }), 400 - - # Hier würden normalerweise die Einstellungen in einer Konfigurationsdatei gespeichert - # Für diese Demo loggen wir nur die Änderungen - app_logger.info(f"Admin-Einstellungen aktualisiert: {data}") - - return jsonify({ - "success": True, - "message": "Einstellungen erfolgreich aktualisiert" - }) - - except Exception as e: - app_logger.error(f"Fehler beim Aktualisieren der Admin-Einstellungen: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Aktualisieren: {str(e)}" - }), 500 - -@app.route('/api/admin/logs/export', methods=['GET']) -@admin_required -def export_admin_logs(): - """Exportiert System-Logs""" - try: - import os - import zipfile - import tempfile - from datetime import datetime - - # Temporäre ZIP-Datei erstellen - temp_dir = tempfile.mkdtemp() - zip_filename = f"myp_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" - zip_path = os.path.join(temp_dir, zip_filename) - - log_dir = os.path.join(os.path.dirname(__file__), 'logs') - - with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - for root, dirs, files in os.walk(log_dir): - for file in files: - if file.endswith('.log'): - file_path = os.path.join(root, file) - arcname = os.path.relpath(file_path, log_dir) - zipf.write(file_path, arcname) - - app_logger.info("System-Logs exportiert") - return send_file(zip_path, as_attachment=True, download_name=zip_filename) - - except Exception as e: - app_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Exportieren: {str(e)}" - }), 500 - -@app.route('/api/logs', methods=['GET']) -@login_required -def get_system_logs(): - """API-Endpunkt zum Laden der System-Logs für das Dashboard.""" - if not current_user.is_admin: - return jsonify({"success": False, "error": "Berechtigung verweigert"}), 403 - - try: - import os - from datetime import datetime - - log_level = request.args.get('log_level', 'all') - log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs') - - # Logeinträge sammeln - app_logs = [] - for category in ['app', 'auth', 'jobs', 'printers', 'scheduler', 'errors']: - log_file = os.path.join(log_dir, category, f'{category}.log') - if os.path.exists(log_file): - try: - with open(log_file, 'r', encoding='utf-8') as f: - lines = f.readlines() - # Nur die letzten 100 Zeilen pro Datei - for line in lines[-100:]: - line = line.strip() - if not line: - continue - - # Log-Level-Filter anwenden - if log_level != 'all': - if log_level.upper() not in line: - continue - - # Log-Eintrag parsen - parts = line.split(' - ') - if len(parts) >= 3: - timestamp = parts[0] - level = parts[1] - message = ' - '.join(parts[2:]) - else: - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - level = 'INFO' - message = line - - app_logs.append({ - 'timestamp': timestamp, - 'level': level, - 'category': category, - 'module': category, - 'message': message, - 'source': category - }) - except Exception as file_error: - app_logger.warning(f"Fehler beim Lesen der Log-Datei {log_file}: {str(file_error)}") - continue - - # Nach Zeitstempel sortieren (neueste zuerst) - try: - logs = sorted(app_logs, key=lambda x: x['timestamp'] if x['timestamp'] else '', reverse=True)[:100] - except: - # Falls Sortierung fehlschlägt, einfach die letzten 100 nehmen - logs = app_logs[-100:] - - app_logger.info(f"Logs erfolgreich geladen: {len(logs)} Einträge") - - return jsonify({ - "success": True, - "logs": logs, - "count": len(logs), - "message": f"{len(logs)} Log-Einträge geladen" - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Logs: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Logs", - "message": str(e), - "logs": [] - }), 500 - -# ===== ENDE FEHLENDE ADMIN-API-ENDPUNKTE ===== - -# ===== BENACHRICHTIGUNGS-API-ENDPUNKTE ===== - -@app.route('/api/notifications', methods=['GET']) -@login_required -def get_notifications(): - """Holt alle Benachrichtigungen für den aktuellen Benutzer""" - try: - db_session = get_db_session() - - # Sicherstellen, dass current_user.id als Integer behandelt wird - user_id = int(current_user.id) - - # Benachrichtigungen für den aktuellen Benutzer laden - notifications = db_session.query(Notification).filter( - Notification.user_id == user_id - ).order_by(Notification.created_at.desc()).limit(50).all() - - notifications_data = [notification.to_dict() for notification in notifications] - - db_session.close() - - return jsonify({ - "success": True, - "notifications": notifications_data - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Benachrichtigungen: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Laden der Benachrichtigungen: {str(e)}" - }), 500 - -@app.route('/api/notifications//read', methods=['POST']) -@login_required -def mark_notification_read(notification_id): - """Markiert eine Benachrichtigung als gelesen""" - try: - db_session = get_db_session() - - # Sicherstellen, dass current_user.id als Integer behandelt wird - user_id = int(current_user.id) - - notification = db_session.query(Notification).filter( - Notification.id == notification_id, - Notification.user_id == user_id - ).first() - - if not notification: - db_session.close() - return jsonify({ - "success": False, - "message": "Benachrichtigung nicht gefunden" - }), 404 - - notification.read = True - db_session.commit() - db_session.close() - - return jsonify({ - "success": True, - "message": "Benachrichtigung als gelesen markiert" - }) - - except Exception as e: - app_logger.error(f"Fehler beim Markieren der Benachrichtigung: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Markieren: {str(e)}" - }), 500 - -@app.route('/api/notifications/mark-all-read', methods=['POST']) -@login_required -def mark_all_notifications_read(): - """Markiert alle Benachrichtigungen als gelesen""" - try: - db_session = get_db_session() - - # Sicherstellen, dass current_user.id als Integer behandelt wird - user_id = int(current_user.id) - - # Alle ungelesenen Benachrichtigungen des Benutzers finden und als gelesen markieren - updated_count = db_session.query(Notification).filter( - Notification.user_id == user_id, - Notification.read == False - ).update({"read": True}) - - db_session.commit() - db_session.close() - - return jsonify({ - "success": True, - "message": f"{updated_count} Benachrichtigungen als gelesen markiert" - }) - - except Exception as e: - app_logger.error(f"Fehler beim Markieren aller Benachrichtigungen: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Markieren: {str(e)}" - }), 500 - -# ===== ENDE BENACHRICHTIGUNGS-API-ENDPUNKTE ===== - -# ===== QUEUE-MANAGER-API-ENDPUNKTE ===== - -@app.route('/api/queue/status', methods=['GET']) -@login_required -def get_queue_status(): - """Gibt den aktuellen Status der Drucker-Warteschlangen zurück.""" - try: - queue_manager = get_queue_manager() - status = queue_manager.get_queue_status() - - return jsonify({ - "success": True, - "queue_status": status - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen des Queue-Status: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim Abrufen des Queue-Status: {str(e)}" - }), 500 - -@app.route('/api/queue/check-now', methods=['POST']) -@login_required -def trigger_queue_check(): - """Triggert eine sofortige Überprüfung der Warteschlangen.""" - try: - # Bestehende check_waiting_jobs API verwenden - return check_waiting_jobs() - - except Exception as e: - app_logger.error(f"Fehler beim manuellen Queue-Check: {str(e)}") - return jsonify({ - "success": False, - "message": f"Fehler beim manuellen Queue-Check: {str(e)}" - }), 500 - -# ===== ENDE QUEUE-MANAGER-API-ENDPUNKTE ===== - - -# ===== NEUE ADMIN API-ROUTEN FÜR BUTTON-FUNKTIONALITÄTEN ===== - -@app.route('/api/admin/maintenance/activate', methods=['POST']) -@admin_required -def activate_maintenance_mode(): - """Aktiviert den Wartungsmodus""" - try: - # Hier würde die Wartungsmodus-Logik implementiert werden - # Zum Beispiel: Setze einen globalen Flag, blockiere neue Jobs, etc. - - # Für Demo-Zwecke simulieren wir die Aktivierung - app_logger.info("Wartungsmodus aktiviert durch Admin") - - return jsonify({ - "success": True, - "message": "Wartungsmodus wurde aktiviert" - }) - except Exception as e: - app_logger.error(f"Fehler beim Aktivieren des Wartungsmodus: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Aktivieren des Wartungsmodus" - }), 500 - -@app.route('/api/admin/maintenance/deactivate', methods=['POST']) -@admin_required -def deactivate_maintenance_mode(): - """Deaktiviert den Wartungsmodus""" - try: - # Hier würde die Wartungsmodus-Deaktivierung implementiert werden - - app_logger.info("Wartungsmodus deaktiviert durch Admin") - - return jsonify({ - "success": True, - "message": "Wartungsmodus wurde deaktiviert" - }) - except Exception as e: - app_logger.error(f"Fehler beim Deaktivieren des Wartungsmodus: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Deaktivieren des Wartungsmodus" - }), 500 - -@app.route('/api/admin/stats/live', methods=['GET']) -@admin_required -def get_live_admin_stats(): - """Liefert Live-Statistiken für das Admin-Dashboard""" - try: - db_session = get_db_session() - - # Benutzer-Statistiken - total_users = db_session.query(func.count(User.id)).scalar() or 0 - - # Drucker-Statistiken - total_printers = db_session.query(func.count(Printer.id)).scalar() or 0 - online_printers = db_session.query(func.count(Printer.id)).filter( - Printer.status.in_(['online', 'idle']) - ).scalar() or 0 - - # Job-Statistiken - active_jobs = db_session.query(func.count(Job.id)).filter( - Job.status == 'running' - ).scalar() or 0 - - queued_jobs = db_session.query(func.count(Job.id)).filter( - Job.status == 'queued' - ).scalar() or 0 - - # Erfolgsrate berechnen - total_jobs = db_session.query(func.count(Job.id)).scalar() or 1 - completed_jobs = db_session.query(func.count(Job.id)).filter( - Job.status == 'completed' - ).scalar() or 0 - - success_rate = round((completed_jobs / total_jobs) * 100, 1) if total_jobs > 0 else 0 - - db_session.close() - - return jsonify({ - "success": True, - "stats": { - "total_users": total_users, - "total_printers": total_printers, - "online_printers": online_printers, - "active_jobs": active_jobs, - "queued_jobs": queued_jobs, - "success_rate": success_rate - } - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Live-Admin-Statistiken: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Statistiken" - }), 500 - -@app.route('/api/admin/system/status', methods=['GET']) -@admin_required -def get_admin_system_status(): - """Liefert detaillierte System-Status-Informationen""" - try: - import psutil - import os - from datetime import datetime, timedelta - - # CPU-Nutzung - cpu_usage = round(psutil.cpu_percent(interval=1), 1) - - # RAM-Nutzung - memory = psutil.virtual_memory() - memory_usage = round(memory.percent, 1) - - # Festplatten-Nutzung - disk = psutil.disk_usage('/') - disk_usage = round((disk.used / disk.total) * 100, 1) - - # System-Uptime - boot_time = datetime.fromtimestamp(psutil.boot_time()) - uptime = datetime.now() - boot_time - uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds//60)%60}m" - - # Datenbankverbindung testen - db_session = get_db_session() - db_status = "Verbunden" - try: - db_session.execute("SELECT 1") - db_session.close() - except: - db_status = "Fehler" - db_session.close() - - return jsonify({ - "success": True, - "status": { - "cpu_usage": cpu_usage, - "memory_usage": memory_usage, - "disk_usage": disk_usage, - "uptime": uptime_str, - "database_status": db_status, - "timestamp": datetime.now().isoformat() - } - }) - - except ImportError: - # Falls psutil nicht verfügbar ist, Dummy-Daten zurückgeben - return jsonify({ - "success": True, - "status": { - "cpu_usage": 15.2, - "memory_usage": 42.8, - "disk_usage": 67.3, - "uptime": "2d 14h 32m", - "database_status": "Verbunden", - "timestamp": datetime.now().isoformat() - } - }) - except Exception as e: - app_logger.error(f"Fehler beim Laden des System-Status: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden des System-Status" - }), 500 - -@app.route('/api/dashboard/stats', methods=['GET']) -@login_required -def get_dashboard_stats(): - """Liefert Dashboard-Statistiken für Hintergrund-Updates""" - try: - db_session = get_db_session() - - # Aktive Jobs zählen - active_jobs_count = db_session.query(func.count(Job.id)).filter( - Job.status == 'running' - ).scalar() or 0 - - # Verfügbare Drucker zählen - available_printers_count = db_session.query(func.count(Printer.id)).filter( - Printer.status.in_(['online', 'idle']) - ).scalar() or 0 - - # Gesamte Jobs zählen - total_jobs_count = db_session.query(func.count(Job.id)).scalar() or 0 - - # Erfolgsrate berechnen - completed_jobs = db_session.query(func.count(Job.id)).filter( - Job.status == 'completed' - ).scalar() or 0 - - success_rate = round((completed_jobs / total_jobs_count) * 100, 1) if total_jobs_count > 0 else 100.0 - - db_session.close() - - return jsonify({ - "success": True, - "active_jobs_count": active_jobs_count, - "available_printers_count": available_printers_count, - "total_jobs_count": total_jobs_count, - "success_rate": success_rate - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Dashboard-Statistiken: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Statistiken" - }), 500 - -@app.route('/api/dashboard/active-jobs', methods=['GET']) -@login_required -def get_dashboard_active_jobs(): - """Liefert aktive Jobs für Dashboard-Updates""" - try: - db_session = get_db_session() - - active_jobs = db_session.query(Job).filter( - Job.status.in_(['running', 'paused']) - ).limit(5).all() - - jobs_data = [] - for job in active_jobs: - jobs_data.append({ - "id": job.id, - "name": job.name, - "status": job.status, - "progress": getattr(job, 'progress', 0), - "printer": job.printer.name if job.printer else 'Unbekannt', - "start_time": job.created_at.strftime('%H:%M') if job.created_at else '--:--' - }) - - db_session.close() - - return jsonify({ - "success": True, - "jobs": jobs_data - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der aktiven Jobs: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der aktiven Jobs" - }), 500 - -@app.route('/api/dashboard/printers', methods=['GET']) -@login_required -def get_dashboard_printers(): - """Liefert Drucker-Status für Dashboard-Updates""" - try: - db_session = get_db_session() - - printers = db_session.query(Printer).limit(5).all() - - printers_data = [] - for printer in printers: - printers_data.append({ - "id": printer.id, - "name": printer.name, - "status": printer.status, - "location": printer.location, - "model": printer.model - }) - - db_session.close() - - return jsonify({ - "success": True, - "printers": printers_data - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Drucker: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Drucker" - }), 500 - -@app.route('/api/dashboard/activities', methods=['GET']) -@login_required -def get_dashboard_activities(): - """Liefert die neuesten Aktivitäten für das Dashboard""" - try: - db_session = get_db_session() - - # Neueste Jobs abrufen - activities = [] - recent_jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(10).all() - - for job in recent_jobs: - activities.append({ - 'description': f"Job '{job.name}' wurde {job.status}", - 'time': job.created_at.strftime('%H:%M'), - 'type': 'job', - 'status': job.status - }) - - db_session.close() - return jsonify({ - 'success': True, - 'activities': activities - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Dashboard-Aktivitäten: {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Fehler beim Laden der Aktivitäten' - }), 500 - -@app.route('/admin/settings', methods=['GET']) -@login_required -@admin_required -def admin_settings(): - """Admin-Einstellungen Seite""" - try: - return render_template('admin_settings.html') - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Einstellungen: {str(e)}") - flash("Fehler beim Laden der Einstellungen", "error") - return redirect(url_for('admin_page')) - -@app.route('/analytics', methods=['GET']) -@login_required -def analytics_page(): - """Analytics Seite""" - try: - return render_template('analytics.html') - except Exception as e: - app_logger.error(f"Fehler beim Laden der Analytics-Seite: {str(e)}") - flash("Fehler beim Laden der Analytics", "error") - return redirect(url_for('dashboard')) - -@app.route('/api/optimization/auto-optimize', methods=['POST']) -@login_required -def auto_optimize_jobs(): - """Automatische Optimierung der Druckaufträge durchführen""" - try: - data = request.get_json() - settings = data.get('settings', {}) - enabled = data.get('enabled', False) - - db_session = get_db_session() - - # Aktuelle Jobs in der Warteschlange abrufen - pending_jobs = db_session.query(Job).filter( - Job.status.in_(['queued', 'pending']) - ).all() - - if not pending_jobs: - db_session.close() - return jsonify({ - 'success': True, - 'message': 'Keine Jobs zur Optimierung verfügbar', - 'optimized_jobs': 0 - }) - - # Verfügbare Drucker abrufen - available_printers = db_session.query(Printer).filter(Printer.active == True).all() - - if not available_printers: - db_session.close() - return jsonify({ - 'success': False, - 'error': 'Keine verfügbaren Drucker für Optimierung' - }) - - # Optimierungs-Algorithmus anwenden - algorithm = settings.get('algorithm', 'round_robin') - optimized_count = 0 - - if algorithm == 'round_robin': - optimized_count = apply_round_robin_optimization(pending_jobs, available_printers, db_session) - elif algorithm == 'load_balance': - optimized_count = apply_load_balance_optimization(pending_jobs, available_printers, db_session) - elif algorithm == 'priority_based': - optimized_count = apply_priority_optimization(pending_jobs, available_printers, db_session) - - db_session.commit() - jobs_logger.info(f"Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert mit Algorithmus {algorithm}") - - # System-Log erstellen - log_entry = SystemLog( - level='INFO', - component='optimization', - message=f'Auto-Optimierung durchgeführt: {optimized_count} Jobs optimiert', - user_id=current_user.id if current_user.is_authenticated else None, - details=json.dumps({ - 'algorithm': algorithm, - 'optimized_jobs': optimized_count, - 'settings': settings - }) - ) - db_session.add(log_entry) - db_session.commit() - db_session.close() - - return jsonify({ - 'success': True, - 'optimized_jobs': optimized_count, - 'algorithm': algorithm, - 'message': f'Optimierung erfolgreich: {optimized_count} Jobs wurden optimiert' - }) - - except Exception as e: - app_logger.error(f"Fehler bei der Auto-Optimierung: {str(e)}") - return jsonify({ - 'success': False, - 'error': f'Optimierung fehlgeschlagen: {str(e)}' - }), 500 - -@app.route('/api/optimization/settings', methods=['GET', 'POST']) -@login_required -def optimization_settings(): - """Optimierungs-Einstellungen abrufen und speichern""" - db_session = get_db_session() - if request.method == 'GET': - try: - # Standard-Einstellungen oder benutzerdefinierte laden - default_settings = { - 'algorithm': 'round_robin', - 'consider_distance': True, - 'minimize_changeover': True, - 'max_batch_size': 10, - 'time_window': 24, - 'auto_optimization_enabled': False - } - - # Benutzereinstellungen aus der Session laden oder Standardwerte verwenden - user_settings = session.get('user_settings', {}) - optimization_settings = user_settings.get('optimization', default_settings) - - # Sicherstellen, dass alle erforderlichen Schlüssel vorhanden sind - for key, value in default_settings.items(): - if key not in optimization_settings: - optimization_settings[key] = value - - return jsonify({ - 'success': True, - 'settings': optimization_settings - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Optimierungs-Einstellungen: {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Fehler beim Laden der Einstellungen' - }), 500 - elif request.method == 'POST': - try: - settings = request.get_json() - - # Validierung der Einstellungen - if not validate_optimization_settings(settings): - return jsonify({ - 'success': False, - 'error': 'Ungültige Optimierungs-Einstellungen' - }), 400 - - # Einstellungen in der Session speichern - user_settings = session.get('user_settings', {}) - if 'optimization' not in user_settings: - user_settings['optimization'] = {} - - # Aktualisiere die Optimierungseinstellungen - user_settings['optimization'].update(settings) - session['user_settings'] = user_settings - - # Einstellungen in der Datenbank speichern, wenn möglich - if hasattr(current_user, 'settings'): - import json - current_user.settings = json.dumps(user_settings) - current_user.updated_at = datetime.now() - db_session.commit() - - app_logger.info(f"Optimierungs-Einstellungen für Benutzer {current_user.id} aktualisiert") - - return jsonify({ - 'success': True, - 'message': 'Optimierungs-Einstellungen erfolgreich gespeichert' - }) - - except Exception as e: - db_session.rollback() - app_logger.error(f"Fehler beim Speichern der Optimierungs-Einstellungen: {str(e)}") - return jsonify({ - 'success': False, - 'error': f'Fehler beim Speichern der Einstellungen: {str(e)}' - }), 500 - -# ===== OPTIMIERUNGS-ALGORITHMUS-FUNKTIONEN ===== - -def apply_round_robin_optimization(jobs, printers, db_session): - """Round-Robin-Optimierung: Gleichmäßige Verteilung der Jobs auf Drucker""" - optimized_count = 0 - printer_index = 0 - - for job in jobs: - if printer_index >= len(printers): - printer_index = 0 - - # Job dem nächsten Drucker zuweisen - job.printer_id = printers[printer_index].id - job.assigned_at = datetime.now() - optimized_count += 1 - printer_index += 1 - - return optimized_count - -def apply_load_balance_optimization(jobs, printers, db_session): - """Load-Balancing-Optimierung: Jobs basierend auf aktueller Auslastung verteilen""" - optimized_count = 0 - - # Aktuelle Drucker-Auslastung berechnen - printer_loads = {} - for printer in printers: - current_jobs = db_session.query(Job).filter( - Job.printer_id == printer.id, - Job.status.in_(['running', 'queued']) - ).count() - printer_loads[printer.id] = current_jobs - - for job in jobs: - # Drucker mit geringster Auslastung finden - min_load_printer_id = min(printer_loads, key=printer_loads.get) - - job.printer_id = min_load_printer_id - job.assigned_at = datetime.now() - - # Auslastung für nächste Iteration aktualisieren - printer_loads[min_load_printer_id] += 1 - optimized_count += 1 - - return optimized_count - -def apply_priority_optimization(jobs, printers, db_session): - """Prioritätsbasierte Optimierung: Jobs nach Priorität und verfügbaren Druckern verteilen""" - optimized_count = 0 - - # Jobs nach Priorität sortieren - priority_order = {'urgent': 1, 'high': 2, 'normal': 3, 'low': 4} - sorted_jobs = sorted(jobs, key=lambda j: priority_order.get(getattr(j, 'priority', 'normal'), 3)) - - # Hochpriorisierte Jobs den besten verfügbaren Druckern zuweisen - printer_assignments = {printer.id: 0 for printer in printers} - - for job in sorted_jobs: - # Drucker mit geringster Anzahl zugewiesener Jobs finden - best_printer_id = min(printer_assignments, key=printer_assignments.get) - - job.printer_id = best_printer_id - job.assigned_at = datetime.now() - - printer_assignments[best_printer_id] += 1 - optimized_count += 1 - - return optimized_count - -def validate_optimization_settings(settings): - """Validiert die Optimierungs-Einstellungen""" - try: - # Algorithmus validieren - valid_algorithms = ['round_robin', 'load_balance', 'priority_based'] - if settings.get('algorithm') not in valid_algorithms: - return False - - # Numerische Werte validieren - max_batch_size = settings.get('max_batch_size', 10) - if not isinstance(max_batch_size, int) or max_batch_size < 1 or max_batch_size > 50: - return False - - time_window = settings.get('time_window', 24) - if not isinstance(time_window, int) or time_window < 1 or time_window > 168: - return False - - return True - - except Exception: - return False - -# ===== ERWEITERTE REFRESH-FUNKTIONEN ===== - -@app.route('/api/dashboard/refresh', methods=['POST']) -@login_required -def refresh_dashboard(): - """Aktualisiert Dashboard-Daten und gibt aktuelle Statistiken zurück""" - try: - db_session = get_db_session() - - # Aktuelle Statistiken abrufen - stats = { - 'active_jobs': db_session.query(Job).filter(Job.status == 'running').count(), - 'available_printers': db_session.query(Printer).filter(Printer.active == True).count(), - 'total_jobs': db_session.query(Job).count(), - 'pending_jobs': db_session.query(Job).filter(Job.status == 'queued').count() - } - - # Erfolgsrate berechnen - total_jobs = stats['total_jobs'] - if total_jobs > 0: - completed_jobs = db_session.query(Job).filter(Job.status == 'completed').count() - stats['success_rate'] = round((completed_jobs / total_jobs) * 100, 1) - else: - stats['success_rate'] = 0 - - db_session.close() - - return jsonify({ - 'success': True, - 'stats': stats, - 'timestamp': datetime.now().isoformat() - }) - - except Exception as e: - app_logger.error(f"Fehler beim Dashboard-Refresh: {str(e)}") - return jsonify({ - 'success': False, - 'error': 'Fehler beim Aktualisieren der Dashboard-Daten' - }), 500 - -# ===== ADMIN GASTAUFTRÄGE API-ENDPUNKTE ===== - -@app.route('/api/admin/guest-requests/test', methods=['GET']) -def test_admin_guest_requests(): - """Test-Endpunkt für Guest Requests Routing""" - app_logger.info("Test-Route /api/admin/guest-requests/test aufgerufen") - return jsonify({ - 'success': True, - 'message': 'Test-Route funktioniert', - 'user_authenticated': current_user.is_authenticated, - 'user_is_admin': current_user.is_admin if current_user.is_authenticated else False - }) - -@app.route('/api/admin/guest-requests', methods=['GET']) -@admin_required -def get_admin_guest_requests(): - """Gibt alle Gastaufträge für Admin-Verwaltung zurück""" - try: - app_logger.info(f"API-Aufruf /api/admin/guest-requests von User {current_user.id if current_user.is_authenticated else 'Anonymous'}") - - db_session = get_db_session() - - # Parameter auslesen - status = request.args.get('status', 'all') - limit = int(request.args.get('limit', 50)) - offset = int(request.args.get('offset', 0)) - search = request.args.get('search', '') - - # Basis-Query - query = db_session.query(GuestRequest) - - # Status-Filter - if status != 'all': - query = query.filter(GuestRequest.status == status) - - # Suchfilter - if search: - search_term = f"%{search}%" - query = query.filter( - (GuestRequest.name.ilike(search_term)) | - (GuestRequest.email.ilike(search_term)) | - (GuestRequest.file_name.ilike(search_term)) | - (GuestRequest.reason.ilike(search_term)) - ) - - # Gesamtanzahl vor Pagination - total = query.count() - - # Sortierung und Pagination - requests = query.order_by(GuestRequest.created_at.desc()).offset(offset).limit(limit).all() - - # Statistiken berechnen - stats = { - 'total': db_session.query(GuestRequest).count(), - 'pending': db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count(), - 'approved': db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count(), - 'rejected': db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count(), - } - - # Requests zu Dictionary konvertieren - requests_data = [] - for req in requests: - # Priorität berechnen - now = datetime.now() - hours_old = (now - req.created_at).total_seconds() / 3600 if req.created_at else 0 - is_urgent = hours_old > 24 and req.status == 'pending' - - request_data = { - 'id': req.id, - 'name': req.name, - 'email': req.email, - 'file_name': req.file_name, - 'file_path': req.file_path, - 'duration_minutes': req.duration_minutes, - 'copies': req.copies, - 'reason': req.reason, - 'status': req.status, - 'created_at': req.created_at.isoformat() if req.created_at else None, - 'updated_at': req.updated_at.isoformat() if req.updated_at else None, - 'approved_at': req.approved_at.isoformat() if req.approved_at else None, - 'rejected_at': req.rejected_at.isoformat() if req.rejected_at else None, - 'approval_notes': req.approval_notes, - 'rejection_reason': req.rejection_reason, - 'is_urgent': is_urgent, - 'hours_old': round(hours_old, 1) - } - requests_data.append(request_data) - - db_session.close() - - app_logger.info(f"Admin-Gastaufträge geladen: {len(requests_data)} von {total} (Status: {status})") - - return jsonify({ - 'success': True, - 'requests': requests_data, - 'stats': stats, - 'total': total, - 'offset': offset, - 'limit': limit, - 'has_more': offset + limit < total - }) - - except Exception as e: - app_logger.error(f"Fehler beim Laden der Admin-Gastaufträge: {str(e)}", exc_info=True) - return jsonify({ - 'success': False, - 'message': f'Fehler beim Laden der Gastaufträge: {str(e)}' - }), 500 - -@app.route('/api/guest-requests//approve', methods=['POST']) -@admin_required -def approve_guest_request(request_id): - """Genehmigt einen Gastauftrag""" - try: - db_session = get_db_session() - - guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first() - - if not guest_request: - db_session.close() - return jsonify({ - 'success': False, - 'message': 'Gastauftrag nicht gefunden' - }), 404 - - if guest_request.status != 'pending': - db_session.close() - return jsonify({ - 'success': False, - 'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht genehmigt werden' - }), 400 - - # Daten aus Request Body - data = request.get_json() or {} - notes = data.get('notes', '') - printer_id = data.get('printer_id') - - # Status aktualisieren - guest_request.status = 'approved' - guest_request.approved_at = datetime.now() - guest_request.approved_by = current_user.id - guest_request.approval_notes = notes - guest_request.updated_at = datetime.now() - - # Falls Drucker zugewiesen werden soll - if printer_id: - printer = db_session.query(Printer).filter(Printer.id == printer_id).first() - if printer: - guest_request.assigned_printer_id = printer_id - - # OTP-Code generieren für den Gast - import secrets - otp_code = ''.join([str(secrets.randbelow(10)) for _ in range(6)]) - guest_request.otp_code = otp_code - guest_request.otp_expires_at = datetime.now() + timedelta(hours=24) - - db_session.commit() - - # Benachrichtigung an den Gast senden (falls E-Mail verfügbar) - if guest_request.email: - try: - # Hier würde normalerweise eine E-Mail gesendet werden - app_logger.info(f"E-Mail-Benachrichtigung würde an {guest_request.email} gesendet (OTP: {otp_code})") - except Exception as e: - app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}") - - db_session.close() - - app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt (OTP: {otp_code})") - - return jsonify({ - 'success': True, - 'message': 'Gastauftrag erfolgreich genehmigt', - 'otp_code': otp_code, - 'expires_at': (datetime.now() + timedelta(hours=24)).isoformat() - }) - - except Exception as e: - app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Genehmigen: {str(e)}' - }), 500 - -@app.route('/api/guest-requests//reject', methods=['POST']) -@admin_required -def reject_guest_request(request_id): - """Lehnt einen Gastauftrag ab""" - try: - db_session = get_db_session() - - guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first() - - if not guest_request: - db_session.close() - return jsonify({ - 'success': False, - 'message': 'Gastauftrag nicht gefunden' - }), 404 - - if guest_request.status != 'pending': - db_session.close() - return jsonify({ - 'success': False, - 'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht abgelehnt werden' - }), 400 - - # Daten aus Request Body - data = request.get_json() or {} - reason = data.get('reason', '').strip() - - if not reason: - db_session.close() - return jsonify({ - 'success': False, - 'message': 'Ablehnungsgrund ist erforderlich' - }), 400 - - # Status aktualisieren - guest_request.status = 'rejected' - guest_request.rejected_at = datetime.now() - guest_request.rejected_by = current_user.id - guest_request.rejection_reason = reason - guest_request.updated_at = datetime.now() - - db_session.commit() - - # Benachrichtigung an den Gast senden (falls E-Mail verfügbar) - if guest_request.email: - try: - # Hier würde normalerweise eine E-Mail gesendet werden - app_logger.info(f"Ablehnungs-E-Mail würde an {guest_request.email} gesendet (Grund: {reason})") - except Exception as e: - app_logger.warning(f"Fehler beim Senden der Ablehnungs-E-Mail: {str(e)}") - - db_session.close() - - app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} abgelehnt (Grund: {reason})") - - return jsonify({ - 'success': True, - 'message': 'Gastauftrag erfolgreich abgelehnt' - }) - - except Exception as e: - app_logger.error(f"Fehler beim Ablehnen des Gastauftrags {request_id}: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Ablehnen: {str(e)}' - }), 500 - -@app.route('/api/guest-requests/', methods=['DELETE']) -@admin_required -def delete_guest_request(request_id): - """Löscht einen Gastauftrag""" - try: - db_session = get_db_session() - - guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first() - - if not guest_request: - db_session.close() - return jsonify({ - 'success': False, - 'message': 'Gastauftrag nicht gefunden' - }), 404 - - # Datei löschen falls vorhanden - if guest_request.file_path and os.path.exists(guest_request.file_path): - try: - os.remove(guest_request.file_path) - app_logger.info(f"Datei {guest_request.file_path} für Gastauftrag {request_id} gelöscht") - except Exception as e: - app_logger.warning(f"Fehler beim Löschen der Datei: {str(e)}") - - # Gastauftrag aus Datenbank löschen - request_name = guest_request.name - db_session.delete(guest_request) - db_session.commit() - db_session.close() - - app_logger.info(f"Gastauftrag {request_id} ({request_name}) von Admin {current_user.id} gelöscht") - - return jsonify({ - 'success': True, - 'message': 'Gastauftrag erfolgreich gelöscht' - }) - - except Exception as e: - app_logger.error(f"Fehler beim Löschen des Gastauftrags {request_id}: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Löschen: {str(e)}' - }), 500 - -@app.route('/api/guest-requests/', methods=['GET']) -@admin_required -def get_guest_request_detail(request_id): - """Gibt Details eines spezifischen Gastauftrags zurück""" - try: - db_session = get_db_session() - - guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first() - - if not guest_request: - db_session.close() - return jsonify({ - 'success': False, - 'message': 'Gastauftrag nicht gefunden' - }), 404 - - # Detaildaten zusammenstellen - request_data = { - 'id': guest_request.id, - 'name': guest_request.name, - 'email': guest_request.email, - 'file_name': guest_request.file_name, - 'file_path': guest_request.file_path, - 'file_size': None, - 'duration_minutes': guest_request.duration_minutes, - 'copies': guest_request.copies, - 'reason': guest_request.reason, - 'status': guest_request.status, - 'created_at': guest_request.created_at.isoformat() if guest_request.created_at else None, - 'updated_at': guest_request.updated_at.isoformat() if guest_request.updated_at else None, - 'approved_at': guest_request.approved_at.isoformat() if guest_request.approved_at else None, - 'rejected_at': guest_request.rejected_at.isoformat() if guest_request.rejected_at else None, - 'approval_notes': guest_request.approval_notes, - 'rejection_reason': guest_request.rejection_reason, - 'otp_code': guest_request.otp_code, - 'otp_expires_at': guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None, - 'author_ip': guest_request.author_ip - } - - # Dateigröße ermitteln - if guest_request.file_path and os.path.exists(guest_request.file_path): - try: - file_size = os.path.getsize(guest_request.file_path) - request_data['file_size'] = file_size - request_data['file_size_mb'] = round(file_size / (1024 * 1024), 2) - except Exception as e: - app_logger.warning(f"Fehler beim Ermitteln der Dateigröße: {str(e)}") - - # Bearbeiter-Informationen hinzufügen - if guest_request.approved_by: - approved_by_user = db_session.query(User).filter(User.id == guest_request.approved_by).first() - if approved_by_user: - request_data['approved_by_name'] = approved_by_user.name or approved_by_user.username - - if guest_request.rejected_by: - rejected_by_user = db_session.query(User).filter(User.id == guest_request.rejected_by).first() - if rejected_by_user: - request_data['rejected_by_name'] = rejected_by_user.name or rejected_by_user.username - - # Zugewiesener Drucker - if hasattr(guest_request, 'assigned_printer_id') and guest_request.assigned_printer_id: - assigned_printer = db_session.query(Printer).filter(Printer.id == guest_request.assigned_printer_id).first() - if assigned_printer: - request_data['assigned_printer'] = { - 'id': assigned_printer.id, - 'name': assigned_printer.name, - 'location': assigned_printer.location, - 'status': assigned_printer.status - } - - db_session.close() - - return jsonify({ - 'success': True, - 'request': request_data - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Details {request_id}: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Abrufen der Details: {str(e)}' - }), 500 - -@app.route('/api/admin/guest-requests/stats', methods=['GET']) -@admin_required -def get_guest_requests_stats(): - """Gibt detaillierte Statistiken zu Gastaufträgen zurück""" - try: - db_session = get_db_session() - - # Basis-Statistiken - total = db_session.query(GuestRequest).count() - pending = db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count() - approved = db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count() - rejected = db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count() - - # Zeitbasierte Statistiken - today = datetime.now().date() - week_ago = datetime.now() - timedelta(days=7) - month_ago = datetime.now() - timedelta(days=30) - - today_requests = db_session.query(GuestRequest).filter( - func.date(GuestRequest.created_at) == today - ).count() - - week_requests = db_session.query(GuestRequest).filter( - GuestRequest.created_at >= week_ago - ).count() - - month_requests = db_session.query(GuestRequest).filter( - GuestRequest.created_at >= month_ago - ).count() - - # Dringende Requests (älter als 24h und pending) - urgent_cutoff = datetime.now() - timedelta(hours=24) - urgent_requests = db_session.query(GuestRequest).filter( - GuestRequest.status == 'pending', - GuestRequest.created_at < urgent_cutoff - ).count() - - # Durchschnittliche Bearbeitungszeit - avg_processing_time = None - try: - processed_requests = db_session.query(GuestRequest).filter( - GuestRequest.status.in_(['approved', 'rejected']), - GuestRequest.updated_at.isnot(None) - ).all() - - if processed_requests: - total_time = sum([ - (req.updated_at - req.created_at).total_seconds() - for req in processed_requests - if req.updated_at and req.created_at - ]) - avg_processing_time = round(total_time / len(processed_requests) / 3600, 2) # Stunden - except Exception as e: - app_logger.warning(f"Fehler beim Berechnen der durchschnittlichen Bearbeitungszeit: {str(e)}") - - # Erfolgsrate - success_rate = 0 - if approved + rejected > 0: - success_rate = round((approved / (approved + rejected)) * 100, 1) - - stats = { - 'total': total, - 'pending': pending, - 'approved': approved, - 'rejected': rejected, - 'urgent': urgent_requests, - 'today': today_requests, - 'week': week_requests, - 'month': month_requests, - 'success_rate': success_rate, - 'avg_processing_time_hours': avg_processing_time, - 'completion_rate': round(((approved + rejected) / total * 100), 1) if total > 0 else 0 - } - - db_session.close() - - return jsonify({ - 'success': True, - 'stats': stats, - 'generated_at': datetime.now().isoformat() - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Statistiken: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Abrufen der Statistiken: {str(e)}' - }), 500 - -@app.route('/api/admin/guest-requests/export', methods=['GET']) -@admin_required -def export_guest_requests(): - """Exportiert Gastaufträge als CSV""" - try: - db_session = get_db_session() - - # Filter-Parameter - status = request.args.get('status', 'all') - start_date = request.args.get('start_date') - end_date = request.args.get('end_date') - - # Query aufbauen - query = db_session.query(GuestRequest) - - if status != 'all': - query = query.filter(GuestRequest.status == status) - - if start_date: - try: - start_dt = datetime.fromisoformat(start_date) - query = query.filter(GuestRequest.created_at >= start_dt) - except ValueError: - pass - - if end_date: - try: - end_dt = datetime.fromisoformat(end_date) - query = query.filter(GuestRequest.created_at <= end_dt) - except ValueError: - pass - - requests = query.order_by(GuestRequest.created_at.desc()).all() - - # CSV-Daten erstellen - import csv - import io - - output = io.StringIO() - writer = csv.writer(output) - - # Header - writer.writerow([ - 'ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt am', - 'Dauer (Min)', 'Kopien', 'Begründung', 'Genehmigt am', - 'Abgelehnt am', 'Bearbeitungsnotizen', 'Ablehnungsgrund' - ]) - - # Daten - for req in requests: - writer.writerow([ - req.id, - req.name or '', - req.email or '', - req.file_name or '', - req.status, - req.created_at.strftime('%Y-%m-%d %H:%M:%S') if req.created_at else '', - req.duration_minutes or '', - req.copies or '', - req.reason or '', - req.approved_at.strftime('%Y-%m-%d %H:%M:%S') if req.approved_at else '', - req.rejected_at.strftime('%Y-%m-%d %H:%M:%S') if req.rejected_at else '', - req.approval_notes or '', - req.rejection_reason or '' - ]) - - db_session.close() - - # Response erstellen - output_value = output.getvalue() - output.close() - - response = make_response(output_value) - response.headers["Content-Disposition"] = f"attachment; filename=gastauftraege_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" - response.headers["Content-Type"] = "text/csv; charset=utf-8" - - app_logger.info(f"Gastaufträge-Export erstellt: {len(requests)} Einträge") - - return response - - except Exception as e: - app_logger.error(f"Fehler beim Exportieren der Gastaufträge: {str(e)}") - return jsonify({ - 'success': False, - 'message': f'Fehler beim Export: {str(e)}' - }), 500 - -# ===== ENDE ADMIN GASTAUFTRÄGE API-ENDPUNKTE ===== - -@app.route("/api/user/settings/auto-logout", methods=["GET"]) -@login_required -def get_auto_logout_settings(): - """Holt nur die Auto-Logout-Einstellungen des Benutzers""" - try: - user_settings = session.get('user_settings', {}) - auto_logout = user_settings.get('privacy', {}).get('auto_logout', 60) - - return jsonify({ - "success": True, - "auto_logout": auto_logout - }) - - except Exception as e: - user_logger.error(f"Fehler beim Laden der Auto-Logout-Einstellungen: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Laden der Auto-Logout-Einstellungen" - }), 500 - -@app.route("/api/user/setting", methods=["PATCH"]) -@login_required -def update_single_setting(): - """Aktualisiert eine einzelne Benutzereinstellung""" - try: - if not request.is_json: - return jsonify({"error": "Anfrage muss im JSON-Format sein"}), 400 - - data = request.get_json() - if not data: - return jsonify({"error": "Keine Daten erhalten"}), 400 - - # Aktuelle Einstellungen laden - user_settings = session.get('user_settings', { - "theme": "system", - "reduced_motion": False, - "contrast": "normal", - "notifications": { - "new_jobs": True, - "job_updates": True, - "system": True, - "email": False - }, - "privacy": { - "activity_logs": True, - "two_factor": False, - "auto_logout": 60 - } - }) - - # Einzelne Einstellung aktualisieren - for key, value in data.items(): - if key == "auto_logout": - # Validierung für Auto-Logout - try: - timeout = int(value) if value != "never" else "never" - if timeout != "never" and (timeout < 5 or timeout > 480): - return jsonify({"error": "Auto-Logout muss zwischen 5 und 480 Minuten liegen"}), 400 - user_settings.setdefault('privacy', {})['auto_logout'] = timeout - except (ValueError, TypeError): - return jsonify({"error": "Ungültiger Auto-Logout-Wert"}), 400 - - user_settings['last_updated'] = datetime.now().isoformat() - session['user_settings'] = user_settings - - user_logger.info(f"Benutzer {current_user.username} hat Einstellung '{key}' aktualisiert") - - return jsonify({ - "success": True, - "message": "Einstellung erfolgreich aktualisiert" - }) - - except Exception as e: - user_logger.error(f"Fehler beim Aktualisieren der Einzeleinstellung: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Aktualisieren der Einstellung" - }), 500 - -@app.route("/api/auth/keep-alive", methods=["POST"]) -@login_required -def keep_alive(): - """Keep-Alive-Endpunkt für Auto-Logout-System""" - try: - # Session-Timestamp aktualisieren - session.permanent = True - session.modified = True - - auth_logger.info(f"Keep-Alive für Benutzer {current_user.username}") - - return jsonify({ - "success": True, - "message": "Session verlängert", - "timestamp": datetime.now().isoformat() - }) - - except Exception as e: - auth_logger.error(f"Fehler beim Keep-Alive: {str(e)}") - return jsonify({ - "success": False, - "error": "Fehler beim Verlängern der Session" - }), 500 - -# ===== FILE-UPLOAD-ROUTEN ===== - -@app.route('/api/upload/job', methods=['POST']) -@login_required -def upload_job_file(): - """ - Lädt eine Datei für einen Druckjob hoch - - Form Data: - file: Die hochzuladende Datei - job_name: Name des Jobs (optional) - """ - try: - if 'file' not in request.files: - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - file = request.files['file'] - job_name = request.form.get('job_name', '') - - if file.filename == '': - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - # Metadaten für die Datei - metadata = { - 'uploader_id': current_user.id, - 'uploader_name': current_user.username, - 'job_name': job_name - } - - # Datei speichern - result = save_job_file(file, current_user.id, metadata) - - if result: - relative_path, absolute_path, file_metadata = result - - app_logger.info(f"Job-Datei hochgeladen: {file_metadata['original_filename']} von User {current_user.id}") - - return jsonify({ - 'success': True, - 'message': 'Datei erfolgreich hochgeladen', - 'file_path': relative_path, - 'filename': file_metadata['original_filename'], - 'unique_filename': file_metadata['unique_filename'], - 'file_size': file_metadata['file_size'], - 'metadata': file_metadata - }) - else: - return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Hochladen der Job-Datei: {str(e)}") - return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 - -@app.route('/api/upload/guest', methods=['POST']) -def upload_guest_file(): - """ - Lädt eine Datei für einen Gastauftrag hoch - - Form Data: - file: Die hochzuladende Datei - guest_name: Name des Gasts (optional) - guest_email: E-Mail des Gasts (optional) - """ - try: - if 'file' not in request.files: - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - file = request.files['file'] - guest_name = request.form.get('guest_name', '') - guest_email = request.form.get('guest_email', '') - - if file.filename == '': - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - # Metadaten für die Datei - metadata = { - 'guest_name': guest_name, - 'guest_email': guest_email - } - - # Datei speichern - result = save_guest_file(file, metadata) - - if result: - relative_path, absolute_path, file_metadata = result - - app_logger.info(f"Gast-Datei hochgeladen: {file_metadata['original_filename']} für {guest_name or 'Unbekannt'}") - - return jsonify({ - 'success': True, - 'message': 'Datei erfolgreich hochgeladen', - 'file_path': relative_path, - 'filename': file_metadata['original_filename'], - 'unique_filename': file_metadata['unique_filename'], - 'file_size': file_metadata['file_size'], - 'metadata': file_metadata - }) - else: - return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Hochladen der Gast-Datei: {str(e)}") - return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 - -@app.route('/api/upload/avatar', methods=['POST']) -@login_required -def upload_avatar(): - """ - Lädt ein Avatar-Bild für den aktuellen Benutzer hoch - - Form Data: - file: Das Avatar-Bild - """ - try: - if 'file' not in request.files: - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - file = request.files['file'] - - if file.filename == '': - return jsonify({'error': 'Keine Datei ausgewählt'}), 400 - - # Nur Bilder erlauben - allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'} - if not file.filename or '.' not in file.filename: - return jsonify({'error': 'Ungültiger Dateityp'}), 400 - - file_ext = file.filename.rsplit('.', 1)[1].lower() - if file_ext not in allowed_extensions: - return jsonify({'error': 'Nur Bilddateien sind erlaubt (PNG, JPG, JPEG, GIF, WebP)'}), 400 - - # Alte Avatar-Datei löschen falls vorhanden - db_session = get_db_session() - user = db_session.query(User).get(current_user.id) - if user and user.avatar_path: - delete_file_safe(user.avatar_path) - - # Neue Avatar-Datei speichern - result = save_avatar_file(file, current_user.id) - - if result: - relative_path, absolute_path, file_metadata = result - - # Benutzer-Avatar-Pfad in Datenbank aktualisieren - user.avatar_path = relative_path - db_session.commit() - db_session.close() - - app_logger.info(f"Avatar hochgeladen für User {current_user.id}: {file_metadata['original_filename']}") - - return jsonify({ - 'success': True, - 'message': 'Avatar erfolgreich hochgeladen', - 'avatar_path': relative_path, - 'filename': file_metadata['original_filename'], - 'file_size': file_metadata['file_size'] - }) - else: - db_session.close() - return jsonify({'error': 'Fehler beim Speichern des Avatars'}), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Hochladen des Avatars: {str(e)}") - return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 - -@app.route('/api/files/', methods=['GET']) -@login_required -def serve_uploaded_file(file_path): - """ - Stellt hochgeladene Dateien bereit (mit Zugriffskontrolle) - """ - try: - # Datei-Info abrufen - file_info = file_manager.get_file_info(file_path) - - if not file_info: - return jsonify({'error': 'Datei nicht gefunden'}), 404 - - # Zugriffskontrolle basierend auf Dateikategorie - if file_path.startswith('jobs/'): - # Job-Dateien: Nur Besitzer und Admins - if not current_user.is_admin: - # Prüfen ob Benutzer der Besitzer ist - if f"user_{current_user.id}" not in file_path: - return jsonify({'error': 'Zugriff verweigert'}), 403 - - elif file_path.startswith('guests/'): - # Gast-Dateien: Nur Admins - if not current_user.is_admin: - return jsonify({'error': 'Zugriff verweigert'}), 403 - - elif file_path.startswith('avatars/'): - # Avatar-Dateien: Öffentlich zugänglich für angemeldete Benutzer - pass - - else: - # Andere Dateien: Nur Admins - if not current_user.is_admin: - return jsonify({'error': 'Zugriff verweigert'}), 403 - - # Datei bereitstellen - return send_file(file_info['absolute_path'], as_attachment=False) - - except Exception as e: - app_logger.error(f"Fehler beim Bereitstellen der Datei {file_path}: {str(e)}") - return jsonify({'error': 'Fehler beim Laden der Datei'}), 500 - -@app.route('/api/files/', methods=['DELETE']) -@login_required -def delete_uploaded_file(file_path): - """ - Löscht eine hochgeladene Datei (mit Zugriffskontrolle) - """ - try: - # Zugriffskontrolle - if file_path.startswith('jobs/'): - if not current_user.is_admin and f"user_{current_user.id}" not in file_path: - return jsonify({'error': 'Zugriff verweigert'}), 403 - elif not current_user.is_admin: - return jsonify({'error': 'Zugriff verweigert'}), 403 - - # Datei löschen - success = delete_file_safe(file_path) - - if success: - app_logger.info(f"Datei gelöscht: {file_path} von User {current_user.id}") - return jsonify({'success': True, 'message': 'Datei erfolgreich gelöscht'}) - else: - return jsonify({'error': 'Datei konnte nicht gelöscht werden'}), 500 - - except Exception as e: - app_logger.error(f"Fehler beim Löschen der Datei {file_path}: {str(e)}") - return jsonify({'error': f'Fehler beim Löschen: {str(e)}'}), 500 - -@app.route('/api/admin/files/stats', methods=['GET']) -@admin_required -def get_file_statistics(): - """ - Gibt Statistiken über alle hochgeladenen Dateien zurück (nur für Admins) - """ - try: - stats = file_manager.get_category_stats() - - # Gesamtstatistiken berechnen - total_files = sum(cat_stats['file_count'] for cat_stats in stats.values()) - total_size = sum(cat_stats['total_size'] for cat_stats in stats.values()) - total_size_mb = round(total_size / (1024 * 1024), 2) - - return jsonify({ - 'success': True, - 'categories': stats, - 'totals': { - 'file_count': total_files, - 'total_size': total_size, - 'total_size_mb': total_size_mb - } - }) - - except Exception as e: - app_logger.error(f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}") - return jsonify({'error': f'Fehler beim Abrufen der Statistiken: {str(e)}'}), 500 - -@app.route('/api/admin/files/cleanup', methods=['POST']) -@admin_required -def cleanup_temp_files(): - """ - Räumt temporäre Dateien auf (nur für Admins) - """ - try: - max_age_hours = request.json.get('max_age_hours', 24) - deleted_count = file_manager.cleanup_temp_files(max_age_hours) - - app_logger.info(f"Temporäre Dateien aufgeräumt: {deleted_count} Dateien gelöscht (älter als {max_age_hours}h)") - - return jsonify({ - 'success': True, - 'message': f'{deleted_count} temporäre Dateien gelöscht', - 'deleted_count': deleted_count - }) - - except Exception as e: - app_logger.error(f"Fehler beim Aufräumen temporärer Dateien: {str(e)}") - return jsonify({'error': f'Fehler beim Aufräumen: {str(e)}'}), 500 - -# ===== FEHLENDE DRUCKER-SPEZIFISCHE API-ENDPOINTS ===== - -@app.route("/api/printers//jobs", methods=["GET"]) -@login_required -def get_printer_jobs(printer_id): - """Gibt alle Jobs für einen spezifischen Drucker zurück.""" - try: - db_session = get_db_session() - - # Prüfen ob Drucker existiert - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Jobs für diesen Drucker abrufen - jobs = db_session.query(Job).filter(Job.printer_id == printer_id).order_by(Job.created_at.desc()).all() - - jobs_data = [] - for job in jobs: - job_data = { - "id": job.id, - "title": job.title, - "status": job.status, - "priority": job.priority, - "created_at": job.created_at.isoformat() if job.created_at else None, - "scheduled_time": job.scheduled_time.isoformat() if job.scheduled_time else None, - "started_at": job.started_at.isoformat() if job.started_at else None, - "finished_at": job.finished_at.isoformat() if job.finished_at else None, - "estimated_duration": job.estimated_duration, - "user_id": job.user_id, - "printer_id": job.printer_id, - "printer_name": printer.name - } - jobs_data.append(job_data) - - db_session.close() - - return jsonify({ - "jobs": jobs_data, - "total": len(jobs_data), - "printer": { - "id": printer.id, - "name": printer.name, - "status": printer.status - } - }) - - except Exception as e: - printers_logger.error(f"Fehler beim Abrufen der Jobs für Drucker {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/printers//stats", methods=["GET"]) -@login_required -def get_printer_stats(printer_id): - """Gibt Statistiken für einen spezifischen Drucker zurück.""" - try: - db_session = get_db_session() - - # Prüfen ob Drucker existiert - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # Statistiken berechnen - total_jobs = db_session.query(Job).filter(Job.printer_id == printer_id).count() - completed_jobs = db_session.query(Job).filter( - Job.printer_id == printer_id, - Job.status == "completed" - ).count() - failed_jobs = db_session.query(Job).filter( - Job.printer_id == printer_id, - Job.status == "failed" - ).count() - active_jobs = db_session.query(Job).filter( - Job.printer_id == printer_id, - Job.status.in_(["scheduled", "running"]) - ).count() - - # Durchschnittliche Job-Dauer berechnen - avg_duration_result = db_session.query(func.avg(Job.estimated_duration)).filter( - Job.printer_id == printer_id, - Job.status == "completed", - Job.estimated_duration.isnot(None) - ).scalar() - - avg_duration = round(avg_duration_result, 2) if avg_duration_result else 0 - - # Erfolgsrate berechnen - success_rate = round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0 - - # Letzte Aktivität - last_job = db_session.query(Job).filter(Job.printer_id == printer_id).order_by(Job.created_at.desc()).first() - last_activity = last_job.created_at.isoformat() if last_job and last_job.created_at else None - - db_session.close() - - stats_data = { - "printer": { - "id": printer.id, - "name": printer.name, - "status": printer.status, - "location": printer.location - }, - "jobs": { - "total": total_jobs, - "completed": completed_jobs, - "failed": failed_jobs, - "active": active_jobs, - "success_rate": success_rate - }, - "performance": { - "average_duration": avg_duration, - "last_activity": last_activity - }, - "generated_at": datetime.now().isoformat() - } - - return jsonify(stats_data) - - except Exception as e: - printers_logger.error(f"Fehler beim Abrufen der Statistiken für Drucker {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/api/printers//test", methods=["POST"]) -@login_required -def test_printer_connection(printer_id): - """Testet die Verbindung zu einem spezifischen Drucker.""" - try: - db_session = get_db_session() - - # Prüfen ob Drucker existiert - printer = db_session.query(Printer).get(printer_id) - if not printer: - db_session.close() - return jsonify({"error": "Drucker nicht gefunden"}), 404 - - # IP-Adresse für Test ermitteln - ip_to_test = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None) - - if not ip_to_test: - db_session.close() - return jsonify({ - "success": False, - "error": "Keine IP-Adresse für Drucker konfiguriert", - "printer": { - "id": printer.id, - "name": printer.name - } - }), 400 - - # Verbindungstest durchführen - printers_logger.info(f"Teste Verbindung zu Drucker {printer.name} (ID: {printer_id}) auf IP {ip_to_test}") - - status, active = check_printer_status(ip_to_test, timeout=10) - - # Status in Datenbank aktualisieren - printer.status = "available" if status == "online" else "offline" - if hasattr(printer, 'active'): - printer.active = active - db_session.commit() - - test_result = { - "success": status == "online", - "status": status, - "active": active, - "ip_address": ip_to_test, - "printer": { - "id": printer.id, - "name": printer.name, - "location": printer.location, - "model": printer.model - }, - "test_time": datetime.now().isoformat(), - "message": f"Drucker ist {'online und erreichbar' if status == 'online' else 'offline oder nicht erreichbar'}" - } - - db_session.close() - - printers_logger.info(f"Verbindungstest für Drucker {printer.name}: {status}") - - return jsonify(test_result) - - except Exception as e: - printers_logger.error(f"Fehler beim Testen der Verbindung zu Drucker {printer_id}: {str(e)}") - return jsonify({ - "success": False, - "error": "Interner Serverfehler beim Verbindungstest", - "details": str(e) - }), 500 - -# ===== ADMIN-SPEZIFISCHE DRUCKER-ENDPOINTS ===== - -@app.route("/api/admin/printers/create", methods=["POST"]) -@login_required -@admin_required -def admin_create_printer_api(): - """Admin-Endpoint zum Erstellen neuer Drucker.""" - try: - data = request.get_json() - - if not data: - return jsonify({"error": "Keine Daten empfangen"}), 400 - - # Pflichtfelder prüfen - required_fields = ["name", "plug_ip"] - for field in required_fields: - if field not in data or not data[field]: - return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400 - - db_session = get_db_session() - - # Prüfen, ob bereits ein Drucker mit diesem Namen existiert - existing_printer = db_session.query(Printer).filter(Printer.name == data["name"]).first() - if existing_printer: - db_session.close() - return jsonify({"error": "Ein Drucker mit diesem Namen existiert bereits"}), 400 - - # Neuen Drucker erstellen - new_printer = Printer( - name=data["name"], - model=data.get("model", ""), - location=data.get("location", ""), - mac_address=data.get("mac_address", ""), - plug_ip=data["plug_ip"], - status="offline", - active=True, - created_at=datetime.now() - ) - - db_session.add(new_printer) - db_session.commit() - - # Sofortiger Status-Check - if new_printer.plug_ip: - status, active = check_printer_status(new_printer.plug_ip) - new_printer.status = "available" if status == "online" else "offline" - new_printer.active = active - db_session.commit() - - printer_data = { - "id": new_printer.id, - "name": new_printer.name, - "model": new_printer.model, - "location": new_printer.location, - "mac_address": new_printer.mac_address, - "plug_ip": new_printer.plug_ip, - "status": new_printer.status, - "active": new_printer.active, - "created_at": new_printer.created_at.isoformat() - } - - db_session.close() - - printers_logger.info(f"Admin {current_user.name} hat Drucker '{new_printer.name}' erstellt") - - return jsonify({ - "success": True, - "message": "Drucker erfolgreich erstellt", - "printer": printer_data - }), 201 - - except Exception as e: - printers_logger.error(f"Fehler beim Erstellen eines Druckers durch Admin: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - -@app.route("/debug/tapo-test", methods=["GET"]) -@login_required -@admin_required -def debug_tapo_test(): - """ - DEBUG-Route: Testet alle bekannten Tapo-Steckdosen-IPs direkt. - """ - from config.settings import TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS - - results = [] - - # Alle bekannten IPs testen - test_ips = [ - "192.168.1.100", "192.168.1.101", "192.168.1.102", - "192.168.1.103", "192.168.1.104", "192.168.1.105", - "192.168.0.100", "192.168.0.101", "192.168.0.102", - "192.168.0.103", "192.168.0.104", "192.168.0.105" - ] - - app_logger.info(f"🔍 DEBUG: Teste {len(test_ips)} Tapo-Steckdosen-IPs...") - - for ip in test_ips: - result = { - "ip": ip, - "ping_success": False, - "tapo_success": False, - "device_on": False, - "device_info": None, - "error": None - } - - # 1. Ping-Test - try: - import subprocess - ping_result = subprocess.run( - ['ping', '-n', '1', '-w', '1000', ip], - capture_output=True, - text=True, - timeout=2 - ) - result["ping_success"] = ping_result.returncode == 0 - except Exception as e: - result["error"] = f"Ping-Fehler: {str(e)}" - - # 2. Tapo-Test (nur wenn Ping erfolgreich) - if result["ping_success"]: - try: - from PyP100 import PyP110 - - p110 = PyP110.P110(ip, TAPO_USERNAME, TAPO_PASSWORD) - p110.handshake() - p110.login() - - device_info = p110.getDeviceInfo() - result["tapo_success"] = True - result["device_on"] = device_info.get('device_on', False) - result["device_info"] = { - "nickname": device_info.get('nickname', 'Unbekannt'), - "model": device_info.get('model', 'Unbekannt'), - "device_id": device_info.get('device_id', 'Unbekannt'), - "fw_ver": device_info.get('fw_ver', 'Unbekannt') - } - - except Exception as e: - result["error"] = f"Tapo-Fehler: {str(e)}" - - results.append(result) - app_logger.info(f" {ip}: Ping={result['ping_success']}, Tapo={result['tapo_success']}, Ein={result['device_on']}") - - # HTML-Response für bessere Darstellung - html = """ - - - - Tapo-Steckdosen Debug-Test - - - -

🔌 Tapo-Steckdosen Debug-Test

-

Benutzername: """ + TAPO_USERNAME + """

-

Passwort: """ + ("*" * len(TAPO_PASSWORD)) + """

-
- - - - - - - - - - - - """ - - found_count = 0 - online_count = 0 - - for result in results: - if result["tapo_success"]: - found_count += 1 - if result["device_on"]: - online_count += 1 - - # Zeilen-CSS-Klasse basierend auf Status - if result["tapo_success"]: - if result["device_on"]: - row_class = "status-on" - status_text = "🟢 EIN" - else: - row_class = "status-off" - status_text = "🔴 AUS" - else: - row_class = "status-error" - status_text = "❌ FEHLER" - - html += f""" - - - - - - - - - - """ - - html += f""" -
IP-AdressePingTapo-VerbindungStatusGerätenameModellFehler
{result['ip']}{'✅' if result['ping_success'] else '❌'}{'✅' if result['tapo_success'] else '❌'}{status_text}{result['device_info']['nickname'] if result['device_info'] else '-'}{result['device_info']['model'] if result['device_info'] else '-'}{result['error'] or '-'}
- -
-

📊 Zusammenfassung

-

Gefundene Steckdosen: {found_count}

-

Eingeschaltete Steckdosen: {online_count}

-

Ausgeschaltete Steckdosen: {found_count - online_count}

- -
-

🔧 Nächste Schritte

-
    -
  • Gefundene Steckdosen in der Datenbank als Drucker anlegen
  • -
  • Plugin-IPs der vorhandenen Drucker auf die gefundenen IPs setzen
  • -
  • Tapo-Anmeldedaten in den Drucker-Einstellungen speichern
  • -
- -

← Zurück zum Admin-Dashboard

- - - """ - - return html - # ===== STARTUP UND MAIN ===== if __name__ == "__main__": import sys diff --git a/backend/app/database/myp.db-shm b/backend/app/database/myp.db-shm index 2c8d62d4..0229cdd6 100644 Binary files a/backend/app/database/myp.db-shm and b/backend/app/database/myp.db-shm differ diff --git a/backend/app/fix_duplicates.py b/backend/app/fix_duplicates.py index 0519ecba..a91e6a78 100644 --- a/backend/app/fix_duplicates.py +++ b/backend/app/fix_duplicates.py @@ -1 +1,56 @@ - \ No newline at end of file +#!/usr/bin/env python3 +""" +Script zur Entfernung der Duplikationen in app.py +Behält nur die erste Occurrence von jeder Funktion/Route. +""" + +def fix_app_duplicates(): + """Entfernt Duplikationen in app.py und behält nur die erste Version.""" + + # Lese die Originaldatei + with open('app.py', 'r', encoding='utf-8') as f: + lines = f.readlines() + + print(f"Original: {len(lines)} Zeilen") + + # Finde die Zeile mit der ersten Duplikation (Login Manager) + duplicate_start = None + main_section_start = None + + for i, line in enumerate(lines): + # Suche nach der doppelten Login Manager Definition + if duplicate_start is None and i > 1000 and "# Login-Manager initialisieren" in line: + duplicate_start = i + print(f"Duplikation beginnt bei Zeile {i+1}: {line.strip()}") + + # Suche nach Main-Sektion + if "# ===== STARTUP UND MAIN =====" in line: + main_section_start = i + print(f"Main-Sektion beginnt bei Zeile {i+1}: {line.strip()}") + break + + if duplicate_start and main_section_start: + # Erstelle neue Datei ohne Duplikation + new_lines = [] + + # Alles bis zur Duplikation + new_lines.extend(lines[:duplicate_start]) + + # Main-Sektion und alles danach + new_lines.extend(lines[main_section_start:]) + + print(f"Bereinigt: {len(new_lines)} Zeilen") + print(f"Entfernt: {len(lines) - len(new_lines)} Zeilen") + + # Schreibe bereinigte Datei + with open('app.py', 'w', encoding='utf-8') as f: + f.writelines(new_lines) + + print("✅ Duplikationen erfolgreich entfernt!") + return True + else: + print("❌ Duplikations-Marker nicht gefunden!") + return False + +if __name__ == "__main__": + fix_app_duplicates() \ No newline at end of file