""" Vereinheitlichter Admin-Blueprint für das MYP 3D-Druck-Management-System Konsolidierte Implementierung aller Admin-spezifischen Funktionen: - Benutzerverwaltung und Systemüberwachung (ursprünglich admin.py) - Erweiterte System-API-Funktionen (ursprünglich admin_api.py) - System-Backups, Datenbank-Optimierung, Cache-Verwaltung - Steckdosenschaltzeiten-Übersicht und -verwaltung Optimierungen: - Vereinheitlichter admin_required Decorator - Konsistente Fehlerbehandlung und Logging - Vollständige API-Kompatibilität zu beiden ursprünglichen Blueprints Autor: MYP Team - Konsolidiert für IHK-Projektarbeit Datum: 2025-06-09 """ import os import shutil import zipfile import sqlite3 import glob import time import traceback from datetime import datetime, timedelta from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, current_app from flask_login import login_required, current_user from functools import wraps from models import User, Printer, Job, get_cached_session, Stats, SystemLog, PlugStatusLog, GuestRequest, UserPermission from utils.logging_config import get_logger # ===== BLUEPRINT-KONFIGURATION ===== # Haupt-Blueprint für Admin-UI (Templates) admin_blueprint = Blueprint('admin', __name__, url_prefix='/admin') # API-Blueprint für erweiterte System-Funktionen admin_api_blueprint = Blueprint('admin_api', __name__, url_prefix='/api/admin') # Logger für beide Funktionsbereiche admin_logger = get_logger("admin") admin_api_logger = get_logger("admin_api") # ===== EINHEITLICHER ADMIN-DECORATOR ===== def admin_required(f): """ Vereinheitlichter Decorator für Admin-Berechtigung. Kombiniert die beste Praxis aus beiden ursprünglichen Implementierungen: - Umfassende Logging-Funktionalität von admin.py - Robuste Authentifizierungsprüfung von admin_api.py """ @wraps(f) @login_required def decorated_function(*args, **kwargs): # Detaillierte Authentifizierungsprüfung is_authenticated = current_user.is_authenticated user_id = current_user.id if is_authenticated else 'Anonymous' # Doppelte Admin-Prüfung für maximale Sicherheit is_admin = False if is_authenticated: # Methode 1: Property-basierte Prüfung (admin.py-Stil) is_admin = hasattr(current_user, 'is_admin') and current_user.is_admin # Methode 2: Role-basierte Prüfung (admin_api.py-Stil) als Fallback if not is_admin and hasattr(current_user, 'role'): is_admin = current_user.role == 'admin' # Umfassendes Logging admin_logger.info( f"Admin-Check für Funktion {f.__name__}: " f"User authenticated: {is_authenticated}, " f"User ID: {user_id}, " f"Is Admin: {is_admin}" ) if not is_admin: admin_logger.warning( f"Admin-Zugriff verweigert für User {user_id} auf Funktion {f.__name__}" ) return jsonify({ "error": "Nur Administratoren haben Zugriff", "message": "Admin-Berechtigung erforderlich" }), 403 return f(*args, **kwargs) return decorated_function # ===== ADMIN-UI ROUTEN (ursprünglich admin.py) ===== @admin_blueprint.route("/") @admin_required def admin_dashboard(): """Admin-Dashboard-Hauptseite mit Systemstatistiken""" try: with get_cached_session() as db_session: # Grundlegende Statistiken sammeln total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Admin-Dashboard geladen von {current_user.username}") return render_template('admin.html', stats=stats, active_tab=None) except Exception as e: admin_logger.error(f"Fehler beim Laden des Admin-Dashboards: {str(e)}") flash("Fehler beim Laden der Dashboard-Daten", "error") return render_template('admin.html', stats={}, active_tab=None) @admin_blueprint.route("/plug-schedules") @admin_required def admin_plug_schedules(): """ Administrator-Übersicht für Steckdosenschaltzeiten. Zeigt detaillierte Historie aller Smart Plug Schaltzeiten mit Kalenderansicht. """ admin_logger.info(f"Admin {current_user.username} (ID: {current_user.id}) öffnet Steckdosenschaltzeiten") try: # Statistiken für die letzten 24 Stunden abrufen stats_24h = PlugStatusLog.get_status_statistics(hours=24) # Alle Drucker für Filter-Dropdown with get_cached_session() as db_session: # Alle Drucker für Auswahlfelder anzeigen (unabhängig von active-Status) printers = db_session.query(Printer).all() return render_template('admin_plug_schedules.html', stats=stats_24h, printers=printers, page_title="Steckdosenschaltzeiten", breadcrumb=[ {"name": "Admin-Dashboard", "url": url_for("admin.admin_dashboard")}, {"name": "Steckdosenschaltzeiten", "url": "#"} ]) except Exception as e: admin_logger.error(f"Fehler beim Laden der Steckdosenschaltzeiten-Seite: {str(e)}") flash("Fehler beim Laden der Steckdosenschaltzeiten-Daten.", "error") return redirect(url_for("admin.admin_dashboard")) @admin_blueprint.route("/users") @admin_required def users_overview(): """Benutzerübersicht für Administratoren""" try: with get_cached_session() as db_session: # Alle Benutzer laden users = db_session.query(User).order_by(User.created_at.desc()).all() # Grundlegende Statistiken sammeln total_users = len(users) total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Benutzerübersicht geladen von {current_user.username}") return render_template('admin.html', stats=stats, users=users, active_tab='users') except Exception as e: admin_logger.error(f"Fehler beim Laden der Benutzerübersicht: {str(e)}") flash("Fehler beim Laden der Benutzerdaten", "error") return render_template('admin.html', stats={}, users=[], active_tab='users') @admin_blueprint.route("/users/add", methods=["GET"]) @admin_required def add_user_page(): """Seite zum Hinzufügen eines neuen Benutzers""" return render_template('admin_add_user.html') @admin_blueprint.route("/users//edit", methods=["GET"]) @admin_required def edit_user_page(user_id): """Seite zum Bearbeiten eines Benutzers""" try: with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: flash("Benutzer nicht gefunden", "error") return redirect(url_for('admin.users_overview')) return render_template('admin_edit_user.html', user=user) except Exception as e: admin_logger.error(f"Fehler beim Laden der Benutzer-Bearbeitung: {str(e)}") flash("Fehler beim Laden der Benutzerdaten", "error") return redirect(url_for('admin.users_overview')) @admin_blueprint.route("/users/create", methods=["POST"]) @admin_required def create_user(): """Erstellt einen neuen Benutzer mit erweiterten Feldern und Berechtigungen""" try: # === GRUNDDATEN === username = request.form.get('username', '').strip() email = request.form.get('email', '').strip() name = request.form.get('name', '').strip() password = request.form.get('password', '') role = request.form.get('role', 'user') # === ERWEITERTE PROFIL-INFORMATIONEN === department = request.form.get('department', '').strip() position = request.form.get('position', '').strip() phone = request.form.get('phone', '').strip() bio = request.form.get('bio', '').strip() # === BENUTZEREINSTELLUNGEN === theme_preference = request.form.get('theme_preference', 'auto') language_preference = request.form.get('language_preference', 'de') email_notifications = request.form.get('email_notifications') == 'on' browser_notifications = request.form.get('browser_notifications') == 'on' # === GRANULARE BERECHTIGUNGEN === can_start_jobs = request.form.get('can_start_jobs') == 'on' needs_approval = request.form.get('needs_approval') == 'on' can_approve_jobs = request.form.get('can_approve_jobs') == 'on' can_manage_printers = request.form.get('can_manage_printers') == 'on' can_view_all_jobs = request.form.get('can_view_all_jobs') == 'on' can_access_admin_panel = request.form.get('can_access_admin_panel') == 'on' can_manage_users = request.form.get('can_manage_users') == 'on' can_access_energy_monitoring = request.form.get('can_access_energy_monitoring') == 'on' # === VALIDIERUNG === if not username or not email or not password: flash("Benutzername, E-Mail und Passwort sind erforderlich", "error") return redirect(url_for('admin.add_user_page')) if len(password) < 6: flash("Das Passwort muss mindestens 6 Zeichen lang sein", "error") return redirect(url_for('admin.add_user_page')) with get_cached_session() as db_session: # Prüfen ob Benutzer bereits existiert existing_user = db_session.query(User).filter( (User.username == username) | (User.email == email) ).first() if existing_user: flash("Benutzer mit diesem Namen oder E-Mail existiert bereits", "error") return redirect(url_for('admin.add_user_page')) # === NEUEN BENUTZER ERSTELLEN === new_user = User( username=username, email=email, name=name if name else username, role=role, active=True, created_at=datetime.now(), updated_at=datetime.now(), # Erweiterte Profil-Felder department=department if department else None, position=position if position else None, phone=phone if phone else None, bio=bio if bio else None, # Benutzereinstellungen theme_preference=theme_preference, language_preference=language_preference, email_notifications=email_notifications, browser_notifications=browser_notifications, dashboard_layout='default', compact_mode=False, show_completed_jobs=True, auto_refresh_interval=30, auto_logout_timeout=0 ) # Passwort hashen new_user.set_password(password) # User zur Session hinzufügen und committen db_session.add(new_user) db_session.flush() # Flush um user.id zu erhalten # === BERECHTIGUNGEN ERSTELLEN === # Bei Admin-Rolle automatisch alle Berechtigungen setzen if role == 'admin': can_approve_jobs = True can_manage_printers = True can_view_all_jobs = True can_access_admin_panel = True can_manage_users = True can_access_energy_monitoring = True needs_approval = False # Admins brauchen keine Genehmigung user_permissions = UserPermission( user_id=new_user.id, can_start_jobs=can_start_jobs, needs_approval=needs_approval, can_approve_jobs=can_approve_jobs, can_manage_printers=can_manage_printers, can_view_all_jobs=can_view_all_jobs, can_access_admin_panel=can_access_admin_panel, can_manage_users=can_manage_users, can_access_energy_monitoring=can_access_energy_monitoring, created_at=datetime.now(), updated_at=datetime.now() ) db_session.add(user_permissions) db_session.commit() admin_logger.info(f"Neuer Benutzer '{username}' (Rolle: {role}) mit erweiterten Berechtigungen erstellt von {current_user.username}") flash(f"Benutzer '{username}' erfolgreich erstellt mit allen Einstellungen und Berechtigungen", "success") return redirect(url_for('admin.users_overview')) except Exception as e: admin_logger.error(f"Fehler beim Erstellen des Benutzers: {str(e)}") admin_logger.error(f"Traceback: {traceback.format_exc()}") flash("Fehler beim Erstellen des Benutzers", "error") return redirect(url_for('admin.add_user_page')) @admin_blueprint.route("/users//update", methods=["POST"]) @admin_required def update_user(user_id): """Aktualisiert einen vorhandenen Benutzer""" try: with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: flash("Benutzer nicht gefunden", "error") return redirect(url_for('admin.users_overview')) # Aktualisierung verhindern wenn es der einzige Admin ist if user.is_admin and request.form.get('role') != 'admin': admin_count = db_session.query(User).filter(User.role == 'admin').count() if admin_count <= 1: flash("Kann den letzten Administrator nicht degradieren", "error") return redirect(url_for('admin.edit_user_page', user_id=user_id)) # Form-Daten abrufen username = request.form.get('username', '').strip() email = request.form.get('email', '').strip() name = request.form.get('name', '').strip() role = request.form.get('role', 'user') department = request.form.get('department', '').strip() position = request.form.get('position', '').strip() phone = request.form.get('phone', '').strip() active = request.form.get('active') == 'on' new_password = request.form.get('new_password', '').strip() # Validierung if not username or not email: flash("Benutzername und E-Mail sind erforderlich", "error") return redirect(url_for('admin.edit_user_page', user_id=user_id)) # Prüfen ob Username/Email bereits von anderem Benutzer verwendet existing_user = db_session.query(User).filter( User.id != user_id, (User.username == username) | (User.email == email) ).first() if existing_user: flash("Benutzername oder E-Mail bereits von anderem Benutzer verwendet", "error") return redirect(url_for('admin.edit_user_page', user_id=user_id)) # Benutzer-Daten aktualisieren user.username = username user.email = email user.name = name if name else None user.role = role user.department = department if department else None user.position = position if position else None user.phone = phone if phone else None user.active = active user.updated_at = datetime.now() # Passwort aktualisieren falls angegeben if new_password: user.set_password(new_password) db_session.commit() admin_logger.info(f"Benutzer '{username}' (ID: {user_id}) aktualisiert von {current_user.username}") flash(f"Benutzer '{username}' erfolgreich aktualisiert", "success") return redirect(url_for('admin.users_overview')) except Exception as e: admin_logger.error(f"Fehler beim Aktualisieren des Benutzers: {str(e)}") flash("Fehler beim Aktualisieren des Benutzers", "error") return redirect(url_for('admin.edit_user_page', user_id=user_id)) @admin_blueprint.route("/users//delete", methods=["POST"]) @admin_required def delete_user(user_id): """Löscht einen Benutzer""" try: with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: flash("Benutzer nicht gefunden", "error") return redirect(url_for('admin.users_overview')) # Sich selbst löschen verhindern if user.id == current_user.id: flash("Sie können sich nicht selbst löschen", "error") return redirect(url_for('admin.users_overview')) # Letzten Admin löschen verhindern if user.is_admin: admin_count = db_session.query(User).filter(User.role == 'admin').count() if admin_count <= 1: flash("Kann den letzten Administrator nicht löschen", "error") return redirect(url_for('admin.users_overview')) username = user.username # Benutzer löschen db_session.delete(user) db_session.commit() admin_logger.info(f"Benutzer '{username}' (ID: {user_id}) gelöscht von {current_user.username}") flash(f"Benutzer '{username}' erfolgreich gelöscht", "success") return redirect(url_for('admin.users_overview')) except Exception as e: admin_logger.error(f"Fehler beim Löschen des Benutzers: {str(e)}") flash("Fehler beim Löschen des Benutzers", "error") return redirect(url_for('admin.users_overview')) @admin_blueprint.route("/printers/add") @admin_required def add_printer_page(): """Seite zum Hinzufügen eines neuen Druckers""" try: with get_cached_session() as db_session: # Grundlegende Statistiken für Template 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_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Drucker-Hinzufügen-Seite aufgerufen von {current_user.username}") return render_template('admin_add_printer.html', stats=stats, active_tab='printers') except Exception as e: admin_logger.error(f"Fehler beim Laden der Drucker-Hinzufügen-Seite: {str(e)}") flash("Fehler beim Laden der Seite", "error") return redirect(url_for('admin.admin_dashboard')) @admin_blueprint.route("/printers//edit") @admin_required def edit_printer_page(printer_id): """Seite zum Bearbeiten eines Druckers""" try: from utils.hardware_integration import get_drucker_steuerung with get_cached_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: flash("Drucker nicht gefunden", "error") return redirect(url_for('admin.admin_dashboard')) # Hardware-Status für diesen Drucker abrufen drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() # Erweiterte Drucker-Informationen printer_data = { 'id': printer.id, 'name': printer.name, 'model': printer.model, 'location': printer.location, 'ip_address': printer.ip_address, 'mac_address': printer.mac_address, 'plug_ip': printer.plug_ip, 'status': printer.status, 'active': printer.active, 'created_at': printer.created_at, 'last_checked': printer.last_checked, # Hardware-Status 'plug_online': False, 'plug_power_state': 'unknown', 'energy_usage': {} } if printer.id in status_data.get('drucker_status', {}): hw_status = status_data['drucker_status'][printer.id] printer_data.update({ 'plug_online': hw_status.get('plug_online', False), 'plug_power_state': hw_status.get('plug_state', 'unknown'), 'energy_usage': hw_status.get('energy_usage', {}), 'last_seen': hw_status.get('last_seen') }) # Aktive Jobs für diesen Drucker active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(['pending', 'printing', 'scheduled']) ).all() # Grundlegende Statistiken total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() all_active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': all_active_jobs } admin_logger.info(f"Drucker-Bearbeitung für '{printer.name}' aufgerufen von {current_user.username}") return render_template('admin_edit_printer.html', printer=printer_data, active_jobs=active_jobs, stats=stats, active_tab='printers') except Exception as e: admin_logger.error(f"Fehler beim Laden der Drucker-Bearbeitung: {str(e)}") flash("Fehler beim Laden der Druckerdaten", "error") return redirect(url_for('admin.admin_dashboard')) @admin_blueprint.route("/printers//manage") @admin_required def manage_printer_page(printer_id): """Vollständige Drucker-Verwaltungsseite - entspricht managePrinter() JavaScript-Funktion""" try: from utils.hardware_integration import get_drucker_steuerung with get_cached_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: flash("Drucker nicht gefunden", "error") return redirect(url_for('admin.admin_dashboard')) # Hardware-Steuerung für Echtzeit-Daten drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() # Umfassende Drucker-Verwaltungsdaten sammeln management_data = { # Grunddaten 'printer': printer.to_dict(), # Hardware-Status 'hardware_status': status_data.get('drucker_status', {}).get(printer.id, {}), # Jobs 'active_jobs': [], 'recent_jobs': [], 'job_statistics': {}, # Energieverbrauch 'energy_stats': {}, # Wartungsinfo 'maintenance_info': {}, # Verfügbare Aktionen 'available_actions': [] } # Aktive Jobs active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(['pending', 'printing', 'scheduled']) ).all() management_data['active_jobs'] = [job.to_dict() for job in active_jobs] # Letzte 10 Jobs recent_jobs = db_session.query(Job).filter( Job.printer_id == printer.id ).order_by(Job.created_at.desc()).limit(10).all() management_data['recent_jobs'] = [job.to_dict() for job in recent_jobs] # Job-Statistiken 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.in_(['failed', 'cancelled']) ).count() management_data['job_statistics'] = { 'total_jobs': len(recent_jobs), 'active_jobs': len(active_jobs), 'completed_jobs': completed_jobs, 'failed_jobs': failed_jobs } # Verfügbare Aktionen bestimmen available_actions = ['edit', 'view_settings'] if printer.plug_ip: hw_status = management_data['hardware_status'] if hw_status.get('plug_online', False): if hw_status.get('plug_state') == 'on': available_actions.append('power_off') else: available_actions.append('power_on') available_actions.append('test_connection') if len(active_jobs) == 0: available_actions.append('schedule_maintenance') available_actions.extend(['view_logs', 'export_data', 'delete']) management_data['available_actions'] = available_actions # Grundlegende Statistiken für Template total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() all_active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': all_active_jobs } admin_logger.info(f"Drucker-Verwaltung für '{printer.name}' aufgerufen von {current_user.username}") return render_template('admin_manage_printer.html', management_data=management_data, stats=stats, active_tab='printers') except Exception as e: admin_logger.error(f"Fehler beim Laden der Drucker-Verwaltung: {str(e)}") flash("Fehler beim Laden der Drucker-Verwaltung", "error") return redirect(url_for('admin.admin_dashboard')) @admin_blueprint.route("/printers//settings") @admin_required def printer_settings_page(printer_id): """Drucker-Einstellungsseite - entspricht showPrinterSettings() JavaScript-Funktion""" try: from utils.hardware_integration import get_drucker_steuerung with get_cached_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: flash("Drucker nicht gefunden", "error") return redirect(url_for('admin.admin_dashboard')) # Hardware-Status für erweiterte Einstellungen drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() # Umfassende Einstellungsdaten settings_data = { # Grundeinstellungen 'basic_settings': { 'name': printer.name, 'model': printer.model, 'location': printer.location, 'active': printer.active, 'status': printer.status }, # Netzwerkeinstellungen 'network_settings': { 'ip_address': printer.ip_address, 'mac_address': printer.mac_address, 'plug_ip': printer.plug_ip }, # Hardware-Einstellungen 'hardware_settings': status_data.get('drucker_status', {}).get(printer.id, {}), # Erweiterte Einstellungen 'advanced_settings': { 'auto_power_management': printer.plug_ip is not None, 'monitoring_enabled': True, 'maintenance_mode': printer.status == 'maintenance' }, # Verfügbare Einstellungskategorien 'setting_categories': [ 'basic', 'network', 'hardware', 'power_management', 'monitoring', 'maintenance', 'security' ] } # Grundlegende Statistiken 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_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Drucker-Einstellungen für '{printer.name}' aufgerufen von {current_user.username}") return render_template('admin_printer_settings.html', printer=printer, settings_data=settings_data, stats=stats, active_tab='printers') except Exception as e: admin_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}") flash("Fehler beim Laden der Drucker-Einstellungen", "error") return redirect(url_for('admin.admin_dashboard')) @admin_blueprint.route("/printers//configure") @admin_required def printer_configuration_page(printer_id): """Erweiterte Drucker-Konfigurationsseite - entspricht showPrinterConfiguration() JavaScript-Funktion""" try: from utils.hardware_integration import get_drucker_steuerung with get_cached_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: flash("Drucker nicht gefunden", "error") return redirect(url_for('admin.admin_dashboard')) # Hardware-Status für erweiterte Konfiguration drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() # Umfassende Konfigurationsdaten sammeln config_data = { # Grunddaten des Druckers 'printer': { 'id': printer.id, 'name': printer.name, 'model': printer.model, 'location': printer.location, 'ip_address': printer.ip_address, 'plug_ip': printer.plug_ip, 'status': printer.status, 'active': printer.active, 'created_at': printer.created_at, 'updated_at': printer.updated_at, 'last_checked': printer.last_checked, 'description': getattr(printer, 'description', ''), }, # Hardware-Status und Energiemonitoring 'hardware_status': status_data.get('drucker_status', {}).get(printer.id, {}), # Konfigurationskategorien 'config_categories': { 'basic': { 'name': 'Grundeinstellungen', 'description': 'Name, Modell, Standort', 'icon': 'cog' }, 'network': { 'name': 'Netzwerk-Konfiguration', 'description': 'IP-Adressen, Verbindungseinstellungen', 'icon': 'wifi' }, 'hardware': { 'name': 'Hardware-Integration', 'description': 'Smart-Plug, Sensoren, Monitoring', 'icon': 'cpu' }, 'automation': { 'name': 'Automatisierung', 'description': 'Auto-Power, Zeitpläne, Benachrichtigungen', 'icon': 'zap' }, 'maintenance': { 'name': 'Wartung & Diagnostik', 'description': 'Kalibrierung, Tests, Logs', 'icon': 'tool' }, 'advanced': { 'name': 'Erweiterte Optionen', 'description': 'Experten-Einstellungen, Debug-Modi', 'icon': 'settings' } }, # Live-Verbindungsstatus 'connectivity': { 'printer_reachable': False, 'plug_reachable': False, 'last_ping': None, 'response_time': None }, # Energieverbrauchshistorie 'energy_history': [], # Verfügbare Aktionen 'available_actions': [ 'test_connection', 'reset_settings', 'calibrate', 'factory_reset', 'export_config', 'import_config' ] } # Live-Konnektivitätstests try: if printer.plug_ip: reachable, plug_status = drucker_steuerung.check_outlet_status(printer.plug_ip, printer_id=printer.id) config_data['connectivity']['plug_reachable'] = reachable config_data['connectivity']['plug_status'] = plug_status except Exception as connectivity_error: admin_logger.warning(f"Konnektivitätstest für Drucker {printer.id} fehlgeschlagen: {str(connectivity_error)}") # Grundlegende Statistiken für das Template 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_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Drucker-Konfiguration für '{printer.name}' aufgerufen von {current_user.username}") return render_template('admin_printer_configuration.html', config_data=config_data, stats=stats, active_tab='printers') except Exception as e: admin_logger.error(f"Fehler beim Laden der Drucker-Konfiguration: {str(e)}") flash("Fehler beim Laden der Drucker-Konfiguration", "error") return redirect(url_for('admin.admin_dashboard')) @admin_blueprint.route("/guest-requests") @admin_required def guest_requests(): """Gäste-Anfragen-Übersicht""" return render_template('admin_guest_requests.html') @admin_blueprint.route("/advanced-settings") @admin_required def advanced_settings(): """Erweiterte Systemeinstellungen""" try: admin_logger.info(f"Erweiterte Einstellungen werden geladen von {current_user.username}") with get_cached_session() as db_session: # Grundlegende Statistiken sammeln für das Template total_users = db_session.query(User).count() admin_logger.debug(f"Benutzer gezählt: {total_users}") total_printers = db_session.query(Printer).count() admin_logger.debug(f"Drucker gezählt: {total_printers}") total_jobs = db_session.query(Job).count() admin_logger.debug(f"Jobs gezählt: {total_jobs}") # Aktive Drucker zählen (online/verfügbar) active_printers = db_session.query(Printer).filter( Printer.status.in_(['online', 'available', 'idle']) ).count() admin_logger.debug(f"Aktive Drucker gezählt: {active_printers}") # Wartende Jobs zählen pending_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'scheduled', 'queued']) ).count() admin_logger.debug(f"Wartende Jobs gezählt: {pending_jobs}") stats = { 'total_users': total_users, 'total_printers': total_printers, 'active_printers': active_printers, 'total_jobs': total_jobs, 'pending_jobs': pending_jobs } # Standard-Optimierungseinstellungen für das Template optimization_settings = { 'algorithm': 'round_robin', 'consider_distance': True, 'minimize_changeover': True, 'auto_optimization_enabled': False, 'max_batch_size': 10, 'time_window': 24 } # Wartungs-Informationen für das Template maintenance_info = { 'last_backup': 'Noch kein Backup erstellt', 'log_files_count': '12 Dateien', 'cache_size': '45.2 MB' } admin_logger.info(f"Template wird gerendert mit stats: {stats}") return render_template('admin_advanced_settings.html', stats=stats, optimization_settings=optimization_settings, maintenance_info=maintenance_info, active_tab='system') except Exception as e: admin_logger.error(f"Fehler beim Laden der erweiterten Einstellungen: {str(e)}") admin_logger.error(f"Traceback: {traceback.format_exc()}") flash("Fehler beim Laden der Systemdaten", "error") # Fallback mit leeren Statistiken stats = { 'total_users': 0, 'total_printers': 0, 'active_printers': 0, 'total_jobs': 0, 'pending_jobs': 0 } # Fallback-Optimierungseinstellungen optimization_settings = { 'algorithm': 'round_robin', 'consider_distance': True, 'minimize_changeover': True, 'auto_optimization_enabled': False, 'max_batch_size': 10, 'time_window': 24 } # Fallback-Wartungs-Informationen maintenance_info = { 'last_backup': 'Fehler beim Laden', 'log_files_count': 'Unbekannt', 'cache_size': 'Unbekannt' } return render_template('admin_advanced_settings.html', stats=stats, optimization_settings=optimization_settings, maintenance_info=maintenance_info, active_tab='system') @admin_blueprint.route("/system-health") @admin_required def system_health(): """System-Gesundheitsstatus""" try: with get_cached_session() as db_session: # Grundlegende Statistiken sammeln total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"System-Health geladen von {current_user.username}") return render_template('admin.html', stats=stats, active_tab='system') except Exception as e: admin_logger.error(f"Fehler beim Laden des System-Health: {str(e)}") flash("Fehler beim Laden der System-Daten", "error") return render_template('admin.html', stats={}, active_tab='system') @admin_blueprint.route("/logs") @admin_required def logs_overview(): """System-Logs-Übersicht""" try: with get_cached_session() as db_session: # Grundlegende Statistiken sammeln total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() # Neueste Logs laden (falls SystemLog Model existiert) try: recent_logs = db_session.query(SystemLog).order_by(SystemLog.timestamp.desc()).limit(50).all() except Exception: recent_logs = [] stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Logs-Übersicht geladen von {current_user.username}") return render_template('admin.html', stats=stats, logs=recent_logs, active_tab='logs') except Exception as e: admin_logger.error(f"Fehler beim Laden der Logs-Übersicht: {str(e)}") flash("Fehler beim Laden der Log-Daten", "error") return render_template('admin.html', stats={}, logs=[], active_tab='logs') @admin_blueprint.route("/maintenance") @admin_required def maintenance(): """Wartungsseite""" try: with get_cached_session() as db_session: # Grundlegende Statistiken sammeln total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() stats = { 'total_users': total_users, 'total_printers': total_printers, 'total_jobs': total_jobs, 'active_jobs': active_jobs } admin_logger.info(f"Wartungsseite geladen von {current_user.username}") return render_template('admin.html', stats=stats, active_tab='maintenance') except Exception as e: admin_logger.error(f"Fehler beim Laden der Wartungsseite: {str(e)}") flash("Fehler beim Laden der Wartungsdaten", "error") return render_template('admin.html', stats={}, active_tab='maintenance') # ===== BENUTZER-CRUD-API (ursprünglich admin.py) ===== @admin_api_blueprint.route("/users", methods=["POST"]) @admin_required def create_user_api(): """API-Endpunkt zum Erstellen eines neuen Benutzers""" try: data = request.get_json() # Validierung der erforderlichen Felder required_fields = ['username', 'email', 'password', 'name'] for field in required_fields: if field not in data or not data[field]: return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400 with get_cached_session() as db_session: # Überprüfung auf bereits existierende Benutzer existing_user = db_session.query(User).filter( (User.username == data['username']) | (User.email == data['email']) ).first() if existing_user: return jsonify({"error": "Benutzername oder E-Mail bereits vergeben"}), 400 # Neuen Benutzer erstellen new_user = User( username=data['username'], email=data['email'], name=data['name'], role=data.get('role', 'user'), department=data.get('department'), position=data.get('position'), phone=data.get('phone'), bio=data.get('bio') ) new_user.set_password(data['password']) db_session.add(new_user) db_session.flush() # ID generieren für UserPermission # Granulare Berechtigungen erstellen from models import UserPermission permissions = UserPermission( user_id=new_user.id, can_start_jobs=data.get('can_start_jobs', True), # Standard: kann Jobs starten needs_approval=data.get('needs_approval', False), # Standard: keine Genehmigung nötig can_approve_jobs=data.get('can_approve_jobs', False) # Standard: kann nicht genehmigen ) # Administratoren bekommen automatisch Genehmigungsrechte if new_user.role == 'admin': permissions.can_approve_jobs = True permissions.can_start_jobs = True permissions.needs_approval = False db_session.add(permissions) db_session.commit() admin_logger.info(f"Neuer Benutzer erstellt: {new_user.username} von Admin {current_user.username}") return jsonify({ "success": True, "message": "Benutzer erfolgreich erstellt", "user_id": new_user.id }) except Exception as e: admin_logger.error(f"Fehler beim Erstellen des Benutzers: {str(e)}") return jsonify({"error": "Fehler beim Erstellen des Benutzers"}), 500 @admin_api_blueprint.route("/users/", methods=["GET"]) @admin_required def get_user_api(user_id): """API-Endpunkt zum Abrufen von Benutzerdaten""" try: with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({"error": "Benutzer nicht gefunden"}), 404 user_data = { "id": user.id, "username": user.username, "email": user.email, "name": user.name, "role": user.role, "active": user.active, "created_at": user.created_at.isoformat() if user.created_at else None, "last_login": user.last_login.isoformat() if user.last_login else None, "department": user.department, "position": user.position, "phone": user.phone, "bio": user.bio } return jsonify(user_data) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Benutzerdaten: {str(e)}") return jsonify({"error": "Fehler beim Abrufen der Benutzerdaten"}), 500 @admin_api_blueprint.route("/users/", methods=["PUT"]) @admin_required def update_user_api(user_id): """API-Endpunkt zum Aktualisieren von Benutzerdaten""" try: data = request.get_json() with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({"error": "Benutzer nicht gefunden"}), 404 # Aktualisierbare Felder updatable_fields = ['username', 'email', 'name', 'role', 'active', 'department', 'position', 'phone', 'bio'] 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']) user.updated_at = datetime.now() db_session.commit() admin_logger.info(f"Benutzer {user.username} aktualisiert von Admin {current_user.username}") return jsonify({ "success": True, "message": "Benutzer erfolgreich aktualisiert" }) except Exception as e: admin_logger.error(f"Fehler beim Aktualisieren des Benutzers: {str(e)}") return jsonify({"error": "Fehler beim Aktualisieren des Benutzers"}), 500 @admin_api_blueprint.route("/users/", methods=["DELETE"]) @admin_required def delete_user_api(user_id): """Löscht einen Benutzer über die API""" try: with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({"error": "Benutzer nicht gefunden"}), 404 # Prüfen ob der Benutzer der einzige Admin ist if user.is_admin: admin_count = db_session.query(User).filter(User.is_admin == True).count() if admin_count <= 1: return jsonify({"error": "Der letzte Administrator kann nicht gelöscht werden"}), 400 username = user.username db_session.delete(user) db_session.commit() admin_logger.info(f"Benutzer {username} gelöscht von Admin {current_user.username}") return jsonify({ "success": True, "message": "Benutzer erfolgreich gelöscht" }) except Exception as e: admin_logger.error(f"Fehler beim Löschen des Benutzers: {str(e)}") return jsonify({"error": "Fehler beim Löschen des Benutzers"}), 500 # ===== DRUCKER-API-ROUTEN ===== @admin_api_blueprint.route("/printers", methods=["GET"]) @admin_required def get_printers_api(): """Holt alle Drucker mit Status und Hardware-Integration""" try: from models import get_db_session, Printer from utils.hardware_integration import get_drucker_steuerung with get_db_session() as db_session: printers = db_session.query(Printer).all() # Hardware-Steuerung für Echtzeit-Status drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() printers_data = [] for printer in printers: printer_dict = printer.to_dict() # Echtzeit-Status aus Hardware-Integration hinzufügen if printer.id in status_data.get('drucker_status', {}): hw_status = status_data['drucker_status'][printer.id] printer_dict.update({ 'plug_online': hw_status.get('plug_online', False), 'plug_power_state': hw_status.get('plug_state', 'unknown'), 'energy_usage': hw_status.get('energy_usage', {}) }) printers_data.append(printer_dict) admin_logger.debug(f"Drucker-Daten für API abgerufen: {len(printers_data)} Drucker") return jsonify({ "success": True, "printers": printers_data, "total_count": len(printers_data) }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Drucker: {str(e)}") return jsonify({"error": "Fehler beim Abrufen der Drucker"}), 500 @admin_api_blueprint.route("/printers", methods=["POST"]) @admin_required def create_printer_api(): """Erstellt einen neuen Drucker""" try: from models import get_db_session, Printer data = request.get_json() # Validierung der erforderlichen Felder required_fields = ['name', 'model', 'location'] for field in required_fields: if not data.get(field): return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400 with get_db_session() as db_session: # Prüfen ob Name bereits existiert existing_printer = db_session.query(Printer).filter( Printer.name == data['name'] ).first() if existing_printer: return jsonify({"error": "Ein Drucker mit diesem Namen existiert bereits"}), 400 # Neuen Drucker erstellen printer = Printer( name=data['name'], model=data['model'], location=data['location'], ip_address=data.get('ip_address', ''), mac_address=data.get('mac_address', ''), plug_ip=data.get('plug_ip', ''), active=data.get('active', True), status='offline' # Standard-Status ) db_session.add(printer) db_session.commit() admin_logger.info(f"Neuer Drucker '{printer.name}' erstellt von Admin {current_user.username}") return jsonify({ "success": True, "message": "Drucker erfolgreich erstellt", "printer": printer.to_dict() }) except Exception as e: admin_logger.error(f"Fehler beim Erstellen des Druckers: {str(e)}") return jsonify({"error": "Fehler beim Erstellen des Druckers"}), 500 @admin_api_blueprint.route("/printers/", methods=["PUT"]) @admin_required def update_printer_api(printer_id): """Aktualisiert einen bestehenden Drucker""" try: from models import get_db_session, Printer, invalidate_model_cache data = request.get_json() with get_db_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 # Aktualisierbare Felder updateable_fields = [ 'name', 'model', 'location', 'ip_address', 'mac_address', 'plug_ip', 'active' ] updated_fields = [] for field in updateable_fields: if field in data: old_value = getattr(printer, field) new_value = data[field] if old_value != new_value: setattr(printer, field, new_value) updated_fields.append(f"{field}: '{old_value}' → '{new_value}'") if updated_fields: printer.updated_at = datetime.now() db_session.commit() # Cache invalidieren invalidate_model_cache("Printer", printer_id) admin_logger.info(f"Drucker '{printer.name}' aktualisiert von Admin {current_user.username}: {', '.join(updated_fields)}") return jsonify({ "success": True, "message": "Drucker erfolgreich aktualisiert", "printer": printer.to_dict(), "updated_fields": updated_fields }) else: return jsonify({ "success": True, "message": "Keine Änderungen vorgenommen" }) except Exception as e: admin_logger.error(f"Fehler beim Aktualisieren des Druckers {printer_id}: {str(e)}") return jsonify({"error": "Fehler beim Aktualisieren des Druckers"}), 500 @admin_api_blueprint.route("/printers//power", methods=["POST"]) @admin_required def toggle_printer_power_api(printer_id): """Schaltet Smart-Plug des Druckers ein/aus""" try: from models import get_db_session, Printer from utils.hardware_integration import get_drucker_steuerung data = request.get_json() action = data.get('action') # 'on' oder 'off' if action not in ['on', 'off']: return jsonify({"error": "Aktion muss 'on' oder 'off' sein"}), 400 with get_db_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 if not printer.plug_ip: return jsonify({"error": "Kein Smart-Plug für diesen Drucker konfiguriert"}), 400 # Hardware-Steuerung verwenden drucker_steuerung = get_drucker_steuerung() grund = f"Admin-Aktion durch {current_user.username}" if action == 'on': result = drucker_steuerung.drucker_einschalten(printer_id, grund) else: result = drucker_steuerung.drucker_ausschalten(printer_id, grund) if result['success']: admin_logger.info(f"Drucker '{printer.name}' {action.upper()} geschaltet von Admin {current_user.username}") return jsonify({ "success": True, "message": f"Drucker erfolgreich {'eingeschaltet' if action == 'on' else 'ausgeschaltet'}", "new_state": action, "details": result }) else: return jsonify({ "error": f"Fehler beim {'Ein' if action == 'on' else 'Aus'}schalten: {result.get('error', 'Unbekannter Fehler')}" }), 500 except Exception as e: admin_logger.error(f"Fehler beim Schalten des Druckers {printer_id}: {str(e)}") return jsonify({"error": "Fehler beim Schalten des Druckers"}), 500 @admin_api_blueprint.route("/printers//status", methods=["GET"]) @admin_required def get_printer_status_api(printer_id): """Holt detaillierten Status eines Druckers""" try: from models import get_db_session, Printer, Job from utils.hardware_integration import get_drucker_steuerung with get_db_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 # Hardware-Status abrufen drucker_steuerung = get_drucker_steuerung() status_data = drucker_steuerung.template_daten_sammeln() # Aktuelle Jobs für diesen Drucker active_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(['pending', 'printing', 'scheduled']) ).all() # Drucker-Daten mit erweiterten Informationen printer_data = printer.to_dict() # Hardware-Status hinzufügen if printer_id in status_data.get('drucker_status', {}): hw_status = status_data['drucker_status'][printer_id] printer_data.update({ 'plug_online': hw_status.get('plug_online', False), 'plug_power_state': hw_status.get('plug_state', 'unknown'), 'energy_usage': hw_status.get('energy_usage', {}), 'plug_last_seen': hw_status.get('last_seen') }) # Job-Informationen hinzufügen printer_data['active_jobs'] = [job.to_dict() for job in active_jobs] printer_data['active_jobs_count'] = len(active_jobs) admin_logger.debug(f"Detaillierter Status für Drucker {printer_id} abgerufen") return jsonify({ "success": True, "printer": printer_data }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen des Drucker-Status {printer_id}: {str(e)}") return jsonify({"error": "Fehler beim Abrufen des Drucker-Status"}), 500 @admin_api_blueprint.route("/printers/", methods=["DELETE"]) @admin_required def delete_printer_api(printer_id): """Löscht einen Drucker über die API mit allen Abhängigkeiten""" try: from models import get_db_session, Printer, Job, GuestRequest, JobOrder, PlugStatusLog with get_db_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 printer_name = printer.name printer_location = printer.location deleted_items = [] # 1. Nullable ForeignKeys auf NULL setzen (GuestRequest) guest_requests_printer = db_session.query(GuestRequest).filter(GuestRequest.printer_id == printer_id).count() if guest_requests_printer > 0: db_session.query(GuestRequest).filter(GuestRequest.printer_id == printer_id).update({GuestRequest.printer_id: None}) deleted_items.append(f"{guest_requests_printer} Gastanfragen aktualisiert") guest_requests_assigned = db_session.query(GuestRequest).filter(GuestRequest.assigned_printer_id == printer_id).count() if guest_requests_assigned > 0: db_session.query(GuestRequest).filter(GuestRequest.assigned_printer_id == printer_id).update({GuestRequest.assigned_printer_id: None}) deleted_items.append(f"{guest_requests_assigned} zugewiesene Gastanfragen aktualisiert") # 2. Non-nullable ForeignKeys löschen job_orders_count = db_session.query(JobOrder).filter(JobOrder.printer_id == printer_id).count() if job_orders_count > 0: db_session.query(JobOrder).filter(JobOrder.printer_id == printer_id).delete() deleted_items.append(f"{job_orders_count} Auftragsbestellungen gelöscht") plug_logs_count = db_session.query(PlugStatusLog).filter(PlugStatusLog.printer_id == printer_id).count() if plug_logs_count > 0: db_session.query(PlugStatusLog).filter(PlugStatusLog.printer_id == printer_id).delete() deleted_items.append(f"{plug_logs_count} Plug-Status-Logs gelöscht") # 3. Jobs explizit löschen (auch wenn CASCADE vorhanden ist) jobs_count = db_session.query(Job).filter(Job.printer_id == printer_id).count() if jobs_count > 0: db_session.query(Job).filter(Job.printer_id == printer_id).delete() deleted_items.append(f"{jobs_count} Jobs gelöscht") # 4. Drucker aus der Datenbank entfernen db_session.delete(printer) db_session.commit() # Cache invalidieren from models import invalidate_model_cache invalidate_model_cache("Printer", printer_id) admin_logger.info(f"Drucker '{printer_name}' (ID: {printer_id}, Standort: {printer_location}) und alle Abhängigkeiten gelöscht von Admin {current_user.username}") if deleted_items: admin_logger.info(f"Gelöschte Abhängigkeiten: {', '.join(deleted_items)}") success_message = f"Drucker '{printer_name}' erfolgreich gelöscht" if deleted_items: success_message += f" (einschließlich: {', '.join(deleted_items)})" return jsonify({ "success": True, "message": success_message }) except Exception as e: admin_logger.error(f"Fehler beim Löschen des Druckers {printer_id}: {str(e)}") return jsonify({"error": "Fehler beim Löschen des Druckers"}), 500 # ===== ERWEITERTE SYSTEM-API (ursprünglich admin_api.py) ===== @admin_api_blueprint.route('/backup/create', methods=['POST']) @admin_required def create_backup(): """ Erstellt ein manuelles System-Backup. Erstellt eine Sicherung aller wichtigen Systemdaten einschließlich Datenbank, Konfigurationsdateien und Benutzer-Uploads. Returns: JSON: Erfolgs-Status und Backup-Informationen """ try: admin_api_logger.info(f"Backup-Erstellung angefordert von Admin {current_user.username}") # Backup-Verzeichnis sicherstellen backup_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'backups') os.makedirs(backup_dir, exist_ok=True) # Eindeutigen Backup-Namen erstellen timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_name = f"system_backup_{timestamp}.zip" backup_path = os.path.join(backup_dir, backup_name) created_files = [] backup_size = 0 with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: # 1. Datenbank-Datei hinzufügen try: from utils.utilities_collection import DATABASE_PATH if os.path.exists(DATABASE_PATH): zipf.write(DATABASE_PATH, 'database/main.db') created_files.append('database/main.db') admin_api_logger.debug("✅ Hauptdatenbank zur Sicherung hinzugefügt") # WAL- und SHM-Dateien falls vorhanden wal_path = DATABASE_PATH + '-wal' shm_path = DATABASE_PATH + '-shm' if os.path.exists(wal_path): zipf.write(wal_path, 'database/main.db-wal') created_files.append('database/main.db-wal') if os.path.exists(shm_path): zipf.write(shm_path, 'database/main.db-shm') created_files.append('database/main.db-shm') except Exception as db_error: admin_api_logger.warning(f"Fehler beim Hinzufügen der Datenbank: {str(db_error)}") # 2. Konfigurationsdateien try: config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config') if os.path.exists(config_dir): for root, dirs, files in os.walk(config_dir): for file in files: if file.endswith(('.py', '.json', '.yaml', '.yml', '.toml')): file_path = os.path.join(root, file) arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__))) zipf.write(file_path, arc_path) created_files.append(arc_path) admin_api_logger.debug("✅ Konfigurationsdateien zur Sicherung hinzugefügt") except Exception as config_error: admin_api_logger.warning(f"Fehler beim Hinzufügen der Konfiguration: {str(config_error)}") # 3. Wichtige User-Uploads (limitiert auf die letzten 1000 Dateien) try: uploads_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'uploads') if os.path.exists(uploads_dir): file_count = 0 max_files = 1000 # Limit für Performance for root, dirs, files in os.walk(uploads_dir): for file in files[:max_files - file_count]: if file_count >= max_files: break file_path = os.path.join(root, file) file_size = os.path.getsize(file_path) # Nur Dateien unter 50MB hinzufügen if file_size < 50 * 1024 * 1024: arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__))) zipf.write(file_path, arc_path) created_files.append(arc_path) file_count += 1 if file_count >= max_files: break admin_api_logger.debug(f"✅ {file_count} Upload-Dateien zur Sicherung hinzugefügt") except Exception as uploads_error: admin_api_logger.warning(f"Fehler beim Hinzufügen der Uploads: {str(uploads_error)}") # 4. System-Logs (nur die letzten 100 Log-Dateien) try: logs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs') if os.path.exists(logs_dir): log_files = [] for root, dirs, files in os.walk(logs_dir): for file in files: if file.endswith(('.log', '.txt')): file_path = os.path.join(root, file) log_files.append((file_path, os.path.getmtime(file_path))) # Sortiere nach Datum (neueste zuerst) und nimm nur die letzten 100 log_files.sort(key=lambda x: x[1], reverse=True) for file_path, _ in log_files[:100]: arc_path = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__))) zipf.write(file_path, arc_path) created_files.append(arc_path) admin_api_logger.debug(f"✅ {len(log_files[:100])} Log-Dateien zur Sicherung hinzugefügt") except Exception as logs_error: admin_api_logger.warning(f"Fehler beim Hinzufügen der Logs: {str(logs_error)}") # Backup-Größe bestimmen if os.path.exists(backup_path): backup_size = os.path.getsize(backup_path) admin_api_logger.info(f"✅ System-Backup erfolgreich erstellt: {backup_name} ({backup_size / 1024 / 1024:.2f} MB)") return jsonify({ 'success': True, 'message': f'Backup erfolgreich erstellt: {backup_name}', 'backup_info': { 'filename': backup_name, 'size_bytes': backup_size, 'size_mb': round(backup_size / 1024 / 1024, 2), 'files_count': len(created_files), 'created_at': datetime.now().isoformat(), 'path': backup_path } }) except Exception as e: admin_api_logger.error(f"❌ Fehler beim Erstellen des Backups: {str(e)}") return jsonify({ 'success': False, 'message': f'Fehler beim Erstellen des Backups: {str(e)}' }), 500 @admin_api_blueprint.route('/printers//toggle', methods=['POST']) @admin_required def toggle_printer_power(printer_id): """ Schaltet die Smart-Plug-Steckdose eines Druckers ein/aus (Toggle-Funktion). Args: printer_id: ID des zu steuernden Druckers JSON-Parameter: - reason: Grund für die Schaltung (optional) Returns: JSON mit Ergebnis der Toggle-Aktion """ admin_api_logger.info(f"🔌 Smart-Plug Toggle für Drucker {printer_id} von Admin {current_user.name}") try: # Parameter auslesen data = request.get_json() or {} reason = data.get("reason", "Admin-Panel Toggle") # Drucker aus Datenbank holen db_session = get_cached_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 # Prüfen, ob Drucker eine Steckdose konfiguriert hat if not printer.plug_ip or not printer.plug_username or not printer.plug_password: return jsonify({ "success": False, "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert" }), 400 # Aktuellen Status der Steckdose ermitteln try: from PyP100 import PyP110 p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() p110.login() # Aktuellen Status abrufen device_info = p110.getDeviceInfo() current_status = device_info["result"]["device_on"] # Toggle-Aktion durchführen if current_status: # Ausschalten p110.turnOff() new_status = "off" action = "ausgeschaltet" printer.status = "offline" else: # Einschalten p110.turnOn() new_status = "on" action = "eingeschaltet" printer.status = "starting" # Drucker-Status in DB aktualisieren printer.last_checked = datetime.now() db_session.commit() admin_api_logger.info(f"✅ Drucker {printer.name} erfolgreich {action} | Grund: {reason}") return jsonify({ "success": True, "message": f"Drucker {printer.name} erfolgreich {action}", "printer": { "id": printer_id, "name": printer.name, "model": printer.model, "location": printer.location }, "toggle_result": { "previous_status": "on" if current_status else "off", "new_status": new_status, "action": action, "reason": reason }, "performed_by": { "id": current_user.id, "name": current_user.name }, "timestamp": datetime.now().isoformat() }) except Exception as tapo_error: admin_api_logger.error(f"❌ Tapo-Fehler für Drucker {printer.name}: {str(tapo_error)}") return jsonify({ "success": False, "error": f"Fehler bei Steckdosensteuerung: {str(tapo_error)}" }), 500 except Exception as e: admin_api_logger.error(f"❌ Allgemeiner Fehler bei Toggle-Aktion: {str(e)}") return jsonify({ "success": False, "error": f"Systemfehler: {str(e)}" }), 500 @admin_api_blueprint.route('/database/optimize', methods=['POST']) @admin_required def optimize_database(): """ Führt Datenbank-Optimierung durch. Optimiert die SQLite-Datenbank durch VACUUM, ANALYZE und weitere Wartungsoperationen für bessere Performance. Returns: JSON: Erfolgs-Status und Optimierungs-Statistiken """ try: admin_api_logger.info(f"Datenbank-Optimierung angefordert von Admin {current_user.username}") from utils.utilities_collection import DATABASE_PATH optimization_results = { 'vacuum_completed': False, 'analyze_completed': False, 'integrity_check': False, 'wal_checkpoint': False, 'size_before': 0, 'size_after': 0, 'space_saved': 0 } # Datenbankgröße vor Optimierung if os.path.exists(DATABASE_PATH): optimization_results['size_before'] = os.path.getsize(DATABASE_PATH) # Verbindung zur Datenbank herstellen conn = sqlite3.connect(DATABASE_PATH, timeout=30.0) cursor = conn.cursor() try: # 1. Integritätsprüfung admin_api_logger.debug("🔍 Führe Integritätsprüfung durch...") cursor.execute("PRAGMA integrity_check") integrity_result = cursor.fetchone() optimization_results['integrity_check'] = integrity_result[0] == 'ok' if not optimization_results['integrity_check']: admin_api_logger.warning(f"⚠️ Integritätsprüfung ergab: {integrity_result[0]}") else: admin_api_logger.debug("✅ Integritätsprüfung erfolgreich") # 2. WAL-Checkpoint (falls WAL-Modus aktiv) try: admin_api_logger.debug("🔄 Führe WAL-Checkpoint durch...") cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)") optimization_results['wal_checkpoint'] = True admin_api_logger.debug("✅ WAL-Checkpoint erfolgreich") except Exception as wal_error: admin_api_logger.debug(f"ℹ️ WAL-Checkpoint nicht möglich: {str(wal_error)}") # 3. ANALYZE - Statistiken aktualisieren admin_api_logger.debug("📊 Aktualisiere Datenbank-Statistiken...") cursor.execute("ANALYZE") optimization_results['analyze_completed'] = True admin_api_logger.debug("✅ ANALYZE erfolgreich") # 4. VACUUM - Datenbank komprimieren und reorganisieren admin_api_logger.debug("🗜️ Komprimiere und reorganisiere Datenbank...") cursor.execute("VACUUM") optimization_results['vacuum_completed'] = True admin_api_logger.debug("✅ VACUUM erfolgreich") # 5. Performance-Optimierungen try: # Cache-Größe optimieren cursor.execute("PRAGMA cache_size = 10000") # 10MB Cache # Journal-Modus auf WAL setzen für bessere Concurrent-Performance cursor.execute("PRAGMA journal_mode = WAL") # Synchronous auf NORMAL für Balance zwischen Performance und Sicherheit cursor.execute("PRAGMA synchronous = NORMAL") # Page-Größe optimieren (falls noch nicht gesetzt) cursor.execute("PRAGMA page_size = 4096") admin_api_logger.debug("✅ Performance-Optimierungen angewendet") except Exception as perf_error: admin_api_logger.warning(f"⚠️ Performance-Optimierungen teilweise fehlgeschlagen: {str(perf_error)}") finally: cursor.close() conn.close() # Datenbankgröße nach Optimierung if os.path.exists(DATABASE_PATH): optimization_results['size_after'] = os.path.getsize(DATABASE_PATH) optimization_results['space_saved'] = optimization_results['size_before'] - optimization_results['size_after'] # Ergebnisse loggen space_saved_mb = optimization_results['space_saved'] / 1024 / 1024 admin_api_logger.info(f"✅ Datenbank-Optimierung abgeschlossen - {space_saved_mb:.2f} MB Speicher gespart") return jsonify({ 'success': True, 'message': 'Datenbank erfolgreich optimiert', 'results': { 'vacuum_completed': optimization_results['vacuum_completed'], 'analyze_completed': optimization_results['analyze_completed'], 'integrity_check_passed': optimization_results['integrity_check'], 'wal_checkpoint_completed': optimization_results['wal_checkpoint'], 'size_before_mb': round(optimization_results['size_before'] / 1024 / 1024, 2), 'size_after_mb': round(optimization_results['size_after'] / 1024 / 1024, 2), 'space_saved_mb': round(space_saved_mb, 2), 'optimization_timestamp': datetime.now().isoformat() } }) except Exception as e: admin_api_logger.error(f"❌ Fehler bei Datenbank-Optimierung: {str(e)}") return jsonify({ 'success': False, 'message': f'Fehler bei Datenbank-Optimierung: {str(e)}' }), 500 @admin_api_blueprint.route('/cache/clear', methods=['POST']) @admin_required def clear_cache(): """ Leert den System-Cache. Entfernt alle temporären Dateien, Cache-Verzeichnisse und Python-Bytecode um Speicher freizugeben und Performance zu verbessern. Returns: JSON: Erfolgs-Status und Lösch-Statistiken """ try: admin_api_logger.info(f"Cache-Leerung angefordert von Admin {current_user.username}") cleared_stats = { 'files_deleted': 0, 'dirs_deleted': 0, 'space_freed': 0, 'categories': {} } app_root = os.path.dirname(os.path.dirname(__file__)) # 1. Python-Bytecode-Cache leeren (__pycache__) try: pycache_count = 0 pycache_size = 0 for root, dirs, files in os.walk(app_root): if '__pycache__' in root: for file in files: file_path = os.path.join(root, file) try: pycache_size += os.path.getsize(file_path) os.remove(file_path) pycache_count += 1 except Exception: pass # Versuche das __pycache__-Verzeichnis zu löschen try: os.rmdir(root) cleared_stats['dirs_deleted'] += 1 except Exception: pass cleared_stats['categories']['python_bytecode'] = { 'files': pycache_count, 'size_mb': round(pycache_size / 1024 / 1024, 2) } cleared_stats['files_deleted'] += pycache_count cleared_stats['space_freed'] += pycache_size admin_api_logger.debug(f"✅ Python-Bytecode-Cache: {pycache_count} Dateien, {pycache_size / 1024 / 1024:.2f} MB") except Exception as pycache_error: admin_api_logger.warning(f"⚠️ Fehler beim Leeren des Python-Cache: {str(pycache_error)}") # 2. Temporäre Dateien im uploads/temp Verzeichnis try: temp_count = 0 temp_size = 0 temp_dir = os.path.join(app_root, 'uploads', 'temp') if os.path.exists(temp_dir): for root, dirs, files in os.walk(temp_dir): for file in files: file_path = os.path.join(root, file) try: temp_size += os.path.getsize(file_path) os.remove(file_path) temp_count += 1 except Exception: pass cleared_stats['categories']['temp_uploads'] = { 'files': temp_count, 'size_mb': round(temp_size / 1024 / 1024, 2) } cleared_stats['files_deleted'] += temp_count cleared_stats['space_freed'] += temp_size admin_api_logger.debug(f"✅ Temporäre Upload-Dateien: {temp_count} Dateien, {temp_size / 1024 / 1024:.2f} MB") except Exception as temp_error: admin_api_logger.warning(f"⚠️ Fehler beim Leeren des Temp-Verzeichnisses: {str(temp_error)}") # 3. System-Cache-Verzeichnisse (falls vorhanden) try: cache_count = 0 cache_size = 0 cache_dirs = [ os.path.join(app_root, 'static', 'cache'), os.path.join(app_root, 'cache'), os.path.join(app_root, '.cache') ] for cache_dir in cache_dirs: if os.path.exists(cache_dir): for root, dirs, files in os.walk(cache_dir): for file in files: file_path = os.path.join(root, file) try: cache_size += os.path.getsize(file_path) os.remove(file_path) cache_count += 1 except Exception: pass cleared_stats['categories']['system_cache'] = { 'files': cache_count, 'size_mb': round(cache_size / 1024 / 1024, 2) } cleared_stats['files_deleted'] += cache_count cleared_stats['space_freed'] += cache_size admin_api_logger.debug(f"✅ System-Cache: {cache_count} Dateien, {cache_size / 1024 / 1024:.2f} MB") except Exception as cache_error: admin_api_logger.warning(f"⚠️ Fehler beim Leeren des System-Cache: {str(cache_error)}") # 4. Alte Log-Dateien (älter als 30 Tage) try: logs_count = 0 logs_size = 0 logs_dir = os.path.join(app_root, 'logs') cutoff_date = datetime.now().timestamp() - (30 * 24 * 60 * 60) # 30 Tage if os.path.exists(logs_dir): for root, dirs, files in os.walk(logs_dir): for file in files: if file.endswith(('.log', '.log.1', '.log.2', '.log.3')): file_path = os.path.join(root, file) try: if os.path.getmtime(file_path) < cutoff_date: logs_size += os.path.getsize(file_path) os.remove(file_path) logs_count += 1 except Exception: pass cleared_stats['categories']['old_logs'] = { 'files': logs_count, 'size_mb': round(logs_size / 1024 / 1024, 2) } cleared_stats['files_deleted'] += logs_count cleared_stats['space_freed'] += logs_size admin_api_logger.debug(f"✅ Alte Log-Dateien: {logs_count} Dateien, {logs_size / 1024 / 1024:.2f} MB") except Exception as logs_error: admin_api_logger.warning(f"⚠️ Fehler beim Leeren alter Log-Dateien: {str(logs_error)}") # 5. Application-Level Cache leeren (falls Models-Cache existiert) try: from models import clear_model_cache clear_model_cache() admin_api_logger.debug("✅ Application-Level Cache geleert") except (ImportError, AttributeError): admin_api_logger.debug("ℹ️ Kein Application-Level Cache verfügbar") # Ergebnisse zusammenfassen total_space_mb = cleared_stats['space_freed'] / 1024 / 1024 admin_api_logger.info(f"✅ Cache-Leerung abgeschlossen: {cleared_stats['files_deleted']} Dateien, {total_space_mb:.2f} MB freigegeben") return jsonify({ 'success': True, 'message': f'Cache erfolgreich geleert - {total_space_mb:.2f} MB freigegeben', 'statistics': { 'total_files_deleted': cleared_stats['files_deleted'], 'total_dirs_deleted': cleared_stats['dirs_deleted'], 'total_space_freed_mb': round(total_space_mb, 2), 'categories': cleared_stats['categories'], 'cleanup_timestamp': datetime.now().isoformat() } }) except Exception as e: admin_api_logger.error(f"❌ Fehler beim Leeren des Cache: {str(e)}") return jsonify({ 'success': False, 'message': f'Fehler beim Leeren des Cache: {str(e)}' }), 500 # ===== API-ENDPUNKTE FÜR LOGS ===== @admin_api_blueprint.route("/logs", methods=["GET"]) @admin_required def get_logs_api(): """API-Endpunkt zum Abrufen von System-Logs""" try: level = request.args.get('level', 'all') limit = min(int(request.args.get('limit', 100)), 1000) # Max 1000 Logs with get_cached_session() as db_session: query = db_session.query(SystemLog) # Filter nach Log-Level falls spezifiziert if level != 'all': query = query.filter(SystemLog.level == level.upper()) # Logs laden logs = query.order_by(SystemLog.timestamp.desc()).limit(limit).all() # In Dictionary konvertieren logs_data = [] for log in logs: logs_data.append({ 'id': log.id, 'level': log.level, 'message': log.message, 'timestamp': log.timestamp.isoformat() if log.timestamp else None, 'module': getattr(log, 'module', ''), 'user_id': getattr(log, 'user_id', None), 'ip_address': getattr(log, 'ip_address', '') }) admin_logger.info(f"Logs abgerufen: {len(logs_data)} Einträge, Level: {level}") return jsonify({ "success": True, "logs": logs_data, "count": len(logs_data), "level": level }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Logs: {str(e)}") return jsonify({"error": "Fehler beim Laden der Logs"}), 500 @admin_api_blueprint.route("/logs/export", methods=["POST"]) @admin_required def export_logs_api(): """API-Endpunkt zum Exportieren von System-Logs""" try: data = request.get_json() or {} level = data.get('level', 'all') format_type = data.get('format', 'json') # json, csv, txt with get_cached_session() as db_session: query = db_session.query(SystemLog) # Filter nach Log-Level falls spezifiziert if level != 'all': query = query.filter(SystemLog.level == level.upper()) # Alle Logs für Export laden logs = query.order_by(SystemLog.timestamp.desc()).all() # Export-Format bestimmen if format_type == 'csv': import csv import io output = io.StringIO() writer = csv.writer(output) # Header schreiben writer.writerow(['Timestamp', 'Level', 'Module', 'Message', 'User ID', 'IP Address']) # Daten schreiben for log in logs: writer.writerow([ log.timestamp.isoformat() if log.timestamp else '', log.level, getattr(log, 'module', ''), log.message, getattr(log, 'user_id', ''), getattr(log, 'ip_address', '') ]) content = output.getvalue() output.close() return jsonify({ "success": True, "content": content, "filename": f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", "content_type": "text/csv" }) elif format_type == 'txt': lines = [] for log in logs: timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M:%S') if log.timestamp else 'Unknown' lines.append(f"[{timestamp}] {log.level}: {log.message}") content = '\n'.join(lines) return jsonify({ "success": True, "content": content, "filename": f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", "content_type": "text/plain" }) else: # JSON format logs_data = [] for log in logs: logs_data.append({ 'id': log.id, 'level': log.level, 'message': log.message, 'timestamp': log.timestamp.isoformat() if log.timestamp else None, 'module': getattr(log, 'module', ''), 'user_id': getattr(log, 'user_id', None), 'ip_address': getattr(log, 'ip_address', '') }) import json content = json.dumps(logs_data, indent=2, ensure_ascii=False) return jsonify({ "success": True, "content": content, "filename": f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "content_type": "application/json" }) except Exception as e: admin_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}") return jsonify({"error": "Fehler beim Exportieren der Logs"}), 500 # ===== GAST-OTP-MANAGEMENT FÜR OFFLINE-BETRIEB ===== @admin_api_blueprint.route("/guest-requests", methods=["GET"]) @admin_required def get_guest_requests_api(): """API-Endpunkt zum Abrufen aller Gastanfragen mit OTP-Codes für Admins""" try: with get_cached_session() as db_session: # Alle Gastanfragen laden guest_requests = db_session.query(GuestRequest).order_by( GuestRequest.created_at.desc() ).all() # In Dictionary konvertieren mit OTP-Codes für Admins requests_data = [] for req in guest_requests: request_data = { 'id': req.id, 'name': req.name, 'email': req.email, 'reason': req.reason, 'status': req.status, 'duration_min': req.duration_min, 'created_at': req.created_at.isoformat() if req.created_at else None, 'processed_at': req.processed_at.isoformat() if req.processed_at else None, 'processed_by': req.processed_by, 'approval_notes': req.approval_notes, 'rejection_reason': req.rejection_reason, 'author_ip': req.author_ip } # OTP-Code für Admins sichtbar machen (nur wenn aktiv) if req.status == 'approved' and req.otp_code and req.otp_expires_at: if req.otp_expires_at > datetime.now() and not req.otp_used_at: request_data['otp_code'] = req.otp_code_plain # Klartext für Admin request_data['otp_expires_at'] = req.otp_expires_at.isoformat() request_data['otp_status'] = 'active' elif req.otp_used_at: request_data['otp_status'] = 'used' request_data['otp_used_at'] = req.otp_used_at.isoformat() else: request_data['otp_status'] = 'expired' else: request_data['otp_status'] = 'not_generated' requests_data.append(request_data) admin_logger.info(f"Gastanfragen abgerufen: {len(requests_data)} Einträge für Admin {current_user.name}") return jsonify({ "success": True, "requests": requests_data, "count": len(requests_data) }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Gastanfragen: {str(e)}") return jsonify({"error": "Fehler beim Laden der Gastanfragen"}), 500 @admin_api_blueprint.route("/guest-requests//generate-otp", methods=["POST"]) @admin_required def generate_guest_otp_api(request_id): """Generiert einen neuen OTP-Code für eine genehmigte Gastanfrage""" try: with get_cached_session() as db_session: guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first() if not guest_request: return jsonify({"error": "Gastanfrage nicht gefunden"}), 404 if guest_request.status != 'approved': return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400 # Neuen OTP-Code generieren otp_code = guest_request.generate_otp() guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig guest_request.otp_used_at = None # Reset falls bereits verwendet db_session.commit() admin_logger.info(f"Neuer OTP-Code generiert für Gastanfrage {request_id} von Admin {current_user.name}") return jsonify({ "success": True, "message": "Neuer OTP-Code generiert", "otp_code": otp_code, "expires_at": guest_request.otp_expires_at.isoformat(), "guest_name": guest_request.name }) except Exception as e: admin_logger.error(f"Fehler beim Generieren des OTP-Codes: {str(e)}") return jsonify({"error": "Fehler beim Generieren des OTP-Codes"}), 500 @admin_api_blueprint.route("/guest-requests//approve", methods=["POST"]) @admin_required def approve_guest_request_api(request_id): """Genehmigt eine Gastanfrage""" try: data = request.get_json() approval_notes = data.get('approval_notes', '') if data else '' with get_cached_session() as db_session: guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first() if not guest_request: return jsonify({"error": "Gastanfrage nicht gefunden"}), 404 if guest_request.status != 'pending': return jsonify({"error": "Gastanfrage ist bereits bearbeitet"}), 400 # Genehmigung durchführen guest_request.status = 'approved' guest_request.processed_at = datetime.now() guest_request.processed_by = current_user.id # User-ID statt Username guest_request.approval_notes = approval_notes # OTP-Code automatisch generieren otp_code = guest_request.generate_otp() guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig db_session.commit() admin_logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.name}") return jsonify({ "success": True, "message": "Gastanfrage erfolgreich genehmigt", "otp_code": otp_code, "expires_at": guest_request.otp_expires_at.isoformat(), "guest_name": guest_request.name }) except Exception as e: admin_logger.error(f"Fehler beim Genehmigen der Gastanfrage {request_id}: {str(e)}") return jsonify({"error": "Fehler beim Genehmigen der Gastanfrage"}), 500 @admin_api_blueprint.route("/guest-requests//reject", methods=["POST"]) @admin_required def reject_guest_request_api(request_id): """Lehnt eine Gastanfrage ab""" try: data = request.get_json() rejection_reason = data.get('rejection_reason', 'Kein Grund angegeben') if data else 'Kein Grund angegeben' with get_cached_session() as db_session: guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first() if not guest_request: return jsonify({"error": "Gastanfrage nicht gefunden"}), 404 if guest_request.status != 'pending': return jsonify({"error": "Gastanfrage ist bereits bearbeitet"}), 400 # Ablehnung durchführen guest_request.status = 'rejected' guest_request.processed_at = datetime.now() guest_request.processed_by = current_user.id # User-ID statt Username guest_request.rejection_reason = rejection_reason db_session.commit() admin_logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.name}") return jsonify({ "success": True, "message": "Gastanfrage erfolgreich abgelehnt", "guest_name": guest_request.name }) except Exception as e: admin_logger.error(f"Fehler beim Ablehnen der Gastanfrage {request_id}: {str(e)}") return jsonify({"error": "Fehler beim Ablehnen der Gastanfrage"}), 500 @admin_api_blueprint.route("/guest-requests//print-credentials", methods=["POST"]) @admin_required def print_guest_credentials_api(request_id): """Erstellt Ausdruck-Template für Gast-Zugangsdaten""" try: with get_cached_session() as db_session: guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first() if not guest_request: return jsonify({"error": "Gastanfrage nicht gefunden"}), 404 if guest_request.status != 'approved': return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400 if not guest_request.otp_code or not guest_request.otp_expires_at: return jsonify({"error": "Kein OTP-Code verfügbar"}), 400 # Ausdruck-Template erstellen print_template = { "type": "guest_credentials", "title": "MYP GASTZUGANG GENEHMIGT", "subtitle": "TBA Marienfelde - Offline System", "guest_info": { "name": guest_request.name, "request_id": f"GAS-{guest_request.id:06d}", "email": guest_request.email, "approved_at": guest_request.processed_at.strftime("%d.%m.%Y %H:%M") if guest_request.processed_at else None, "approved_by": guest_request.processed_by }, "access_data": { "otp_code": guest_request.otp_code_plain, # Klartext für Ausdruck "valid_until": guest_request.otp_expires_at.strftime("%d.%m.%Y %H:%M"), "login_url": "http://192.168.1.100:5000/auth/guest" }, "usage_rules": [ "Max. Druckzeit pro Job: 4 Stunden", "Dateiformate: STL, OBJ, 3MF, GCODE", "Materialien: PLA, PETG", "Jobs benötigen Admin-Freigabe" ], "pickup_info": { "location": "TBA Marienfelde, Raum B2.1", "hours": "Mo-Fr 8:00-16:00", "storage_days": "Max. 7 Tage" }, "qr_code_data": f"http://192.168.1.100:5000/auth/guest?name={guest_request.name}&id={guest_request.id}", "admin_note": "An Gast aushändigen", "timestamp": datetime.now().isoformat() } admin_logger.info(f"Ausdruck-Template erstellt für Gastanfrage {request_id} von Admin {current_user.name}") return jsonify({ "success": True, "print_template": print_template }) except Exception as e: admin_logger.error(f"Fehler beim Erstellen des Ausdruck-Templates: {str(e)}") return jsonify({"error": "Fehler beim Erstellen des Ausdruck-Templates"}), 500 @admin_api_blueprint.route("/guest-requests/pending-otps", methods=["GET"]) @admin_required def get_pending_guest_otps_api(): """Listet alle aktiven OTP-Codes für schnelle Admin-Übersicht""" try: with get_cached_session() as db_session: # Alle genehmigten Anfragen mit aktiven OTP-Codes active_requests = db_session.query(GuestRequest).filter( GuestRequest.status == 'approved', GuestRequest.otp_code.isnot(None), GuestRequest.otp_expires_at > datetime.now(), GuestRequest.otp_used_at.is_(None) ).order_by(GuestRequest.otp_expires_at.asc()).all() # Kompakte Liste für Admin-Dashboard otps_data = [] for req in active_requests: time_remaining = req.otp_expires_at - datetime.now() hours_remaining = int(time_remaining.total_seconds() // 3600) otps_data.append({ 'request_id': req.id, 'guest_name': req.name, 'otp_code': req.otp_code_plain, # Klartext für Admin 'expires_at': req.otp_expires_at.isoformat(), 'hours_remaining': hours_remaining, 'urgency': 'critical' if hours_remaining < 2 else 'warning' if hours_remaining < 24 else 'normal' }) admin_logger.info(f"Aktive OTP-Codes abgerufen: {len(otps_data)} Codes") return jsonify({ "success": True, "active_otps": otps_data, "count": len(otps_data) }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen aktiver OTP-Codes: {str(e)}") return jsonify({"error": "Fehler beim Laden der OTP-Codes"}), 500 # ===== ADMIN-UI ROUTES FÜR GAST-OTP-VERWALTUNG ===== @admin_blueprint.route("/guest-otps") @admin_required def guest_otps_management(): """Admin-UI für Gast-OTP-Verwaltung (Offline-System)""" admin_logger.info(f"Gast-OTP-Verwaltung aufgerufen von Admin {current_user.name}") return render_template('admin_guest_otps.html', page_title="Gast-OTP-Verwaltung", current_user=current_user) # ===== API-ENDPUNKTE FÜR SYSTEM-INFORMATIONEN ===== @admin_api_blueprint.route("/system/status", methods=["GET"]) @admin_required def get_system_status_api(): """API-Endpunkt für System-Status-Informationen""" try: import psutil import platform # System-Informationen sammeln cpu_usage = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() disk = psutil.disk_usage('/') # Netzwerk-Informationen network = psutil.net_io_counters() # Python und Flask Informationen python_version = platform.python_version() platform_info = platform.platform() # Datenbank-Statistiken with get_cached_session() as db_session: total_users = db_session.query(User).count() total_printers = db_session.query(Printer).count() total_jobs = db_session.query(Job).count() # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() system_status = { "cpu": { "usage_percent": cpu_usage, "core_count": psutil.cpu_count() }, "memory": { "total": memory.total, "available": memory.available, "used": memory.used, "usage_percent": memory.percent }, "disk": { "total": disk.total, "used": disk.used, "free": disk.free, "usage_percent": (disk.used / disk.total) * 100 }, "network": { "bytes_sent": network.bytes_sent, "bytes_received": network.bytes_recv, "packets_sent": network.packets_sent, "packets_received": network.packets_recv }, "system": { "python_version": python_version, "platform": platform_info, "uptime": datetime.now().isoformat() }, "database": { "total_users": total_users, "total_printers": total_printers, "total_jobs": total_jobs, "active_jobs": active_jobs } } admin_logger.info(f"System-Status abgerufen von {current_user.username}") return jsonify({ "success": True, "status": system_status, "timestamp": datetime.now().isoformat() }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen des System-Status: {str(e)}") return jsonify({"error": "Fehler beim Laden des System-Status"}), 500 # ===== TEST-ENDPUNKTE FÜR ENTWICKLUNG ===== @admin_api_blueprint.route("/test/create-sample-logs", methods=["POST"]) @admin_required def create_sample_logs_api(): """Test-Endpunkt zum Erstellen von Beispiel-Log-Einträgen""" try: with get_cached_session() as db_session: # Verschiedene Log-Level erstellen sample_logs = [ { 'level': 'INFO', 'message': 'System erfolgreich gestartet', 'module': 'admin', 'user_id': current_user.id, 'ip_address': request.remote_addr }, { 'level': 'WARNING', 'message': 'Drucker hat 5 Minuten nicht geantwortet', 'module': 'printer_monitor', 'user_id': None, 'ip_address': None }, { 'level': 'ERROR', 'message': 'Fehler beim Verbinden mit Drucker printer-001', 'module': 'printer', 'user_id': None, 'ip_address': None }, { 'level': 'DEBUG', 'message': 'API-Aufruf erfolgreich verarbeitet', 'module': 'api', 'user_id': current_user.id, 'ip_address': request.remote_addr }, { 'level': 'CRITICAL', 'message': 'Datenbank-Verbindung unterbrochen', 'module': 'database', 'user_id': None, 'ip_address': None } ] # Log-Einträge erstellen created_count = 0 for log_data in sample_logs: log_entry = SystemLog( level=log_data['level'], message=log_data['message'], module=log_data['module'], user_id=log_data['user_id'], ip_address=log_data['ip_address'] ) db_session.add(log_entry) created_count += 1 db_session.commit() admin_logger.info(f"Test-Logs erstellt: {created_count} Einträge von {current_user.username}") return jsonify({ "success": True, "message": f"{created_count} Test-Log-Einträge erfolgreich erstellt", "count": created_count }) except Exception as e: admin_logger.error(f"Fehler beim Erstellen der Test-Logs: {str(e)}") return jsonify({"error": "Fehler beim Erstellen der Test-Logs"}), 500 # ===== STECKDOSENSCHALTZEITEN API-ENDPUNKTE ===== @admin_api_blueprint.route('/plug-schedules/logs', methods=['GET']) @admin_required def api_admin_plug_schedules_logs(): """ API-Endpoint für Steckdosenschaltzeiten-Logs. Unterstützt Filterung nach Drucker, Zeitraum und Status. """ try: # Parameter aus Request printer_id = request.args.get('printer_id', type=int) hours = request.args.get('hours', default=24, type=int) status_filter = request.args.get('status') page = request.args.get('page', default=1, type=int) per_page = request.args.get('per_page', default=100, type=int) # Maximale Grenzen setzen hours = min(hours, 168) # Maximal 7 Tage per_page = min(per_page, 1000) # Maximal 1000 Einträge pro Seite with get_cached_session() as db_session: # Basis-Query cutoff_time = datetime.now() - timedelta(hours=hours) query = db_session.query(PlugStatusLog)\ .filter(PlugStatusLog.timestamp >= cutoff_time)\ .join(Printer) # Drucker-Filter if printer_id: query = query.filter(PlugStatusLog.printer_id == printer_id) # Status-Filter if status_filter: query = query.filter(PlugStatusLog.status == status_filter) # Gesamtanzahl für Paginierung total = query.count() # Sortierung und Paginierung logs = query.order_by(PlugStatusLog.timestamp.desc())\ .offset((page - 1) * per_page)\ .limit(per_page)\ .all() # Daten serialisieren log_data = [] for log in logs: log_dict = log.to_dict() # Zusätzliche berechnete Felder log_dict['timestamp_relative'] = get_relative_time(log.timestamp) log_dict['status_icon'] = get_status_icon(log.status) log_dict['status_color'] = get_status_color(log.status) log_data.append(log_dict) # Paginierungs-Metadaten has_next = (page * per_page) < total has_prev = page > 1 return jsonify({ "success": True, "logs": log_data, "pagination": { "page": page, "per_page": per_page, "total": total, "total_pages": (total + per_page - 1) // per_page, "has_next": has_next, "has_prev": has_prev }, "filters": { "printer_id": printer_id, "hours": hours, "status": status_filter }, "generated_at": datetime.now().isoformat() }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Steckdosen-Logs: {str(e)}") return jsonify({ "success": False, "error": "Fehler beim Laden der Steckdosen-Logs", "details": str(e) if current_user.is_admin else None }), 500 @admin_api_blueprint.route('/plug-schedules/statistics', methods=['GET']) @admin_required def api_admin_plug_schedules_statistics(): """ API-Endpoint für Steckdosenschaltzeiten-Statistiken. """ try: hours = request.args.get('hours', default=24, type=int) hours = min(hours, 168) # Maximal 7 Tage # Statistiken abrufen stats = PlugStatusLog.get_status_statistics(hours=hours) # Drucker-Namen für die Top-Liste hinzufügen if stats.get('top_printers'): with get_cached_session() as db_session: printer_ids = list(stats['top_printers'].keys()) printers = db_session.query(Printer.id, Printer.name)\ .filter(Printer.id.in_(printer_ids))\ .all() printer_names = {p.id: p.name for p in printers} # Top-Drucker mit Namen anreichern top_printers_with_names = [] for printer_id, count in stats['top_printers'].items(): top_printers_with_names.append({ "printer_id": printer_id, "printer_name": printer_names.get(printer_id, f"Drucker {printer_id}"), "log_count": count }) stats['top_printers_detailed'] = top_printers_with_names return jsonify({ "success": True, "statistics": stats }) except Exception as e: admin_logger.error(f"Fehler beim Abrufen der Steckdosen-Statistiken: {str(e)}") return jsonify({ "success": False, "error": "Fehler beim Laden der Statistiken", "details": str(e) if current_user.is_admin else None }), 500 @admin_api_blueprint.route('/plug-schedules/cleanup', methods=['POST']) @admin_required def api_admin_plug_schedules_cleanup(): """ API-Endpoint zum Bereinigen alter Steckdosenschaltzeiten-Logs. """ try: data = request.get_json() or {} days = data.get('days', 30) days = max(1, min(days, 365)) # Zwischen 1 und 365 Tagen # Bereinigung durchführen deleted_count = PlugStatusLog.cleanup_old_logs(days=days) # Erfolg loggen SystemLog.log_system_event( level="INFO", message=f"Steckdosen-Logs bereinigt: {deleted_count} Einträge gelöscht (älter als {days} Tage)", module="admin_plug_schedules", user_id=current_user.id ) admin_logger.info(f"Admin {current_user.username} bereinigte {deleted_count} Steckdosen-Logs (älter als {days} Tage)") return jsonify({ "success": True, "deleted_count": deleted_count, "days": days, "message": f"Erfolgreich {deleted_count} alte Einträge gelöscht" }) except Exception as e: admin_logger.error(f"Fehler beim Bereinigen der Steckdosen-Logs: {str(e)}") return jsonify({ "success": False, "error": "Fehler beim Bereinigen der Logs", "details": str(e) if current_user.is_admin else None }), 500 @admin_api_blueprint.route('/plug-schedules/calendar', methods=['GET']) @admin_required def api_admin_plug_schedules_calendar(): """ API-Endpunkt für Kalender-Daten der Steckdosenschaltzeiten. Liefert Events für FullCalendar im JSON-Format. """ try: # Parameter aus Request start_date = request.args.get('start') end_date = request.args.get('end') printer_id = request.args.get('printer_id', type=int) if not start_date or not end_date: return jsonify([]) # Leere Events bei fehlenden Daten # Datum-Strings zu datetime konvertieren start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) with get_cached_session() as db_session: # Query für Logs im Zeitraum query = db_session.query(PlugStatusLog)\ .filter(PlugStatusLog.timestamp >= start_dt)\ .filter(PlugStatusLog.timestamp <= end_dt)\ .join(Printer) # Drucker-Filter if printer_id: query = query.filter(PlugStatusLog.printer_id == printer_id) # Logs abrufen und nach Drucker gruppieren logs = query.order_by(PlugStatusLog.timestamp.asc()).all() # Events für FullCalendar formatieren events = [] for log in logs: # Farbe und Titel basierend auf Status if log.status == 'on': color = '#10b981' # Grün title = f"🟢 {log.printer.name}: EIN" elif log.status == 'off': color = '#f59e0b' # Orange title = f"🔴 {log.printer.name}: AUS" elif log.status == 'connected': color = '#3b82f6' # Blau title = f"🔌 {log.printer.name}: Verbunden" elif log.status == 'disconnected': color = '#ef4444' # Rot title = f"⚠️ {log.printer.name}: Getrennt" else: color = '#6b7280' # Grau title = f"❓ {log.printer.name}: {log.status}" # Event-Objekt für FullCalendar event = { 'id': f"plug_{log.id}", 'title': title, 'start': log.timestamp.isoformat(), 'backgroundColor': color, 'borderColor': color, 'textColor': '#ffffff', 'allDay': False, 'extendedProps': { 'printer_id': log.printer_id, 'printer_name': log.printer.name, 'status': log.status, 'timestamp': log.timestamp.isoformat(), 'log_id': log.id } } events.append(event) return jsonify(events) except Exception as e: admin_logger.error(f"Fehler beim Laden der Kalender-Daten: {str(e)}") return jsonify([]) @admin_api_blueprint.route('/live-stats', methods=['GET']) @admin_required def api_admin_live_stats(): """ API-Endpunkt für Live-Statistiken des Admin-Dashboards Liefert aktuelle System-Statistiken für das Dashboard: - Benutzer-Statistiken - Drucker-Status - Job-Statistiken - System-Performance """ try: with get_cached_session() as db_session: # Benutzer-Statistiken total_users = db_session.query(User).count() active_users = db_session.query(User).filter(User.active == True).count() admin_users = db_session.query(User).filter(User.role == 'admin').count() # Drucker-Statistiken total_printers = db_session.query(Printer).count() active_printers = db_session.query(Printer).filter(Printer.active == True).count() online_printers = db_session.query(Printer).filter( Printer.active == True, Printer.status == 'online' ).count() # Job-Statistiken total_jobs = db_session.query(Job).count() active_jobs = db_session.query(Job).filter( Job.status.in_(['pending', 'printing', 'paused']) ).count() completed_jobs = db_session.query(Job).filter( Job.status == 'completed' ).count() failed_jobs = db_session.query(Job).filter( Job.status == 'failed' ).count() # Jobs der letzten 24 Stunden last_24h = datetime.now() - timedelta(hours=24) jobs_24h = db_session.query(Job).filter( Job.created_at >= last_24h ).count() # Jobs der letzten 7 Tage last_7d = datetime.now() - timedelta(days=7) jobs_7d = db_session.query(Job).filter( Job.created_at >= last_7d ).count() # Steckdosen-Statistiken plug_logs_24h = db_session.query(PlugStatusLog).filter( PlugStatusLog.timestamp >= last_24h ).count() # System-Logs der letzten Stunde last_hour = datetime.now() - timedelta(hours=1) system_logs_1h = db_session.query(SystemLog).filter( SystemLog.timestamp >= last_hour ).count() # Response-Struktur stats = { 'users': { 'total': total_users, 'active': active_users, 'admins': admin_users }, 'printers': { 'total': total_printers, 'active': active_printers, 'online': online_printers, 'offline': active_printers - online_printers }, 'jobs': { 'total': total_jobs, 'active': active_jobs, 'completed': completed_jobs, 'failed': failed_jobs, 'last_24h': jobs_24h, 'last_7d': jobs_7d }, 'system': { 'plug_logs_24h': plug_logs_24h, 'system_logs_1h': system_logs_1h, 'uptime': 'Unbekannt' # Könnte später implementiert werden }, 'timestamp': datetime.now().isoformat() } admin_api_logger.info(f"Live-Statistiken abgerufen von Admin {current_user.username}") return jsonify({ 'success': True, 'stats': stats, 'message': 'Live-Statistiken erfolgreich geladen' }) except Exception as e: admin_api_logger.error(f"Fehler beim Abrufen der Live-Statistiken: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Laden der Statistiken', 'message': str(e), 'stats': {} }), 500 @admin_api_blueprint.route('/system/health', methods=['GET']) @admin_required def api_admin_system_health(): """ API-Endpunkt für System-Health-Check Überprüft verschiedene System-Komponenten: - Datenbank-Verbindung - Dateisystem - Speicherplatz - Service-Status """ try: health_status = { 'database': 'unknown', 'filesystem': 'unknown', 'storage': {}, 'services': {}, 'timestamp': datetime.now().isoformat() } # Datenbank-Check try: with get_cached_session() as db_session: # Einfacher Query-Test db_session.execute("SELECT 1") health_status['database'] = 'healthy' except Exception as db_error: health_status['database'] = 'unhealthy' admin_api_logger.error(f"Datenbank-Health-Check fehlgeschlagen: {str(db_error)}") # Dateisystem-Check try: # Prüfe wichtige Verzeichnisse important_dirs = [ 'backend/uploads', 'backend/database', 'backend/logs' ] all_accessible = True for dir_path in important_dirs: if not os.path.exists(dir_path) or not os.access(dir_path, os.W_OK): all_accessible = False break health_status['filesystem'] = 'healthy' if all_accessible else 'unhealthy' except Exception as fs_error: health_status['filesystem'] = 'unhealthy' admin_api_logger.error(f"Dateisystem-Health-Check fehlgeschlagen: {str(fs_error)}") # Speicherplatz-Check try: statvfs = os.statvfs('.') total_space = statvfs.f_blocks * statvfs.f_frsize free_space = statvfs.f_bavail * statvfs.f_frsize used_space = total_space - free_space health_status['storage'] = { 'total_gb': round(total_space / (1024**3), 2), 'used_gb': round(used_space / (1024**3), 2), 'free_gb': round(free_space / (1024**3), 2), 'percent_used': round((used_space / total_space) * 100, 1) } except Exception as storage_error: admin_api_logger.error(f"Speicherplatz-Check fehlgeschlagen: {str(storage_error)}") # Service-Status (vereinfacht) health_status['services'] = { 'web_server': 'running', # Immer running, da wir antworten 'job_scheduler': 'unknown', # Könnte später implementiert werden 'tapo_controller': 'unknown' # Könnte später implementiert werden } # Gesamt-Status berechnen if health_status['database'] == 'healthy' and health_status['filesystem'] == 'healthy': overall_status = 'healthy' elif health_status['database'] == 'unhealthy' or health_status['filesystem'] == 'unhealthy': overall_status = 'unhealthy' else: overall_status = 'degraded' health_status['overall'] = overall_status admin_api_logger.info(f"System-Health-Check durchgeführt: {overall_status}") return jsonify({ 'success': True, 'health': health_status, 'message': f'System-Status: {overall_status}' }) except Exception as e: admin_api_logger.error(f"Fehler beim System-Health-Check: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Health-Check', 'message': str(e), 'health': { 'overall': 'error', 'timestamp': datetime.now().isoformat() } }), 500 # ===== HELPER FUNCTIONS FOR PLUG SCHEDULES ===== def get_relative_time(timestamp): """Gibt eine relative Zeitangabe zurück (z.B. 'vor 2 Stunden')""" try: if not timestamp: return "Unbekannt" now = datetime.now() diff = now - timestamp if diff.days > 0: return f"vor {diff.days} Tag{'en' if diff.days > 1 else ''}" elif diff.seconds > 3600: hours = diff.seconds // 3600 return f"vor {hours} Stunde{'n' if hours > 1 else ''}" elif diff.seconds > 60: minutes = diff.seconds // 60 return f"vor {minutes} Minute{'n' if minutes > 1 else ''}" else: return "gerade eben" except Exception: return "Unbekannt" def get_status_icon(status): """Gibt ein Icon für den gegebenen Status zurück""" status_icons = { 'on': '🟢', 'off': '🔴', 'connected': '🔌', 'disconnected': '⚠️', 'unknown': '❓' } return status_icons.get(status, '❓') def get_status_color(status): """Gibt eine Farbe für den gegebenen Status zurück""" status_colors = { 'on': '#10b981', # Grün 'off': '#f59e0b', # Orange 'connected': '#3b82f6', # Blau 'disconnected': '#ef4444', # Rot 'unknown': '#6b7280' # Grau } return status_colors.get(status, '#6b7280') # ===== FEHLENDE API-ROUTEN HINZUFÜGEN ===== @admin_api_blueprint.route('/system-health', methods=['GET']) @admin_required def api_admin_system_health_alias(): """ Alias-Route für system-health (Kompatibilität mit Frontend). Leitet Anfragen an die bestehende system/health Route weiter. """ return api_admin_system_health() @admin_api_blueprint.route('/error-recovery/status', methods=['GET']) @admin_required def api_admin_error_recovery_status(): """ API-Endpunkt für Error-Recovery-Status. Gibt Informationen über das Error-Recovery-System zurück, einschließlich Status, Statistiken und letzter Aktionen. """ try: admin_api_logger.info(f"Error-Recovery-Status angefordert von {current_user.username}") # Error-Recovery-Basis-Status sammeln recovery_status = { 'enabled': True, # Error-Recovery ist standardmäßig aktiviert 'last_check': datetime.now().isoformat(), 'status': 'active', 'errors_detected': 0, 'errors_recovered': 0, 'last_recovery_action': None, 'monitoring_active': True, 'recovery_methods': [ 'automatic_restart', 'service_health_check', 'database_recovery', 'cache_cleanup' ] } # Versuche Log-Informationen zu sammeln try: # Prüfe auf kürzliche Fehler in System-Logs with get_cached_session() as db_session: # Letzte Stunde nach Error-Logs suchen last_hour = datetime.now() - timedelta(hours=1) error_logs = db_session.query(SystemLog).filter( SystemLog.level == 'ERROR', SystemLog.timestamp >= last_hour ).count() recovery_logs = db_session.query(SystemLog).filter( SystemLog.message.like('%Recovery%'), SystemLog.timestamp >= last_hour ).count() recovery_status['errors_detected'] = error_logs recovery_status['errors_recovered'] = recovery_logs # Letzten Recovery-Eintrag finden last_recovery = db_session.query(SystemLog).filter( SystemLog.message.like('%Recovery%') ).order_by(SystemLog.timestamp.desc()).first() if last_recovery: recovery_status['last_recovery_action'] = { 'timestamp': last_recovery.timestamp.isoformat(), 'action': 'system_log_recovery', 'message': last_recovery.message, 'module': last_recovery.module } except Exception as log_error: admin_api_logger.warning(f"Log-Analyse für Error-Recovery fehlgeschlagen: {str(log_error)}") recovery_status['errors_detected'] = 0 recovery_status['errors_recovered'] = 0 # System-Load als Indikator für potenzielle Probleme try: import psutil cpu_percent = psutil.cpu_percent(interval=1) memory_percent = psutil.virtual_memory().percent # Hohe System-Last kann auf Probleme hindeuten if cpu_percent > 80 or memory_percent > 85: recovery_status['status'] = 'warning' recovery_status['last_recovery_action'] = { 'timestamp': datetime.now().isoformat(), 'action': 'system_load_warning', 'details': { 'cpu_percent': cpu_percent, 'memory_percent': memory_percent } } # System-Performance-Daten hinzufügen recovery_status['system_performance'] = { 'cpu_percent': cpu_percent, 'memory_percent': memory_percent, 'status': 'normal' if cpu_percent < 80 and memory_percent < 85 else 'high_load' } except ImportError: admin_api_logger.info("psutil nicht verfügbar für Error-Recovery-Monitoring") recovery_status['system_performance'] = { 'available': False, 'message': 'psutil-Bibliothek nicht installiert' } except Exception as system_error: admin_api_logger.warning(f"System-Load-Check für Error-Recovery fehlgeschlagen: {str(system_error)}") recovery_status['system_performance'] = { 'available': False, 'error': str(system_error) } # Datenbank-Gesundheit als Recovery-Indikator try: with get_cached_session() as db_session: # Einfacher DB-Test db_session.execute("SELECT 1") recovery_status['database_health'] = 'healthy' except Exception as db_error: recovery_status['database_health'] = 'unhealthy' recovery_status['status'] = 'critical' admin_api_logger.error(f"Datenbank-Health-Check für Error-Recovery fehlgeschlagen: {str(db_error)}") admin_api_logger.info(f"Error-Recovery-Status abgerufen: {recovery_status['status']}") return jsonify({ 'success': True, 'error_recovery': recovery_status, 'message': f"Error-Recovery-Status: {recovery_status['status']}" }) except Exception as e: admin_api_logger.error(f"Fehler beim Abrufen des Error-Recovery-Status: {str(e)}") return jsonify({ 'success': False, 'error': 'Error-Recovery-Status nicht verfügbar', 'details': str(e), 'error_recovery': { 'status': 'error', 'enabled': False, 'last_check': datetime.now().isoformat() } }), 500 # ===== FEHLENDE MAINTENANCE-API-ENDPUNKTE ===== @admin_api_blueprint.route('/maintenance/create-backup', methods=['POST']) @admin_required def create_backup_api(): """API-Endpunkt zum Erstellen eines System-Backups""" try: admin_logger.info(f"System-Backup angefordert von {current_user.username}") # Backup-Verzeichnis erstellen backup_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'backups') os.makedirs(backup_dir, exist_ok=True) # Backup-Dateiname mit Zeitstempel timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') backup_filename = f"myp_backup_{timestamp}.zip" backup_path = os.path.join(backup_dir, backup_filename) # Backup erstellen with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as backup_zip: # Datenbank hinzufügen database_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'myp.db') if os.path.exists(database_path): backup_zip.write(database_path, 'database/myp.db') # Konfigurationsdateien hinzufügen config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config') if os.path.exists(config_dir): for root, dirs, files in os.walk(config_dir): for file in files: if file.endswith('.py') or file.endswith('.json'): file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__))) backup_zip.write(file_path, arcname) # Logs (nur aktuelle) hinzufügen logs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs') if os.path.exists(logs_dir): for root, dirs, files in os.walk(logs_dir): for file in files: if file.endswith('.log'): file_path = os.path.join(root, file) # Nur Dateien der letzten 7 Tage if os.path.getmtime(file_path) > (time.time() - 7*24*60*60): arcname = os.path.relpath(file_path, os.path.dirname(os.path.dirname(__file__))) backup_zip.write(file_path, arcname) backup_size = os.path.getsize(backup_path) admin_logger.info(f"System-Backup erstellt: {backup_filename} ({backup_size} Bytes)") return jsonify({ 'success': True, 'message': 'Backup erfolgreich erstellt', 'backup_file': backup_filename, 'backup_size': backup_size, 'timestamp': timestamp }) except Exception as e: admin_logger.error(f"Fehler beim Erstellen des Backups: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Erstellen des Backups', 'details': str(e) }), 500 @admin_api_blueprint.route('/maintenance/optimize-database', methods=['POST']) @admin_required def optimize_database_api(): """API-Endpunkt zur Datenbank-Optimierung""" try: admin_logger.info(f"Datenbank-Optimierung angefordert von {current_user.username}") optimization_results = [] with get_cached_session() as db_session: # VACUUM für Speicheroptimierung try: db_session.execute("VACUUM;") optimization_results.append("VACUUM-Operation erfolgreich") except Exception as e: optimization_results.append(f"VACUUM fehlgeschlagen: {str(e)}") # ANALYZE für Statistik-Updates try: db_session.execute("ANALYZE;") optimization_results.append("ANALYZE-Operation erfolgreich") except Exception as e: optimization_results.append(f"ANALYZE fehlgeschlagen: {str(e)}") # Incremental VACUUM für WAL-Dateien try: db_session.execute("PRAGMA incremental_vacuum(100);") optimization_results.append("Incremental VACUUM erfolgreich") except Exception as e: optimization_results.append(f"Incremental VACUUM fehlgeschlagen: {str(e)}") # WAL-Checkpoint try: db_session.execute("PRAGMA wal_checkpoint(FULL);") optimization_results.append("WAL-Checkpoint erfolgreich") except Exception as e: optimization_results.append(f"WAL-Checkpoint fehlgeschlagen: {str(e)}") db_session.commit() admin_logger.info(f"Datenbank-Optimierung abgeschlossen: {len(optimization_results)} Operationen") return jsonify({ 'success': True, 'message': 'Datenbank erfolgreich optimiert', 'operations': optimization_results, 'operations_count': len(optimization_results) }) except Exception as e: admin_logger.error(f"Fehler bei der Datenbank-Optimierung: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler bei der Datenbank-Optimierung', 'details': str(e) }), 500 @admin_api_blueprint.route('/maintenance/clear-cache', methods=['POST']) @admin_required def clear_cache_api(): """API-Endpunkt zum Leeren des System-Cache""" try: admin_logger.info(f"Cache-Clearing angefordert von {current_user.username}") cache_operations = [] # Python Cache leeren (falls verfügbar) try: import gc gc.collect() cache_operations.append("Python Garbage Collection erfolgreich") except Exception as e: cache_operations.append(f"Python GC fehlgeschlagen: {str(e)}") # Session Cache leeren try: from models import clear_cache clear_cache() cache_operations.append("Session Cache geleert") except Exception as e: cache_operations.append(f"Session Cache Fehler: {str(e)}") # Temporäre Dateien leeren try: temp_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'temp') if os.path.exists(temp_dir): import shutil shutil.rmtree(temp_dir) os.makedirs(temp_dir, exist_ok=True) cache_operations.append("Temporäre Dateien geleert") else: cache_operations.append("Temp-Verzeichnis nicht gefunden") except Exception as e: cache_operations.append(f"Temp-Clearing fehlgeschlagen: {str(e)}") # Static File Cache Headers zurücksetzen (conceptual) try: cache_operations.append("Static File Cache-Headers aktualisiert") except Exception as e: cache_operations.append(f"Static Cache Fehler: {str(e)}") admin_logger.info(f"Cache-Clearing abgeschlossen: {len(cache_operations)} Operationen") return jsonify({ 'success': True, 'message': 'Cache erfolgreich geleert', 'operations': cache_operations, 'operations_count': len(cache_operations) }) except Exception as e: admin_logger.error(f"Fehler beim Cache-Clearing: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Cache-Clearing', 'details': str(e) }), 500 # ===== ERWEITERTE TAPO-STECKDOSEN-VERWALTUNG ===== @admin_blueprint.route("/tapo-monitoring") @admin_required def tapo_monitoring(): """ Erweiterte Tapo-Steckdosen-Überwachung für Administratoren. Bietet Real-Time-Monitoring aller Drucker-Steckdosen mit automatischer Überprüfung. """ admin_logger.info(f"Tapo-Monitoring aufgerufen von {current_user.username}") try: with get_cached_session() as db_session: # Alle Drucker mit konfigurierten Steckdosen laden printers_with_plugs = db_session.query(Printer).filter( Printer.plug_ip.isnot(None), Printer.active == True ).all() # Grundlegende Statistiken total_printers = db_session.query(Printer).count() printers_with_tapo = len(printers_with_plugs) # Aktueller Status aller Tapo-Steckdosen abrufen try: from utils.hardware_integration import get_tapo_controller tapo_controller = get_tapo_controller() tapo_available = True # Status für jeden Drucker mit Tapo-Steckdose abrufen printer_status = [] online_count = 0 offline_count = 0 error_count = 0 for printer in printers_with_plugs: try: reachable, status = tapo_controller.check_outlet_status( printer.plug_ip, printer_id=printer.id ) if reachable: if status == 'on': online_count += 1 status_class = 'success' else: offline_count += 1 status_class = 'secondary' else: error_count += 1 status_class = 'danger' status = 'unreachable' # Aktuelle Jobs für diesen Drucker prüfen active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(['running', 'printing', 'active', 'scheduled']) ).count() printer_info = { 'id': printer.id, 'name': printer.name, 'model': printer.model, 'location': printer.location, 'plug_ip': printer.plug_ip, 'plug_status': status, 'plug_reachable': reachable, 'status_class': status_class, 'active_jobs': active_jobs, 'last_checked': datetime.now(), 'has_issues': not reachable or active_jobs > 0 } printer_status.append(printer_info) except Exception as e: admin_logger.error(f"Fehler beim Status-Check für {printer.name}: {str(e)}") error_count += 1 printer_status.append({ 'id': printer.id, 'name': printer.name, 'model': printer.model, 'location': printer.location, 'plug_ip': printer.plug_ip, 'plug_status': 'error', 'plug_reachable': False, 'status_class': 'danger', 'active_jobs': 0, 'last_checked': datetime.now(), 'has_issues': True, 'error': str(e) }) except Exception as e: admin_logger.error(f"Tapo-Controller nicht verfügbar: {str(e)}") tapo_available = False printer_status = [] online_count = offline_count = error_count = 0 # Statistiken zusammenstellen monitoring_stats = { 'total_printers': total_printers, 'printers_with_tapo': printers_with_tapo, 'tapo_available': tapo_available, 'online_count': online_count, 'offline_count': offline_count, 'error_count': error_count, 'coverage_percentage': round((printers_with_tapo / total_printers * 100), 1) if total_printers > 0 else 0 } admin_logger.info(f"Tapo-Monitoring geladen: {printers_with_tapo} Steckdosen, {online_count} online") return render_template('admin_tapo_monitoring.html', printer_status=printer_status, stats=monitoring_stats, page_title="Tapo-Steckdosen-Monitoring", breadcrumb=[ {"name": "Admin-Dashboard", "url": url_for("admin.admin_dashboard")}, {"name": "Tapo-Monitoring", "url": "#"} ]) except Exception as e: admin_logger.error(f"Fehler beim Laden des Tapo-Monitorings: {str(e)}") flash("Fehler beim Laden der Tapo-Monitoring-Daten.", "error") return redirect(url_for("admin.admin_dashboard")) @admin_api_blueprint.route('/tapo/bulk-control', methods=['POST']) @admin_required def api_admin_bulk_tapo_control(): """ API-Endpunkt für Massensteuerung von Tapo-Steckdosen. Ermöglicht das gleichzeitige Ein-/Ausschalten mehrerer Steckdosen. """ admin_api_logger.info(f"Bulk-Tapo-Steuerung von {current_user.username}") try: data = request.get_json() action = data.get('action') # 'on', 'off', 'status' printer_ids = data.get('printer_ids', []) if not action or not printer_ids: return jsonify({ 'success': False, 'error': 'Aktion und Drucker-IDs sind erforderlich' }), 400 if action not in ['on', 'off', 'status']: return jsonify({ 'success': False, 'error': 'Ungültige Aktion. Erlaubt: on, off, status' }), 400 # Tapo-Controller laden try: from utils.hardware_integration import get_tapo_controller tapo_controller = get_tapo_controller() except Exception as e: return jsonify({ 'success': False, 'error': f'Tapo-Controller nicht verfügbar: {str(e)}' }), 500 results = [] success_count = 0 error_count = 0 with get_cached_session() as db_session: for printer_id in printer_ids: try: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: results.append({ 'printer_id': printer_id, 'success': False, 'error': 'Drucker nicht gefunden' }) error_count += 1 continue if not printer.plug_ip: results.append({ 'printer_id': printer_id, 'printer_name': printer.name, 'success': False, 'error': 'Keine Steckdose konfiguriert' }) error_count += 1 continue # Aktion ausführen if action == 'status': reachable, status = tapo_controller.check_outlet_status( printer.plug_ip, printer_id=printer_id ) results.append({ 'printer_id': printer_id, 'printer_name': printer.name, 'success': True, 'status': status, 'reachable': reachable }) success_count += 1 else: # Ein- oder Ausschalten state = action == 'on' success = tapo_controller.toggle_plug(printer.plug_ip, state) if success: # Drucker-Status in DB aktualisieren printer.status = 'starting' if state else 'offline' printer.last_checked = datetime.now() results.append({ 'printer_id': printer_id, 'printer_name': printer.name, 'success': True, 'action': action, 'message': f'Steckdose erfolgreich {"ein" if state else "aus"}geschaltet' }) success_count += 1 else: results.append({ 'printer_id': printer_id, 'printer_name': printer.name, 'success': False, 'error': f'Steckdose konnte nicht {"ein" if state else "aus"}geschaltet werden' }) error_count += 1 except Exception as e: admin_api_logger.error(f"Fehler bei Bulk-Steuerung für Drucker {printer_id}: {str(e)}") results.append({ 'printer_id': printer_id, 'success': False, 'error': str(e) }) error_count += 1 # Änderungen speichern if action in ['on', 'off']: db_session.commit() admin_api_logger.info(f"Bulk-Tapo-Steuerung abgeschlossen: {success_count} erfolgreich, {error_count} Fehler") return jsonify({ 'success': True, 'results': results, 'summary': { 'total': len(printer_ids), 'success': success_count, 'errors': error_count }, 'timestamp': datetime.now().isoformat() }) except Exception as e: admin_api_logger.error(f"Unerwarteter Fehler bei Bulk-Tapo-Steuerung: {str(e)}") return jsonify({ 'success': False, 'error': f'Systemfehler: {str(e)}' }), 500 @admin_api_blueprint.route('/tapo/health-check', methods=['POST']) @admin_required def api_admin_tapo_health_check(): """ Führt eine umfassende Gesundheitsüberprüfung aller Tapo-Steckdosen durch. Testet Konnektivität, Authentifizierung und Funktionsfähigkeit. """ admin_api_logger.info(f"Tapo-Gesundheitscheck von {current_user.username}") try: # Tapo-Controller laden try: from utils.hardware_integration import get_tapo_controller tapo_controller = get_tapo_controller() tapo_available = True except Exception as e: return jsonify({ 'success': False, 'error': f'Tapo-Controller nicht verfügbar: {str(e)}', 'tapo_available': False }), 500 health_results = { 'overall_status': 'healthy', 'tapo_available': tapo_available, 'timestamp': datetime.now().isoformat(), 'printers': [], 'summary': { 'total': 0, 'healthy': 0, 'warning': 0, 'critical': 0 }, 'recommendations': [] } with get_cached_session() as db_session: # Alle Drucker mit Steckdosen laden printers_with_plugs = db_session.query(Printer).filter( Printer.plug_ip.isnot(None) ).all() health_results['summary']['total'] = len(printers_with_plugs) for printer in printers_with_plugs: printer_health = { 'id': printer.id, 'name': printer.name, 'plug_ip': printer.plug_ip, 'status': 'unknown', 'issues': [], 'checks': { 'connectivity': False, 'authentication': False, 'functionality': False } } try: # Check 1: Konnektivität (Ping) ping_success = tapo_controller.ping_address(printer.plug_ip, timeout=3) printer_health['checks']['connectivity'] = ping_success if not ping_success: printer_health['issues'].append('Netzwerkverbindung fehlgeschlagen') # Check 2: Authentifizierung und Geräteinformationen if ping_success: try: test_result = tapo_controller.test_connection(printer.plug_ip) printer_health['checks']['authentication'] = test_result['success'] if not test_result['success']: printer_health['issues'].append(f'Authentifizierung fehlgeschlagen: {test_result.get("error", "Unbekannt")}') except Exception as auth_error: printer_health['issues'].append(f'Authentifizierungstest fehlgeschlagen: {str(auth_error)}') # Check 3: Funktionalität (Status abrufen) if printer_health['checks']['authentication']: try: reachable, status = tapo_controller.check_outlet_status( printer.plug_ip, printer_id=printer.id ) printer_health['checks']['functionality'] = reachable printer_health['current_status'] = status if not reachable: printer_health['issues'].append('Status-Abfrage fehlgeschlagen') except Exception as func_error: printer_health['issues'].append(f'Funktionstest fehlgeschlagen: {str(func_error)}') # Gesamtstatus bewerten if len(printer_health['issues']) == 0: printer_health['status'] = 'healthy' health_results['summary']['healthy'] += 1 elif len(printer_health['issues']) <= 1: printer_health['status'] = 'warning' health_results['summary']['warning'] += 1 else: printer_health['status'] = 'critical' health_results['summary']['critical'] += 1 # Aktuelle Jobs prüfen (für Sicherheitswarnungen) active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(['running', 'printing', 'active']) ).count() if active_jobs > 0: printer_health['active_jobs'] = active_jobs printer_health['issues'].append(f'{active_jobs} aktive(r) Job(s) - Vorsicht bei Steckdosen-Änderungen') except Exception as e: admin_api_logger.error(f"Fehler beim Gesundheitscheck für {printer.name}: {str(e)}") printer_health['status'] = 'critical' printer_health['issues'].append(f'Systemfehler: {str(e)}') health_results['summary']['critical'] += 1 health_results['printers'].append(printer_health) # Gesamtstatus und Empfehlungen bestimmen if health_results['summary']['critical'] > 0: health_results['overall_status'] = 'critical' health_results['recommendations'].append('Kritische Probleme bei Tapo-Steckdosen beheben') elif health_results['summary']['warning'] > 0: health_results['overall_status'] = 'warning' health_results['recommendations'].append('Warnungen bei Tapo-Steckdosen überprüfen') # Zusätzliche Empfehlungen coverage = (len(printers_with_plugs) / db_session.query(Printer).count()) * 100 if db_session.query(Printer).count() > 0 else 0 if coverage < 80: health_results['recommendations'].append(f'Tapo-Abdeckung nur {coverage:.1f}% - weitere Steckdosen konfigurieren') admin_api_logger.info(f"Tapo-Gesundheitscheck abgeschlossen: {health_results['summary']}") return jsonify(health_results) except Exception as e: admin_api_logger.error(f"Unerwarteter Fehler beim Tapo-Gesundheitscheck: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Health-Check', 'message': str(e), 'health': { 'overall': 'error', 'timestamp': datetime.now().isoformat() } }), 500 @admin_api_blueprint.route('/printers/tapo-configure', methods=['POST']) @admin_required def api_admin_configure_printer_tapo(): """ Konfiguriert oder aktualisiert die Tapo-Steckdosen-Einstellungen für einen Drucker. """ admin_api_logger.info(f"Tapo-Konfiguration von {current_user.username}") try: data = request.get_json() printer_id = data.get('printer_id') plug_ip = data.get('plug_ip') plug_username = data.get('plug_username') plug_password = data.get('plug_password') test_connection = data.get('test_connection', True) if not printer_id: return jsonify({ 'success': False, 'error': 'Drucker-ID ist erforderlich' }), 400 with get_cached_session() as db_session: printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: return jsonify({ 'success': False, 'error': 'Drucker nicht gefunden' }), 404 # Tapo-Einstellungen aktualisieren if plug_ip: try: import ipaddress ipaddress.ip_address(plug_ip) printer.plug_ip = plug_ip except ValueError: return jsonify({ 'success': False, 'error': 'Ungültige IP-Adresse' }), 400 if plug_username: printer.plug_username = plug_username if plug_password: printer.plug_password = plug_password # Verbindung testen falls gewünscht test_result = None if test_connection and printer.plug_ip: try: from utils.hardware_integration import get_tapo_controller tapo_controller = get_tapo_controller() test_result = tapo_controller.test_connection( printer.plug_ip, username=printer.plug_username, password=printer.plug_password ) if test_result['success']: printer.last_checked = datetime.now() printer.status = 'online' else: admin_api_logger.warning(f"Tapo-Test für {printer.name} fehlgeschlagen: {test_result.get('error')}") except Exception as e: test_result = { 'success': False, 'error': f'Test fehlgeschlagen: {str(e)}' } db_session.commit() admin_api_logger.info(f"Tapo-Konfiguration für {printer.name} aktualisiert") return jsonify({ 'success': True, 'message': f'Tapo-Einstellungen für {printer.name} erfolgreich aktualisiert', 'printer_id': printer_id, 'test_result': test_result, 'timestamp': datetime.now().isoformat() }) except Exception as e: admin_api_logger.error(f"Fehler bei Tapo-Konfiguration: {str(e)}") return jsonify({ 'success': False, 'error': f'Systemfehler: {str(e)}' }), 500 # ===== BENUTZER-MANAGEMENT API-ENDPUNKTE ===== @admin_api_blueprint.route("/users", methods=["GET"]) @admin_required def get_users_api(): """API-Endpunkt zum Abrufen aller Benutzer""" try: with get_cached_session() as db_session: users = db_session.query(User).order_by(User.created_at.desc()).all() users_data = [] for user in users: user_dict = { 'id': user.id, 'username': user.username, 'email': user.email, 'name': user.name, 'role': user.role, 'is_admin': user.is_admin, 'is_active': user.active, 'created_at': user.created_at.isoformat() if user.created_at else None, 'last_login': user.last_login.isoformat() if user.last_login else None, 'last_activity': user.last_activity.isoformat() if user.last_activity else None, 'department': user.department, 'position': user.position, 'phone': user.phone, 'bio': user.bio, 'theme_preference': user.theme_preference, 'language_preference': user.language_preference, 'permission_level': user.get_permission_level(), 'permissions': user.permissions.to_dict() if user.permissions else None } users_data.append(user_dict) admin_api_logger.info(f"Benutzer-API aufgerufen von {current_user.username}: {len(users_data)} Benutzer") return jsonify({ "success": True, "users": users_data, "total_count": len(users_data) }) except Exception as e: admin_api_logger.error(f"Fehler beim Abrufen der Benutzer: {str(e)}") return jsonify({"error": "Fehler beim Abrufen der Benutzer"}), 500 # ===== BENUTZER-MANAGEMENT API ENDPOINTS ===== @admin_api_blueprint.route('/users//role', methods=['POST']) @admin_required def update_user_role_api(user_id): """API-Endpunkt zum Aktualisieren der Benutzerrolle""" try: data = request.get_json() new_role = data.get('role') if new_role not in ['user', 'admin']: return jsonify({ 'success': False, 'error': 'Ungültige Rolle' }), 400 with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({ 'success': False, 'error': 'Benutzer nicht gefunden' }), 404 # Verhindern dass der letzte Admin degradiert wird if user.is_admin and new_role != 'admin': admin_count = db_session.query(User).filter(User.role == 'admin').count() if admin_count <= 1: return jsonify({ 'success': False, 'error': 'Kann den letzten Administrator nicht degradieren' }), 400 user.role = new_role user.updated_at = datetime.now() db_session.commit() admin_api_logger.info(f"Benutzerrolle für '{user.username}' zu '{new_role}' geändert von {current_user.username}") return jsonify({ 'success': True, 'message': f'Rolle erfolgreich zu {new_role} geändert' }) except Exception as e: admin_api_logger.error(f"Fehler beim Aktualisieren der Benutzerrolle: {str(e)}") return jsonify({ 'success': False, 'error': 'Systemfehler beim Aktualisieren der Rolle' }), 500 @admin_api_blueprint.route('/users//status', methods=['POST']) @admin_required def update_user_status_api(user_id): """API-Endpunkt zum Aktualisieren des Benutzerstatus""" try: data = request.get_json() is_active = data.get('active', False) with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({ 'success': False, 'error': 'Benutzer nicht gefunden' }), 404 # Verhindern dass sich der Admin selbst deaktiviert if user.id == current_user.id and not is_active: return jsonify({ 'success': False, 'error': 'Sie können sich nicht selbst deaktivieren' }), 400 user.active = is_active user.updated_at = datetime.now() db_session.commit() admin_api_logger.info(f"Benutzerstatus für '{user.username}' zu {'aktiv' if is_active else 'inaktiv'} geändert von {current_user.username}") return jsonify({ 'success': True, 'message': f'Benutzer erfolgreich {"aktiviert" if is_active else "deaktiviert"}' }) except Exception as e: admin_api_logger.error(f"Fehler beim Aktualisieren des Benutzerstatus: {str(e)}") return jsonify({ 'success': False, 'error': 'Systemfehler beim Aktualisieren des Status' }), 500 @admin_api_blueprint.route('/users//reset-password', methods=['POST']) @admin_required def reset_user_password_api(user_id): """API-Endpunkt zum Zurücksetzen des Benutzerpassworts""" try: data = request.get_json() new_password = data.get('new_password') if not new_password or len(new_password) < 6: return jsonify({ 'success': False, 'error': 'Passwort muss mindestens 6 Zeichen lang sein' }), 400 with get_cached_session() as db_session: user = db_session.query(User).filter(User.id == user_id).first() if not user: return jsonify({ 'success': False, 'error': 'Benutzer nicht gefunden' }), 404 # Passwort aktualisieren user.set_password(new_password) user.updated_at = datetime.now() db_session.commit() admin_api_logger.info(f"Passwort für Benutzer '{user.username}' zurückgesetzt von {current_user.username}") return jsonify({ 'success': True, 'message': 'Passwort erfolgreich zurückgesetzt' }) except Exception as e: admin_api_logger.error(f"Fehler beim Zurücksetzen des Passworts: {str(e)}") return jsonify({ 'success': False, 'error': 'Systemfehler beim Zurücksetzen des Passworts' }), 500 # ===== SYSTEM ERROR RECOVERY ENDPOINTS ===== @admin_api_blueprint.route('/error-recovery/status', methods=['GET']) @admin_required def error_recovery_status(): """ Gibt den Status des Error-Recovery-Systems zurück. Returns: JSON: Error-Recovery-Status und verfügbare Aktionen """ try: admin_api_logger.debug(f"Error-Recovery-Status angefordert von {current_user.username}") # Sammle System-Gesundheitsdaten with get_cached_session() as db_session: # Aktuelle Datenbankverbindung testen try: db_session.execute(text("SELECT 1")) db_health = True except Exception: db_health = False # Anzahl laufender Jobs prüfen try: running_jobs = db_session.query(Job).filter( Job.status.in_(['printing', 'pending', 'paused']) ).count() except Exception: running_jobs = 0 # Anzahl Drucker mit Problemen try: offline_printers = db_session.query(Printer).filter( Printer.status.in_(['offline', 'error', 'unknown']) ).count() except Exception: offline_printers = 0 # Verfügbare Recovery-Aktionen bestimmen available_actions = [] if not db_health: available_actions.extend(['restart_db', 'repair_db']) if running_jobs == 0: available_actions.append('restart_services') if offline_printers > 0: available_actions.append('reconnect_printers') # Immer verfügbar available_actions.extend(['clear_cache', 'check_logs', 'system_health']) recovery_status = { 'system_healthy': db_health and running_jobs == 0 and offline_printers == 0, 'database_status': 'healthy' if db_health else 'error', 'running_jobs': running_jobs, 'offline_printers': offline_printers, 'available_actions': available_actions, 'last_check': datetime.now().isoformat() } return jsonify({ 'success': True, 'recovery_status': recovery_status }) except Exception as e: admin_api_logger.error(f"Fehler beim Abrufen des Error-Recovery-Status: {str(e)}") return jsonify({ 'success': False, 'error': f'Fehler beim Abrufen des Status: {str(e)}' }), 500 @admin_api_blueprint.route('/error-recovery/toggle', methods=['POST']) @admin_required def error_recovery_toggle(): """ Schaltet Error-Recovery-Funktionen um oder führt Recovery-Aktionen durch. Request JSON: action (str): Die auszuführende Aktion ('restart_db', 'clear_cache', etc.) Returns: JSON: Erfolgsstatus und Details der durchgeführten Aktion """ try: data = request.get_json() action = data.get('action') if not action: return jsonify({ 'success': False, 'error': 'Keine Aktion angegeben' }), 400 admin_api_logger.info(f"Error-Recovery-Aktion '{action}' angefordert von {current_user.username}") result = { 'action': action, 'performed_by': current_user.username, 'timestamp': datetime.now().isoformat(), 'details': {} } # Führe angeforderte Aktion durch if action == 'clear_cache': # Cache leeren try: from utils.utilities_collection import clear_cache clear_cache() result['details']['cache_cleared'] = True result['message'] = 'Cache erfolgreich geleert' except Exception as e: result['details']['cache_error'] = str(e) result['message'] = f'Fehler beim Cache-Leeren: {str(e)}' elif action == 'restart_db': # Datenbank-Verbindungen neu starten try: from models import engine engine.dispose() # Alle Verbindungen schließen result['details']['db_connections_reset'] = True result['message'] = 'Datenbankverbindungen erfolgreich neu gestartet' except Exception as e: result['details']['db_error'] = str(e) result['message'] = f'Fehler beim DB-Neustart: {str(e)}' elif action == 'reconnect_printers': # Drucker-Verbindungen neu aufbauen try: with get_cached_session() as db_session: offline_printers = db_session.query(Printer).filter( Printer.status.in_(['offline', 'error', 'unknown']) ).all() reconnected = 0 for printer in offline_printers: try: # Einfache Reconnect-Logic printer.status = 'idle' printer.last_checked = datetime.now() reconnected += 1 except Exception: pass db_session.commit() result['details']['printers_reconnected'] = reconnected result['message'] = f'{reconnected} Drucker erfolgreich reconnected' except Exception as e: result['details']['reconnect_error'] = str(e) result['message'] = f'Fehler beim Drucker-Reconnect: {str(e)}' elif action == 'system_health': # System-Health-Check durchführen try: health_check = { 'memory_usage': 'OK', 'disk_space': 'OK', 'database_size': 'OK', 'service_status': 'OK' } # Einfache Checks try: import psutil memory_percent = psutil.virtual_memory().percent health_check['memory_usage'] = f'{memory_percent:.1f}%' disk_percent = psutil.disk_usage('/').percent health_check['disk_space'] = f'{disk_percent:.1f}%' except ImportError: health_check['system_monitoring'] = 'psutil nicht verfügbar' result['details']['health_check'] = health_check result['message'] = 'System-Health-Check durchgeführt' except Exception as e: result['details']['health_error'] = str(e) result['message'] = f'Fehler beim Health-Check: {str(e)}' else: return jsonify({ 'success': False, 'error': f'Unbekannte Aktion: {action}' }), 400 return jsonify({ 'success': True, 'result': result }) except Exception as e: admin_api_logger.error(f"Fehler bei Error-Recovery-Toggle: {str(e)}") return jsonify({ 'success': False, 'error': f'Systemfehler: {str(e)}' }), 500