🔧 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:
2025-06-01 22:43:42 +02:00
parent 317f7dc9dc
commit 62efe03887
40 changed files with 3856 additions and 229 deletions

View File

@@ -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():