""" API-Blueprint für das 3D-Druck-Management-System Dieses Modul enthält allgemeine API-Endpunkte und WebSocket-Fallback-Funktionalität. """ import logging from datetime import datetime, timedelta from flask import Blueprint, jsonify, request, session from flask_login import login_required, current_user from models import get_db_session, User, Notification, Printer, Job, Stats from utils.logging_config import get_logger from utils.permissions import admin_required # Blueprint erstellen api_blueprint = Blueprint('api', __name__, url_prefix='/api') # Logger initialisieren api_logger = get_logger("api") @api_blueprint.route('/ws-fallback', methods=['GET']) @login_required def ws_fallback(): """WebSocket-Fallback für Browser ohne WebSocket-Unterstützung""" try: # Einfache Polling-Antwort für Clients ohne WebSocket return jsonify({ 'success': True, 'timestamp': datetime.now().isoformat(), 'user_id': current_user.id, 'message': 'WebSocket-Fallback aktiv' }) except Exception as e: api_logger.error(f"Fehler im WebSocket-Fallback: {str(e)}") return jsonify({'error': 'WebSocket-Fallback-Fehler'}), 500 @api_blueprint.route('/notifications', methods=['GET']) @login_required def get_notifications(): """Abrufen der Benutzer-Benachrichtigungen""" try: db_session = get_db_session() # Benutzer-spezifische Benachrichtigungen notifications = db_session.query(Notification).filter( Notification.user_id == current_user.id, Notification.is_read == False ).order_by(Notification.created_at.desc()).limit(20).all() notification_list = [] for notification in notifications: notification_list.append({ 'id': notification.id, 'title': notification.title, 'message': notification.message, 'type': notification.type, 'created_at': notification.created_at.isoformat(), 'is_read': notification.is_read }) db_session.close() return jsonify({ 'success': True, 'notifications': notification_list, 'count': len(notification_list) }) except Exception as e: api_logger.error(f"Fehler beim Abrufen der Benachrichtigungen: {str(e)}") return jsonify({'error': 'Fehler beim Laden der Benachrichtigungen'}), 500 @api_blueprint.route('/notifications//read', methods=['POST']) @login_required def mark_notification_read(notification_id): """Markiert eine Benachrichtigung als gelesen""" try: db_session = get_db_session() # Benachrichtigung finden und prüfen ob sie dem aktuellen Benutzer gehört notification = db_session.query(Notification).filter( Notification.id == notification_id, Notification.user_id == current_user.id ).first() if not notification: return jsonify({'error': 'Benachrichtigung nicht gefunden'}), 404 # Als gelesen markieren notification.is_read = True notification.read_at = datetime.now() db_session.commit() db_session.close() api_logger.info(f"Benachrichtigung {notification_id} als gelesen markiert von Benutzer {current_user.id}") return jsonify({ 'success': True, 'message': 'Benachrichtigung als gelesen markiert' }) except Exception as e: api_logger.error(f"Fehler beim Markieren der Benachrichtigung als gelesen: {str(e)}") return jsonify({'error': 'Fehler beim Markieren als gelesen'}), 500 @api_blueprint.route('/notifications/mark-all-read', methods=['POST']) @login_required def mark_all_notifications_read(): """Markiert alle Benachrichtigungen des Benutzers als gelesen""" try: db_session = get_db_session() # Alle ungelesenen Benachrichtigungen des Benutzers finden unread_notifications = db_session.query(Notification).filter( Notification.user_id == current_user.id, Notification.is_read == False ).all() # Alle als gelesen markieren for notification in unread_notifications: notification.is_read = True notification.read_at = datetime.now() db_session.commit() count = len(unread_notifications) db_session.close() api_logger.info(f"{count} Benachrichtigungen als gelesen markiert von Benutzer {current_user.id}") return jsonify({ 'success': True, 'message': f'{count} Benachrichtigungen als gelesen markiert', 'count': count }) except Exception as e: api_logger.error(f"Fehler beim Markieren aller Benachrichtigungen als gelesen: {str(e)}") return jsonify({'error': 'Fehler beim Markieren aller als gelesen'}), 500 @api_blueprint.route('/system/status', methods=['GET']) @login_required def system_status(): """Gibt den System-Status zurück""" try: return jsonify({ 'success': True, 'status': 'online', 'timestamp': datetime.now().isoformat(), 'user': { 'id': current_user.id, 'username': current_user.username, 'is_admin': current_user.is_admin } }) except Exception as e: api_logger.error(f"Fehler beim Abrufen des System-Status: {str(e)}") return jsonify({'error': 'System-Status nicht verfügbar'}), 500 @api_blueprint.route('/heartbeat', methods=['POST']) @login_required def heartbeat(): """Heartbeat-Endpunkt für Frontend-Verbindungsmonitoring""" try: # Session-Aktivität NICHT in Cookie speichern # session['last_heartbeat'] = datetime.now().strftime('%H:%M:%S') # ENTFERNT session.permanent = True return jsonify({ 'success': True, 'timestamp': datetime.now().isoformat(), 'user_id': current_user.id }) except Exception as e: api_logger.error(f"Fehler im Heartbeat: {str(e)}") return jsonify({'error': 'Heartbeat-Fehler'}), 500 @api_blueprint.route('/session/status', methods=['GET']) def session_status(): """Gibt den aktuellen Session-Status zurück""" try: # Prüfe ob Benutzer über Flask-Login authentifiziert ist if hasattr(current_user, 'is_authenticated') and current_user.is_authenticated: # Benutzer ist angemeldet from backend.config.settings import SESSION_LIFETIME # Session-Informationen sammeln session_start = session.get('session_start', datetime.now().isoformat()) # Standard Session-Lifetime verwenden max_inactive_minutes = int(SESSION_LIFETIME.total_seconds() / 60) # Verbleibende Zeit berechnen (volle Session-Zeit wenn angemeldet) time_left_seconds = int(SESSION_LIFETIME.total_seconds()) return jsonify({ 'success': True, 'user': { 'id': current_user.id, 'username': current_user.username, 'email': current_user.email, 'is_admin': current_user.is_admin if hasattr(current_user, 'is_admin') else False }, 'session': { 'is_authenticated': True, 'max_inactive_minutes': max_inactive_minutes, 'time_left_seconds': time_left_seconds, 'last_activity': datetime.now().isoformat(), 'session_start': session_start }, 'timestamp': datetime.now().isoformat() }) else: # Benutzer ist nicht angemeldet return jsonify({ 'success': True, 'user': None, 'session': { 'is_authenticated': False, 'max_inactive_minutes': 0, 'time_left_seconds': 0, 'last_activity': None, 'session_start': None }, 'timestamp': datetime.now().isoformat() }) except Exception as e: api_logger.error(f"Fehler beim Abrufen des Session-Status: {str(e)}") return jsonify({ 'success': False, 'error': 'Session-Status nicht verfügbar', 'message': str(e) }), 500 @api_blueprint.route('/session/heartbeat', methods=['POST']) @login_required def session_heartbeat(): """Session-Heartbeat für automatische Verlängerung""" try: # Letzte Aktivität NICHT in Cookie speichern (Cookie-Größe reduzieren) # session['last_activity'] = datetime.now().isoformat() # ENTFERNT # Session als permanent markieren für Verlängerung session.permanent = True # Verbleibende Session-Zeit berechnen from backend.config.settings import SESSION_LIFETIME time_left_seconds = int(SESSION_LIFETIME.total_seconds()) api_logger.debug(f"Session-Heartbeat von Benutzer {current_user.username}") return jsonify({ 'success': True, 'message': 'Session aktualisiert', 'time_left_seconds': time_left_seconds, 'timestamp': datetime.now().isoformat() }) except Exception as e: api_logger.error(f"Fehler beim Session-Heartbeat: {str(e)}") return jsonify({ 'success': False, 'error': 'Session-Heartbeat fehlgeschlagen', 'message': str(e) }), 500 @api_blueprint.route('/session/extend', methods=['POST']) @login_required def extend_session(): """Verlängert die aktuelle Session""" try: data = request.get_json() or {} extend_minutes = data.get('extend_minutes', 30) # Session verlängern durch Markierung als permanent session.permanent = True # Neue Aktivitätszeit NICHT in Cookie speichern # session['last_activity'] = datetime.now().isoformat() # ENTFERNT api_logger.info(f"Session für Benutzer {current_user.username} um {extend_minutes} Minuten verlängert") return jsonify({ 'success': True, 'message': f'Session um {extend_minutes} Minuten verlängert', 'extended_minutes': extend_minutes, 'timestamp': datetime.now().isoformat() }) except Exception as e: api_logger.error(f"Fehler beim Verlängern der Session: {str(e)}") return jsonify({ 'success': False, 'error': 'Session-Verlängerung fehlgeschlagen', 'message': str(e) }), 500 @api_blueprint.route('/stats', methods=['GET']) @login_required def get_stats(): """ Hauptstatistiken-Endpunkt für das System. Liefert umfassende Statistiken über Drucker, Jobs und Benutzer. """ try: db_session = get_db_session() # Grundlegende Zählungen total_printers = db_session.query(Printer).count() active_printers = db_session.query(Printer).filter(Printer.active == True).count() total_jobs = db_session.query(Job).count() # Job-Status-Verteilung completed_jobs = db_session.query(Job).filter(Job.status == 'completed').count() running_jobs = db_session.query(Job).filter(Job.status == 'running').count() pending_jobs = db_session.query(Job).filter(Job.status == 'pending').count() failed_jobs = db_session.query(Job).filter(Job.status == 'failed').count() # Benutzer-Statistiken total_users = db_session.query(User).count() active_users = db_session.query(User).filter(User.active == True).count() # Zeitbasierte Statistiken (letzte 24 Stunden) yesterday = datetime.now() - timedelta(days=1) jobs_last_24h = db_session.query(Job).filter(Job.created_at >= yesterday).count() # Gesamtdruckzeit berechnen completed_jobs_with_duration = db_session.query(Job).filter( Job.status == 'completed', Job.duration_minutes.isnot(None) ).all() total_print_hours = sum(job.duration_minutes for job in completed_jobs_with_duration) / 60.0 db_session.close() # Erfolgsrate berechnen success_rate = round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0 stats_data = { 'success': True, 'timestamp': datetime.now().isoformat(), # Grundlegende Zählungen 'total_printers': total_printers, 'active_printers': active_printers, 'online_printers': active_printers, # Alias für Frontend-Kompatibilität 'total_jobs': total_jobs, 'total_users': total_users, 'active_users': active_users, # Job-Statistiken 'completed_jobs': completed_jobs, 'active_jobs': running_jobs, # Alias für Frontend-Kompatibilität 'running_jobs': running_jobs, 'pending_jobs': pending_jobs, 'failed_jobs': failed_jobs, 'jobs_last_24h': jobs_last_24h, # Druckzeit-Statistiken 'total_print_hours': round(total_print_hours, 2), 'completion_rate': success_rate, 'success_rate': success_rate # Alias für Frontend-Kompatibilität } api_logger.info(f"Statistiken abgerufen von Benutzer {current_user.username}") return jsonify(stats_data) except Exception as e: api_logger.error(f"Fehler beim Abrufen der Statistiken: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Laden der Statistiken', 'message': str(e) }), 500 @api_blueprint.route('/admin/system-health', methods=['GET']) @admin_required def get_system_health(): """ System-Gesundheitsstatus für Administratoren. Liefert detaillierte Informationen über den System-Zustand. """ try: db_session = get_db_session() # Datenbankverbindung testen db_healthy = True try: db_session.execute("SELECT 1") except Exception as e: db_healthy = False api_logger.error(f"Datenbankfehler: {str(e)}") # Systemressourcen prüfen import psutil import os # CPU und Speicher cpu_usage = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() disk = psutil.disk_usage('/') # Aktive Drucker prüfen active_printers = db_session.query(Printer).filter(Printer.active == True).count() total_printers = db_session.query(Printer).count() # Laufende Jobs prüfen running_jobs = db_session.query(Job).filter(Job.status == 'running').count() db_session.close() # Gesamtstatus bestimmen overall_status = 'healthy' if not db_healthy or cpu_usage > 90 or memory.percent > 90: overall_status = 'warning' if not db_healthy or cpu_usage > 95 or memory.percent > 95: overall_status = 'critical' health_data = { 'success': True, 'timestamp': datetime.now().isoformat(), 'overall_status': overall_status, # System-Status 'database': { 'status': 'healthy' if db_healthy else 'error', 'connection': db_healthy }, # Systemressourcen 'system': { 'cpu_usage': cpu_usage, 'memory_usage': memory.percent, 'memory_available_gb': round(memory.available / (1024**3), 2), 'disk_usage': disk.percent, 'disk_free_gb': round(disk.free / (1024**3), 2) }, # Drucker-Status 'printers': { 'total': total_printers, 'active': active_printers, 'inactive': total_printers - active_printers }, # Job-Status 'jobs': { 'running': running_jobs } } api_logger.info(f"System-Health abgerufen von Admin {current_user.username}") return jsonify(health_data) except ImportError: # Fallback ohne psutil health_data = { 'success': True, 'timestamp': datetime.now().isoformat(), 'overall_status': 'healthy', 'database': {'status': 'healthy', 'connection': True}, 'system': {'cpu_usage': 0, 'memory_usage': 0, 'disk_usage': 0}, 'printers': {'total': 0, 'active': 0, 'inactive': 0}, 'jobs': {'running': 0}, 'note': 'Eingeschränkte Systemüberwachung (psutil nicht verfügbar)' } return jsonify(health_data) except Exception as e: api_logger.error(f"Fehler beim Abrufen des System-Health: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Laden des System-Status', 'message': str(e) }), 500 @api_blueprint.route('/stats/export', methods=['GET']) @login_required def export_stats(): """ Exportiert Systemstatistiken als CSV-Datei. Erstellt eine herunterladbare CSV-Datei mit allen wichtigen Systemstatistiken. """ try: import csv import io from flask import make_response db_session = get_db_session() # Umfassende Statistiken sammeln stats_data = { 'Gesamte_Drucker': db_session.query(Printer).count(), 'Aktive_Drucker': db_session.query(Printer).filter(Printer.active == True).count(), 'Gesamte_Jobs': db_session.query(Job).count(), 'Abgeschlossene_Jobs': db_session.query(Job).filter(Job.status == 'completed').count(), 'Laufende_Jobs': db_session.query(Job).filter(Job.status == 'running').count(), 'Wartende_Jobs': db_session.query(Job).filter(Job.status == 'pending').count(), 'Fehlgeschlagene_Jobs': db_session.query(Job).filter(Job.status == 'failed').count(), 'Gesamte_Benutzer': db_session.query(User).count(), 'Aktive_Benutzer': db_session.query(User).filter(User.active == True).count(), } # Zeitbasierte Statistiken yesterday = datetime.now() - timedelta(days=1) week_ago = datetime.now() - timedelta(days=7) month_ago = datetime.now() - timedelta(days=30) stats_data.update({ 'Jobs_letzte_24h': db_session.query(Job).filter(Job.created_at >= yesterday).count(), 'Jobs_letzte_Woche': db_session.query(Job).filter(Job.created_at >= week_ago).count(), 'Jobs_letzter_Monat': db_session.query(Job).filter(Job.created_at >= month_ago).count(), }) # Erfolgsrate berechnen total_jobs = stats_data['Gesamte_Jobs'] completed_jobs = stats_data['Abgeschlossene_Jobs'] success_rate = round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0 stats_data['Erfolgsrate_Prozent'] = success_rate # Gesamtdruckzeit berechnen completed_jobs_with_duration = db_session.query(Job).filter( Job.status == 'completed', Job.duration_minutes.isnot(None) ).all() total_print_hours = sum(job.duration_minutes for job in completed_jobs_with_duration) / 60.0 stats_data['Gesamtdruckzeit_Stunden'] = round(total_print_hours, 2) db_session.close() # CSV erstellen output = io.StringIO() writer = csv.writer(output) # Header writer.writerow(['Export_Zeitstempel', datetime.now().strftime('%Y-%m-%d_%H-%M-%S')]) writer.writerow(['Benutzer', current_user.username]) writer.writerow([]) # Leerzeile writer.writerow(['Statistik', 'Wert']) # Daten schreiben for key, value in stats_data.items(): writer.writerow([key.replace('_', ' '), value]) # Detaillierte Drucker-Informationen writer.writerow([]) writer.writerow(['=== Drucker Details ===']) writer.writerow(['Drucker_Name', 'Status', 'Aktiv', 'Letzte_Nutzung']) db_session = get_db_session() printers = db_session.query(Printer).all() for printer in printers: last_job = db_session.query(Job).filter(Job.printer_id == printer.id).order_by(Job.created_at.desc()).first() last_usage = last_job.created_at.strftime('%Y-%m-%d %H:%M') if last_job else 'Nie' writer.writerow([ printer.name, printer.status, 'Ja' if printer.active else 'Nein', last_usage ]) db_session.close() # Response erstellen output.seek(0) response = make_response(output.getvalue()) response.headers['Content-Type'] = 'text/csv; charset=utf-8' response.headers['Content-Disposition'] = f'attachment; filename=MYP_Statistiken_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' api_logger.info(f"Statistiken exportiert von Benutzer {current_user.username}") return response except Exception as e: api_logger.error(f"Fehler beim Exportieren der Statistiken: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Exportieren der Statistiken', 'message': str(e) }), 500 @api_blueprint.route('/admin/error-recovery/status', methods=['GET']) @admin_required def get_error_recovery_status(): """ Fehlerwiederherstellungs-Status für Administratoren. Liefert Informationen über System-Fehler und Wiederherstellungsoptionen. """ try: db_session = get_db_session() # Fehlgeschlagene Jobs der letzten 24 Stunden yesterday = datetime.now() - timedelta(days=1) failed_jobs = db_session.query(Job).filter( Job.status == 'failed', Job.created_at >= yesterday ).all() # Inaktive Drucker inactive_printers = db_session.query(Printer).filter(Printer.active == False).all() # Hängende Jobs (länger als 6 Stunden im Status 'running') six_hours_ago = datetime.now() - timedelta(hours=6) stuck_jobs = db_session.query(Job).filter( Job.status == 'running', Job.start_time <= six_hours_ago ).all() db_session.close() # Wiederherstellungsaktionen bestimmen recovery_actions = [] if failed_jobs: recovery_actions.append({ 'type': 'restart_failed_jobs', 'count': len(failed_jobs), 'description': f'{len(failed_jobs)} fehlgeschlagene Jobs neu starten' }) if inactive_printers: recovery_actions.append({ 'type': 'reactivate_printers', 'count': len(inactive_printers), 'description': f'{len(inactive_printers)} inaktive Drucker reaktivieren' }) if stuck_jobs: recovery_actions.append({ 'type': 'reset_stuck_jobs', 'count': len(stuck_jobs), 'description': f'{len(stuck_jobs)} hängende Jobs zurücksetzen' }) # Gesamtstatus bestimmen status = 'healthy' if failed_jobs or inactive_printers: status = 'warning' if stuck_jobs: status = 'critical' recovery_data = { 'success': True, 'timestamp': datetime.now().isoformat(), 'status': status, 'issues': { 'failed_jobs': len(failed_jobs), 'inactive_printers': len(inactive_printers), 'stuck_jobs': len(stuck_jobs) }, 'recovery_actions': recovery_actions, 'last_check': datetime.now().isoformat() } api_logger.info(f"Error-Recovery-Status abgerufen von Admin {current_user.username}") return jsonify(recovery_data) except Exception as e: api_logger.error(f"Fehler beim Abrufen des Error-Recovery-Status: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Laden des Wiederherstellungs-Status', 'message': str(e) }), 500 @api_blueprint.route('/admin/fix-permissions', methods=['POST']) @admin_required def fix_admin_permissions(): """ Korrigiert die Admin-Berechtigungen im System. Nur für Administratoren zugänglich. """ try: from utils.permissions import fix_all_admin_permissions result = fix_all_admin_permissions() if result['success']: api_logger.info(f"Admin-Berechtigungen korrigiert von {current_user.username}: {result['created']} erstellt, {result['corrected']} aktualisiert") return jsonify({ 'success': True, 'message': 'Admin-Berechtigungen erfolgreich korrigiert', 'details': result }) else: api_logger.error(f"Fehler beim Korrigieren der Admin-Berechtigungen: {result['error']}") return jsonify({ 'success': False, 'error': 'Fehler beim Korrigieren der Berechtigungen', 'message': result['error'] }), 500 except Exception as e: api_logger.error(f"Fehler beim Korrigieren der Admin-Berechtigungen: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler beim Korrigieren der Berechtigungen', 'message': str(e) }), 500