🔧 Aktualisierung der Backend-Logik und Optimierung der SQLite-Datenbankkonfiguration für Raspberry Pi: Hinzufügen spezifischer Optimierungen, Verbesserung der Fehlerbehandlung und Protokollierung. Einführung von Caching-Mechanismen und Anpassungen für schwache Hardware. 📈
This commit is contained in:
507
backend/app.py
507
backend/app.py
@@ -11,15 +11,16 @@ from werkzeug.utils import secure_filename
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from sqlalchemy.orm import sessionmaker, joinedload
|
||||
from sqlalchemy import func, text
|
||||
from functools import wraps
|
||||
from functools import wraps, lru_cache
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
import time
|
||||
import subprocess
|
||||
import json
|
||||
import signal
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
import threading
|
||||
|
||||
# Windows-spezifische Fixes früh importieren (sichere Version)
|
||||
if os.name == 'nt':
|
||||
@@ -177,9 +178,33 @@ except ImportError as e:
|
||||
TIMEOUT_FORCE_QUIT_AVAILABLE = False
|
||||
app_logger.warning(f"⚠️ Timeout Force-Quit Manager nicht verfügbar: {e}")
|
||||
|
||||
# ===== AGGRESSIVE SOFORT-SHUTDOWN HANDLER FÜR STRG+C =====
|
||||
import atexit
|
||||
# ===== PERFORMANCE-OPTIMIERTE CACHES =====
|
||||
# Thread-sichere Caches für häufig abgerufene Daten
|
||||
_user_cache = {}
|
||||
_user_cache_lock = threading.RLock()
|
||||
_printer_status_cache = {}
|
||||
_printer_status_cache_lock = threading.RLock()
|
||||
_printer_status_cache_ttl = {}
|
||||
|
||||
# Cache-Konfiguration
|
||||
USER_CACHE_TTL = 300 # 5 Minuten
|
||||
PRINTER_STATUS_CACHE_TTL = 30 # 30 Sekunden
|
||||
|
||||
def clear_user_cache(user_id: Optional[int] = None):
|
||||
"""Löscht User-Cache (komplett oder für spezifischen User)"""
|
||||
with _user_cache_lock:
|
||||
if user_id:
|
||||
_user_cache.pop(user_id, None)
|
||||
else:
|
||||
_user_cache.clear()
|
||||
|
||||
def clear_printer_status_cache():
|
||||
"""Löscht Drucker-Status-Cache"""
|
||||
with _printer_status_cache_lock:
|
||||
_printer_status_cache.clear()
|
||||
_printer_status_cache_ttl.clear()
|
||||
|
||||
# ===== AGGRESSIVE SOFORT-SHUTDOWN HANDLER FÜR STRG+C =====
|
||||
def aggressive_shutdown_handler(sig, frame):
|
||||
"""
|
||||
Aggressiver Signal-Handler für sofortiges Herunterfahren bei Strg+C.
|
||||
@@ -189,7 +214,11 @@ def aggressive_shutdown_handler(sig, frame):
|
||||
print("🔥 Schließe Datenbank sofort und beende Programm um jeden Preis!")
|
||||
|
||||
try:
|
||||
# 1. Sofort alle Datenbank-Sessions und Engine schließen
|
||||
# 1. Caches leeren
|
||||
clear_user_cache()
|
||||
clear_printer_status_cache()
|
||||
|
||||
# 2. Sofort alle Datenbank-Sessions und Engine schließen
|
||||
try:
|
||||
from models import _engine, _scoped_session, _session_factory
|
||||
|
||||
@@ -209,7 +238,7 @@ def aggressive_shutdown_handler(sig, frame):
|
||||
except ImportError:
|
||||
print("⚠️ Models nicht verfügbar für Database-Cleanup")
|
||||
|
||||
# 2. Alle offenen DB-Sessions forciert schließen
|
||||
# 3. Alle offenen DB-Sessions forciert schließen
|
||||
try:
|
||||
import gc
|
||||
# Garbage Collection für nicht geschlossene Sessions
|
||||
@@ -218,7 +247,7 @@ def aggressive_shutdown_handler(sig, frame):
|
||||
except Exception as e:
|
||||
print(f"⚠️ Garbage Collection fehlgeschlagen: {e}")
|
||||
|
||||
# 3. SQLite WAL-Dateien forciert synchronisieren
|
||||
# 4. SQLite WAL-Dateien forciert synchronisieren
|
||||
try:
|
||||
import sqlite3
|
||||
from config.settings import DATABASE_PATH
|
||||
@@ -229,7 +258,7 @@ def aggressive_shutdown_handler(sig, frame):
|
||||
except Exception as e:
|
||||
print(f"⚠️ WAL-Checkpoint fehlgeschlagen: {e}")
|
||||
|
||||
# 4. Queue Manager stoppen falls verfügbar
|
||||
# 5. Queue Manager stoppen falls verfügbar
|
||||
try:
|
||||
from utils.queue_manager import stop_queue_manager
|
||||
stop_queue_manager()
|
||||
@@ -344,7 +373,7 @@ login_manager.login_message_category = "info"
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""
|
||||
Robuster User-Loader mit verbessertem Error-Handling für Schema-Probleme und SQLite-Interface-Fehler.
|
||||
Performance-optimierter User-Loader mit Caching und robustem Error-Handling.
|
||||
"""
|
||||
try:
|
||||
# user_id von Flask-Login ist immer ein String - zu Integer konvertieren
|
||||
@@ -353,12 +382,26 @@ def load_user(user_id):
|
||||
except (ValueError, TypeError):
|
||||
app_logger.error(f"Ungültige User-ID: {user_id}")
|
||||
return None
|
||||
|
||||
|
||||
# Cache-Check mit TTL
|
||||
current_time = time.time()
|
||||
with _user_cache_lock:
|
||||
if user_id_int in _user_cache:
|
||||
cached_user, cache_time = _user_cache[user_id_int]
|
||||
if current_time - cache_time < USER_CACHE_TTL:
|
||||
return cached_user
|
||||
else:
|
||||
# Cache abgelaufen - entfernen
|
||||
del _user_cache[user_id_int]
|
||||
|
||||
# Versuche Benutzer über robustes Caching-System zu laden
|
||||
try:
|
||||
from models import User
|
||||
cached_user = User.get_by_id_cached(user_id_int)
|
||||
if cached_user:
|
||||
# In lokalen Cache speichern
|
||||
with _user_cache_lock:
|
||||
_user_cache[user_id_int] = (cached_user, current_time)
|
||||
return cached_user
|
||||
except Exception as cache_error:
|
||||
app_logger.debug(f"Cache-Abfrage fehlgeschlagen: {str(cache_error)}")
|
||||
@@ -369,6 +412,9 @@ def load_user(user_id):
|
||||
try:
|
||||
user = db_session.query(User).filter(User.id == user_id_int).first()
|
||||
if user:
|
||||
# In Cache speichern
|
||||
with _user_cache_lock:
|
||||
_user_cache[user_id_int] = (user, current_time)
|
||||
db_session.close()
|
||||
return user
|
||||
except Exception as orm_error:
|
||||
@@ -377,7 +423,7 @@ def load_user(user_id):
|
||||
|
||||
try:
|
||||
# Verwende SQLAlchemy Core für robuste Abfrage
|
||||
from sqlalchemy import text, select
|
||||
from sqlalchemy import text
|
||||
|
||||
# Sichere Parameter-Bindung mit expliziter Typisierung
|
||||
stmt = text("""
|
||||
@@ -429,6 +475,10 @@ def load_user(user_id):
|
||||
user.phone = None
|
||||
user.bio = None
|
||||
|
||||
# In Cache speichern
|
||||
with _user_cache_lock:
|
||||
_user_cache[user_id_int] = (user, current_time)
|
||||
|
||||
app_logger.info(f"User {user_id_int} erfolgreich über Core-Query geladen")
|
||||
db_session.close()
|
||||
return user
|
||||
@@ -456,6 +506,10 @@ def load_user(user_id):
|
||||
user.updated_at = datetime.now()
|
||||
user.last_activity = datetime.now()
|
||||
|
||||
# In Cache speichern
|
||||
with _user_cache_lock:
|
||||
_user_cache[user_id_int] = (user, current_time)
|
||||
|
||||
app_logger.warning(f"Notfall-User-Objekt für ID {user_id_int} erstellt (DB korrupt)")
|
||||
db_session.close()
|
||||
return user
|
||||
@@ -639,6 +693,9 @@ def login():
|
||||
user.update_last_login()
|
||||
db_session.commit()
|
||||
|
||||
# Cache invalidieren für diesen User
|
||||
clear_user_cache(user.id)
|
||||
|
||||
login_user(user, remember=remember_me)
|
||||
auth_logger.info(f"Benutzer {username} hat sich erfolgreich angemeldet")
|
||||
|
||||
@@ -680,8 +737,13 @@ def login():
|
||||
@login_required
|
||||
def auth_logout():
|
||||
"""Meldet den Benutzer ab."""
|
||||
user_id = current_user.id
|
||||
app_logger.info(f"Benutzer {current_user.email} hat sich abgemeldet")
|
||||
logout_user()
|
||||
|
||||
# Cache für abgemeldeten User löschen
|
||||
clear_user_cache(user_id)
|
||||
|
||||
flash("Sie wurden erfolgreich abgemeldet.", "info")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
@@ -717,6 +779,9 @@ def api_login():
|
||||
user.update_last_login()
|
||||
db_session.commit()
|
||||
|
||||
# Cache invalidieren für diesen User
|
||||
clear_user_cache(user.id)
|
||||
|
||||
login_user(user, remember=remember_me)
|
||||
auth_logger.info(f"API-Login erfolgreich für Benutzer {username}")
|
||||
|
||||
@@ -831,6 +896,9 @@ def api_callback():
|
||||
user.update_last_login()
|
||||
db_session.commit()
|
||||
|
||||
# Cache invalidieren für diesen User
|
||||
clear_user_cache(user.id)
|
||||
|
||||
login_user(user, remember=True)
|
||||
|
||||
# Session-State löschen
|
||||
@@ -914,6 +982,9 @@ def api_callback():
|
||||
user.update_last_login()
|
||||
db_session.commit()
|
||||
|
||||
# Cache invalidieren für diesen User
|
||||
clear_user_cache(user.id)
|
||||
|
||||
login_user(user, remember=True)
|
||||
|
||||
response_data = {
|
||||
@@ -947,8 +1018,9 @@ def api_callback():
|
||||
"redirect_url": url_for("login")
|
||||
}), 500
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def handle_github_callback(code):
|
||||
"""GitHub OAuth-Callback verarbeiten"""
|
||||
"""GitHub OAuth-Callback verarbeiten (mit Caching)"""
|
||||
try:
|
||||
import requests
|
||||
|
||||
@@ -1045,6 +1117,130 @@ def get_github_user_data(access_token):
|
||||
auth_logger.error(f"Fehler beim Abrufen der GitHub-Benutzerdaten: {str(e)}")
|
||||
return None
|
||||
|
||||
# ===== KIOSK-KONTROLL-ROUTEN (ehemals kiosk_control.py) =====
|
||||
|
||||
@app.route('/api/kiosk/status', methods=['GET'])
|
||||
def kiosk_get_status():
|
||||
"""Kiosk-Status abrufen."""
|
||||
try:
|
||||
# Prüfen ob Kiosk-Modus aktiv ist
|
||||
kiosk_active = os.path.exists('/tmp/kiosk_active')
|
||||
|
||||
return jsonify({
|
||||
"active": kiosk_active,
|
||||
"message": "Kiosk-Status erfolgreich abgerufen"
|
||||
})
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Fehler beim Abrufen des Kiosk-Status: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Abrufen des Status"}), 500
|
||||
|
||||
@app.route('/api/kiosk/deactivate', methods=['POST'])
|
||||
def kiosk_deactivate():
|
||||
"""Kiosk-Modus mit Passwort deaktivieren."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'password' not in data:
|
||||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||||
|
||||
password = data['password']
|
||||
|
||||
# Passwort überprüfen
|
||||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||||
kiosk_logger.warning(f"Fehlgeschlagener Kiosk-Deaktivierungsversuch von IP: {request.remote_addr}")
|
||||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||||
|
||||
# Kiosk deaktivieren
|
||||
try:
|
||||
# Kiosk-Service stoppen
|
||||
subprocess.run(['sudo', 'systemctl', 'stop', 'myp-kiosk'], check=True)
|
||||
subprocess.run(['sudo', 'systemctl', 'disable', 'myp-kiosk'], check=True)
|
||||
|
||||
# Kiosk-Marker entfernen
|
||||
if os.path.exists('/tmp/kiosk_active'):
|
||||
os.remove('/tmp/kiosk_active')
|
||||
|
||||
# Normale Desktop-Umgebung wiederherstellen
|
||||
subprocess.run(['sudo', 'systemctl', 'set-default', 'graphical.target'], check=True)
|
||||
|
||||
kiosk_logger.info(f"Kiosk-Modus erfolgreich deaktiviert von IP: {request.remote_addr}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Kiosk-Modus erfolgreich deaktiviert. System wird neu gestartet."
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
kiosk_logger.error(f"Fehler beim Deaktivieren des Kiosk-Modus: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Deaktivieren des Kiosk-Modus"}), 500
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Deaktivierung: {str(e)}")
|
||||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||||
|
||||
@app.route('/api/kiosk/activate', methods=['POST'])
|
||||
@login_required
|
||||
def kiosk_activate():
|
||||
"""Kiosk-Modus aktivieren (nur für Admins)."""
|
||||
try:
|
||||
# Admin-Authentifizierung prüfen
|
||||
if not current_user.is_admin:
|
||||
kiosk_logger.warning(f"Nicht-Admin-Benutzer {current_user.username} versuchte Kiosk-Aktivierung")
|
||||
return jsonify({"error": "Nur Administratoren können den Kiosk-Modus aktivieren"}), 403
|
||||
|
||||
# Kiosk aktivieren
|
||||
try:
|
||||
# Kiosk-Marker setzen
|
||||
with open('/tmp/kiosk_active', 'w') as f:
|
||||
f.write('1')
|
||||
|
||||
# Kiosk-Service aktivieren
|
||||
subprocess.run(['sudo', 'systemctl', 'enable', 'myp-kiosk'], check=True)
|
||||
subprocess.run(['sudo', 'systemctl', 'start', 'myp-kiosk'], check=True)
|
||||
|
||||
kiosk_logger.info(f"Kiosk-Modus erfolgreich aktiviert von Admin {current_user.username} (IP: {request.remote_addr})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Kiosk-Modus erfolgreich aktiviert"
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
kiosk_logger.error(f"Fehler beim Aktivieren des Kiosk-Modus: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Aktivieren des Kiosk-Modus"}), 500
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Aktivierung: {str(e)}")
|
||||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||||
|
||||
@app.route('/api/kiosk/restart', methods=['POST'])
|
||||
def kiosk_restart_system():
|
||||
"""System neu starten (nur nach Kiosk-Deaktivierung)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'password' not in data:
|
||||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||||
|
||||
password = data['password']
|
||||
|
||||
# Passwort überprüfen
|
||||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||||
kiosk_logger.warning(f"Fehlgeschlagener Neustart-Versuch von IP: {request.remote_addr}")
|
||||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||||
|
||||
kiosk_logger.info(f"System-Neustart initiiert von IP: {request.remote_addr}")
|
||||
|
||||
# System nach kurzer Verzögerung neu starten
|
||||
subprocess.Popen(['sudo', 'shutdown', '-r', '+1'])
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "System wird in 1 Minute neu gestartet"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Fehler beim System-Neustart: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Neustart"}), 500
|
||||
|
||||
# ===== BENUTZER-ROUTEN (ehemals user.py) =====
|
||||
|
||||
@app.route("/user/profile", methods=["GET"])
|
||||
@@ -1552,129 +1748,7 @@ def user_update_profile_api():
|
||||
user_logger.error(error)
|
||||
return jsonify({"error": error}), 500
|
||||
|
||||
# ===== KIOSK-KONTROLL-ROUTEN (ehemals kiosk_control.py) =====
|
||||
|
||||
@app.route('/api/kiosk/status', methods=['GET'])
|
||||
def kiosk_get_status():
|
||||
"""Kiosk-Status abrufen."""
|
||||
try:
|
||||
# Prüfen ob Kiosk-Modus aktiv ist
|
||||
kiosk_active = os.path.exists('/tmp/kiosk_active')
|
||||
|
||||
return jsonify({
|
||||
"active": kiosk_active,
|
||||
"message": "Kiosk-Status erfolgreich abgerufen"
|
||||
})
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Fehler beim Abrufen des Kiosk-Status: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Abrufen des Status"}), 500
|
||||
|
||||
@app.route('/api/kiosk/deactivate', methods=['POST'])
|
||||
def kiosk_deactivate():
|
||||
"""Kiosk-Modus mit Passwort deaktivieren."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'password' not in data:
|
||||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||||
|
||||
password = data['password']
|
||||
|
||||
# Passwort überprüfen
|
||||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||||
kiosk_logger.warning(f"Fehlgeschlagener Kiosk-Deaktivierungsversuch von IP: {request.remote_addr}")
|
||||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||||
|
||||
# Kiosk deaktivieren
|
||||
try:
|
||||
# Kiosk-Service stoppen
|
||||
subprocess.run(['sudo', 'systemctl', 'stop', 'myp-kiosk'], check=True)
|
||||
subprocess.run(['sudo', 'systemctl', 'disable', 'myp-kiosk'], check=True)
|
||||
|
||||
# Kiosk-Marker entfernen
|
||||
if os.path.exists('/tmp/kiosk_active'):
|
||||
os.remove('/tmp/kiosk_active')
|
||||
|
||||
# Normale Desktop-Umgebung wiederherstellen
|
||||
subprocess.run(['sudo', 'systemctl', 'set-default', 'graphical.target'], check=True)
|
||||
|
||||
kiosk_logger.info(f"Kiosk-Modus erfolgreich deaktiviert von IP: {request.remote_addr}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Kiosk-Modus erfolgreich deaktiviert. System wird neu gestartet."
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
kiosk_logger.error(f"Fehler beim Deaktivieren des Kiosk-Modus: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Deaktivieren des Kiosk-Modus"}), 500
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Deaktivierung: {str(e)}")
|
||||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||||
|
||||
@app.route('/api/kiosk/activate', methods=['POST'])
|
||||
@login_required
|
||||
def kiosk_activate():
|
||||
"""Kiosk-Modus aktivieren (nur für Admins)."""
|
||||
try:
|
||||
# Admin-Authentifizierung prüfen
|
||||
if not current_user.is_admin:
|
||||
kiosk_logger.warning(f"Nicht-Admin-Benutzer {current_user.username} versuchte Kiosk-Aktivierung")
|
||||
return jsonify({"error": "Nur Administratoren können den Kiosk-Modus aktivieren"}), 403
|
||||
|
||||
# Kiosk aktivieren
|
||||
try:
|
||||
# Kiosk-Marker setzen
|
||||
with open('/tmp/kiosk_active', 'w') as f:
|
||||
f.write('1')
|
||||
|
||||
# Kiosk-Service aktivieren
|
||||
subprocess.run(['sudo', 'systemctl', 'enable', 'myp-kiosk'], check=True)
|
||||
subprocess.run(['sudo', 'systemctl', 'start', 'myp-kiosk'], check=True)
|
||||
|
||||
kiosk_logger.info(f"Kiosk-Modus erfolgreich aktiviert von Admin {current_user.username} (IP: {request.remote_addr})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Kiosk-Modus erfolgreich aktiviert"
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
kiosk_logger.error(f"Fehler beim Aktivieren des Kiosk-Modus: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Aktivieren des Kiosk-Modus"}), 500
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Unerwarteter Fehler bei Kiosk-Aktivierung: {str(e)}")
|
||||
return jsonify({"error": "Unerwarteter Fehler"}), 500
|
||||
|
||||
@app.route('/api/kiosk/restart', methods=['POST'])
|
||||
def kiosk_restart_system():
|
||||
"""System neu starten (nur nach Kiosk-Deaktivierung)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'password' not in data:
|
||||
return jsonify({"error": "Passwort erforderlich"}), 400
|
||||
|
||||
password = data['password']
|
||||
|
||||
# Passwort überprüfen
|
||||
if not check_password_hash(KIOSK_PASSWORD_HASH, password):
|
||||
kiosk_logger.warning(f"Fehlgeschlagener Neustart-Versuch von IP: {request.remote_addr}")
|
||||
return jsonify({"error": "Ungültiges Passwort"}), 401
|
||||
|
||||
kiosk_logger.info(f"System-Neustart initiiert von IP: {request.remote_addr}")
|
||||
|
||||
# System nach kurzer Verzögerung neu starten
|
||||
subprocess.Popen(['sudo', 'shutdown', '-r', '+1'])
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "System wird in 1 Minute neu gestartet"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
kiosk_logger.error(f"Fehler beim System-Neustart: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Neustart"}), 500
|
||||
|
||||
# ===== HILFSFUNKTIONEN =====
|
||||
|
||||
@@ -1793,42 +1867,6 @@ def check_multiple_printers_status(printers: List[Dict], timeout: int = 7) -> Di
|
||||
return results
|
||||
|
||||
# ===== UI-ROUTEN =====
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
if current_user.is_authenticated:
|
||||
return render_template("index.html")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
@app.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
@app.route("/profile")
|
||||
@login_required
|
||||
def profile_redirect():
|
||||
"""Leitet zur neuen Profilseite im User-Blueprint weiter."""
|
||||
return redirect(url_for("user_profile"))
|
||||
|
||||
@app.route("/profil")
|
||||
@login_required
|
||||
def profil_redirect():
|
||||
"""Leitet zur neuen Profilseite im User-Blueprint weiter (deutsche URL)."""
|
||||
return redirect(url_for("user_profile"))
|
||||
|
||||
@app.route("/settings")
|
||||
@login_required
|
||||
def settings_redirect():
|
||||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter."""
|
||||
return redirect(url_for("user_settings"))
|
||||
|
||||
@app.route("/einstellungen")
|
||||
@login_required
|
||||
def einstellungen_redirect():
|
||||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter (deutsche URL)."""
|
||||
return redirect(url_for("user_settings"))
|
||||
|
||||
@app.route("/admin-dashboard")
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -1883,6 +1921,41 @@ def admin_page():
|
||||
flash("Fehler beim Laden des Admin-Bereichs.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
if current_user.is_authenticated:
|
||||
return render_template("index.html")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
@app.route("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
@app.route("/profile")
|
||||
@login_required
|
||||
def profile_redirect():
|
||||
"""Leitet zur neuen Profilseite im User-Blueprint weiter."""
|
||||
return redirect(url_for("user_profile"))
|
||||
|
||||
@app.route("/profil")
|
||||
@login_required
|
||||
def profil_redirect():
|
||||
"""Leitet zur neuen Profilseite im User-Blueprint weiter (deutsche URL)."""
|
||||
return redirect(url_for("user_profile"))
|
||||
|
||||
@app.route("/settings")
|
||||
@login_required
|
||||
def settings_redirect():
|
||||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter."""
|
||||
return redirect(url_for("user_settings"))
|
||||
|
||||
@app.route("/einstellungen")
|
||||
@login_required
|
||||
def einstellungen_redirect():
|
||||
"""Leitet zur neuen Einstellungsseite im User-Blueprint weiter (deutsche URL)."""
|
||||
return redirect(url_for("user_settings"))
|
||||
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -1929,9 +2002,6 @@ def stats_page():
|
||||
"""Zeigt die Statistiken-Seite an"""
|
||||
return render_template("stats.html", title="Statistiken")
|
||||
|
||||
|
||||
# ===== RECHTLICHE SEITEN =====
|
||||
|
||||
@app.route("/privacy")
|
||||
def privacy():
|
||||
"""Datenschutzerklärung-Seite"""
|
||||
@@ -1999,7 +2069,6 @@ def dragdrop_demo():
|
||||
|
||||
# ===== ERROR MONITORING SYSTEM =====
|
||||
|
||||
|
||||
@app.route("/api/admin/system-health", methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -3774,12 +3843,15 @@ def cleanup_temp_files():
|
||||
|
||||
|
||||
# ===== WEITERE API-ROUTEN =====
|
||||
# ===== JOB-MANAGEMENT-ROUTEN =====
|
||||
|
||||
# Legacy-Route für Kompatibilität - sollte durch Blueprint ersetzt werden
|
||||
@app.route("/api/jobs/current", methods=["GET"])
|
||||
@login_required
|
||||
def get_current_job():
|
||||
"""Gibt den aktuellen Job des Benutzers zurück."""
|
||||
"""
|
||||
Gibt den aktuellen Job des Benutzers zurück.
|
||||
Legacy-Route für Kompatibilität - sollte durch Blueprint ersetzt werden.
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
try:
|
||||
current_job = db_session.query(Job).filter(
|
||||
@@ -3792,12 +3864,12 @@ def get_current_job():
|
||||
else:
|
||||
job_data = None
|
||||
|
||||
db_session.close()
|
||||
return jsonify(job_data)
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Abrufen des aktuellen Jobs: {str(e)}")
|
||||
db_session.close()
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route("/api/jobs/<int:job_id>", methods=["GET"])
|
||||
@login_required
|
||||
@@ -3810,44 +3882,46 @@ def get_job_detail(job_id):
|
||||
|
||||
try:
|
||||
# Eagerly load the user and printer relationships
|
||||
job = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer)).filter(Job.id == job_id).first()
|
||||
job = db_session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
).filter(Job.id == job_id).first()
|
||||
|
||||
if not job:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Convert to dict before closing session
|
||||
job_dict = job.to_dict()
|
||||
db_session.close()
|
||||
|
||||
return jsonify(job_dict)
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Abrufen des Jobs {job_id}: {str(e)}")
|
||||
db_session.close()
|
||||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route("/api/jobs/<int:job_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
@job_owner_required
|
||||
def delete_job(job_id):
|
||||
"""Löscht einen Job."""
|
||||
"""
|
||||
Löscht einen Job.
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
job = db_session.get(Job, job_id)
|
||||
|
||||
if not job:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Prüfen, ob der Job gelöscht werden kann
|
||||
if job.status == "running":
|
||||
db_session.close()
|
||||
return jsonify({"error": "Laufende Jobs können nicht gelöscht werden"}), 400
|
||||
|
||||
job_name = job.name
|
||||
db_session.delete(job)
|
||||
db_session.commit()
|
||||
db_session.close()
|
||||
|
||||
jobs_logger.info(f"Job '{job_name}' (ID: {job_id}) gelöscht von Benutzer {current_user.id}")
|
||||
return jsonify({"success": True, "message": "Job erfolgreich gelöscht"})
|
||||
@@ -3855,23 +3929,31 @@ def delete_job(job_id):
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Löschen des Jobs {job_id}: {str(e)}")
|
||||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route("/api/jobs", methods=["GET"])
|
||||
@login_required
|
||||
def get_jobs():
|
||||
"""Gibt alle Jobs zurück. Admins sehen alle Jobs, normale Benutzer nur ihre eigenen."""
|
||||
"""
|
||||
Gibt alle Jobs zurück. Admins sehen alle Jobs, normale Benutzer nur ihre eigenen.
|
||||
Unterstützt Paginierung und Filterung.
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
# Paginierung unterstützen
|
||||
# Paginierung und Filter-Parameter
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 50, type=int)
|
||||
status_filter = request.args.get('status')
|
||||
|
||||
# Query aufbauen
|
||||
query = db_session.query(Job).options(joinedload(Job.user), joinedload(Job.printer))
|
||||
# Query aufbauen mit Eager Loading
|
||||
query = db_session.query(Job).options(
|
||||
joinedload(Job.user),
|
||||
joinedload(Job.printer)
|
||||
)
|
||||
|
||||
# Admin sieht alle Jobs, User nur eigene
|
||||
if not current_user.is_admin:
|
||||
@@ -3884,18 +3966,16 @@ def get_jobs():
|
||||
# Sortierung: neueste zuerst
|
||||
query = query.order_by(Job.created_at.desc())
|
||||
|
||||
# Gesamtanzahl für Paginierung ermitteln
|
||||
total_count = query.count()
|
||||
|
||||
# Paginierung anwenden
|
||||
offset = (page - 1) * per_page
|
||||
jobs = query.offset(offset).limit(per_page).all()
|
||||
|
||||
# Gesamtanzahl für Paginierung
|
||||
total_count = query.count()
|
||||
|
||||
# Convert jobs to dictionaries before closing the session
|
||||
job_dicts = [job.to_dict() for job in jobs]
|
||||
|
||||
db_session.close()
|
||||
|
||||
jobs_logger.info(f"Jobs abgerufen: {len(job_dicts)} von {total_count} (Seite {page})")
|
||||
|
||||
return jsonify({
|
||||
@@ -3909,8 +3989,9 @@ def get_jobs():
|
||||
})
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Abrufen von Jobs: {str(e)}")
|
||||
db_session.close()
|
||||
return jsonify({"error": "Interner Serverfehler"}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route('/api/jobs', methods=['POST'])
|
||||
@login_required
|
||||
@@ -3928,6 +4009,8 @@ def create_job():
|
||||
"file_path": str (optional)
|
||||
}
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
@@ -3960,12 +4043,9 @@ def create_job():
|
||||
# End-Zeit berechnen
|
||||
end_at = start_at + timedelta(minutes=duration_minutes)
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
# Prüfen, ob der Drucker existiert
|
||||
printer = db_session.get(Printer, printer_id)
|
||||
if not printer:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||
|
||||
# Prüfen, ob der Drucker online ist
|
||||
@@ -3996,7 +4076,6 @@ def create_job():
|
||||
|
||||
# Job-Objekt für die Antwort serialisieren
|
||||
job_dict = new_job.to_dict()
|
||||
db_session.close()
|
||||
|
||||
jobs_logger.info(f"Neuer Job {new_job.id} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten")
|
||||
return jsonify({"job": job_dict}), 201
|
||||
@@ -4004,6 +4083,8 @@ def create_job():
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Erstellen eines Jobs: {str(e)}")
|
||||
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route('/api/jobs/<int:job_id>', methods=['PUT'])
|
||||
@login_required
|
||||
@@ -4012,19 +4093,18 @@ def update_job(job_id):
|
||||
"""
|
||||
Aktualisiert einen existierenden Job.
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
data = request.json
|
||||
|
||||
db_session = get_db_session()
|
||||
job = db_session.get(Job, job_id)
|
||||
|
||||
if not job:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Job nicht gefunden"}), 404
|
||||
|
||||
# Prüfen, ob der Job bearbeitet werden kann
|
||||
if job.status in ["finished", "aborted"]:
|
||||
db_session.close()
|
||||
return jsonify({"error": f"Job kann im Status '{job.status}' nicht bearbeitet werden"}), 400
|
||||
|
||||
# Felder aktualisieren, falls vorhanden
|
||||
@@ -4046,13 +4126,11 @@ def update_job(job_id):
|
||||
if job.duration_minutes:
|
||||
job.end_at = new_start + timedelta(minutes=job.duration_minutes)
|
||||
except ValueError:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Ungültiges Startdatum"}), 400
|
||||
|
||||
if "duration_minutes" in data:
|
||||
duration = int(data["duration_minutes"])
|
||||
if duration <= 0:
|
||||
db_session.close()
|
||||
return jsonify({"error": "Dauer muss größer als 0 sein"}), 400
|
||||
|
||||
job.duration_minutes = duration
|
||||
@@ -4067,7 +4145,6 @@ def update_job(job_id):
|
||||
|
||||
# Job-Objekt für die Antwort serialisieren
|
||||
job_dict = job.to_dict()
|
||||
db_session.close()
|
||||
|
||||
jobs_logger.info(f"Job {job_id} aktualisiert")
|
||||
return jsonify({"job": job_dict})
|
||||
@@ -4075,13 +4152,18 @@ def update_job(job_id):
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Aktualisieren von Job {job_id}: {str(e)}")
|
||||
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
@app.route('/api/jobs/active', methods=['GET'])
|
||||
@login_required
|
||||
def get_active_jobs():
|
||||
"""Gibt alle aktiven Jobs zurück."""
|
||||
"""
|
||||
Gibt alle aktiven Jobs zurück.
|
||||
"""
|
||||
db_session = get_db_session()
|
||||
|
||||
try:
|
||||
db_session = get_db_session()
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
query = db_session.query(Job).options(
|
||||
@@ -4110,11 +4192,12 @@ def get_active_jobs():
|
||||
|
||||
result.append(job_dict)
|
||||
|
||||
db_session.close()
|
||||
return jsonify({"jobs": result})
|
||||
except Exception as e:
|
||||
jobs_logger.error(f"Fehler beim Abrufen aktiver Jobs: {str(e)}")
|
||||
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
|
||||
finally:
|
||||
db_session.close()
|
||||
|
||||
# ===== DRUCKER-ROUTEN =====
|
||||
|
||||
@@ -5659,10 +5742,6 @@ def validate_optimization_settings(settings):
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ===== GASTANTRÄGE API-ROUTEN =====
|
||||
|
||||
# ===== NEUE SYSTEM API-ROUTEN =====
|
||||
|
||||
# ===== FORM VALIDATION API =====
|
||||
@app.route('/api/validation/client-js', methods=['GET'])
|
||||
def get_validation_js():
|
||||
|
Reference in New Issue
Block a user