558 lines
20 KiB
Python
558 lines
20 KiB
Python
"""
|
|
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/<int:notification_id>/read', methods=['POST'])
|
|
@login_required
|
|
def mark_notification_read(notification_id):
|
|
"""Markiert eine Benachrichtigung als gelesen"""
|
|
try:
|
|
db_session = get_db_session()
|
|
|
|
notification = db_session.query(Notification).filter(
|
|
Notification.id == notification_id,
|
|
Notification.user_id == current_user.id
|
|
).first()
|
|
|
|
if not notification:
|
|
db_session.close()
|
|
return jsonify({'error': 'Benachrichtigung nicht gefunden'}), 404
|
|
|
|
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")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'message': 'Benachrichtigung als gelesen markiert'
|
|
})
|
|
|
|
except Exception as e:
|
|
api_logger.error(f"Fehler beim Markieren der Benachrichtigung: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Markieren der Benachrichtigung'}), 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()
|
|
|
|
stats_data = {
|
|
'success': True,
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
|
# Grundlegende Zählungen
|
|
'total_printers': total_printers,
|
|
'active_printers': active_printers,
|
|
'total_jobs': total_jobs,
|
|
'total_users': total_users,
|
|
'active_users': active_users,
|
|
|
|
# Job-Statistiken
|
|
'completed_jobs': completed_jobs,
|
|
'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': round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0
|
|
}
|
|
|
|
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('/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 |